story: tab-mode
This commit is contained in:
parent
7fc90eba74
commit
f3f4c43e81
|
|
@ -32,6 +32,7 @@ use crate::{
|
|||
Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT,
|
||||
Powerful, PreviousGuardianAction, Role, RoleTitle,
|
||||
},
|
||||
team::Team,
|
||||
};
|
||||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
|
@ -242,6 +243,16 @@ impl Character {
|
|||
!self.is_wolf()
|
||||
}
|
||||
|
||||
pub fn team(&self) -> Team {
|
||||
if let Alignment::Traitor = self.alignment() {
|
||||
return Team::AnyEvil;
|
||||
}
|
||||
if self.is_wolf() {
|
||||
return Team::Wolves;
|
||||
}
|
||||
Team::Village
|
||||
}
|
||||
|
||||
pub const fn known_elder(&self) -> bool {
|
||||
matches!(
|
||||
self.role,
|
||||
|
|
@ -600,8 +611,15 @@ impl Character {
|
|||
})
|
||||
}
|
||||
Role::MasonLeader { .. } => {
|
||||
log::error!(
|
||||
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
||||
log::debug!(
|
||||
"night_action_prompts got to MasonLeader;
|
||||
mason leader alive: {}; night: {night}; current prompt titles: {}",
|
||||
self.alive(),
|
||||
prompts
|
||||
.iter()
|
||||
.map(|p| p.title().to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,30 @@ pub enum DiedTo {
|
|||
}
|
||||
|
||||
impl DiedTo {
|
||||
pub const fn day(&self) -> Option<NonZeroU8> {
|
||||
match self {
|
||||
DiedTo::Execution { day } => Some(*day),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub const fn night(&self) -> Option<u8> {
|
||||
Some(match self {
|
||||
DiedTo::Execution { .. } => return None,
|
||||
|
||||
DiedTo::MapleWolf { night, .. }
|
||||
| DiedTo::MapleWolfStarved { night }
|
||||
| DiedTo::Militia { night, .. }
|
||||
| DiedTo::Wolfpack { night, .. }
|
||||
| DiedTo::AlphaWolf { night, .. }
|
||||
| DiedTo::Shapeshift { night, .. }
|
||||
| DiedTo::Hunter { night, .. }
|
||||
| DiedTo::GuardianProtecting { night, .. }
|
||||
| DiedTo::PyreMasterLynchMob { night, .. }
|
||||
| DiedTo::PyreMaster { night, .. } => night.get(),
|
||||
|
||||
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => *night,
|
||||
})
|
||||
}
|
||||
pub fn next_night(&self) -> Option<DiedTo> {
|
||||
let mut next = self.clone();
|
||||
match &mut next {
|
||||
|
|
|
|||
|
|
@ -192,6 +192,7 @@ impl Game {
|
|||
GameActions::NightDetails(NightDetails::new(
|
||||
&night.used_actions(),
|
||||
recorded_changes,
|
||||
self.village(),
|
||||
)),
|
||||
)?;
|
||||
self.state = GameState::Day {
|
||||
|
|
|
|||
|
|
@ -45,13 +45,17 @@ pub struct NightDetails {
|
|||
}
|
||||
|
||||
impl NightDetails {
|
||||
pub fn new(choices: &[(ActionPrompt, ActionResult)], changes: Box<[NightChange]>) -> Self {
|
||||
pub fn new(
|
||||
choices: &[(ActionPrompt, ActionResult)],
|
||||
changes: Box<[NightChange]>,
|
||||
village: &Village,
|
||||
) -> Self {
|
||||
Self {
|
||||
changes,
|
||||
choices: choices
|
||||
.iter()
|
||||
.cloned()
|
||||
.filter_map(|(prompt, result)| NightChoice::new(prompt, result))
|
||||
.filter_map(|(prompt, result)| NightChoice::new(prompt, result, village))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
|
@ -64,9 +68,9 @@ pub struct NightChoice {
|
|||
}
|
||||
|
||||
impl NightChoice {
|
||||
pub fn new(prompt: ActionPrompt, result: ActionResult) -> Option<Self> {
|
||||
pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option<Self> {
|
||||
Some(Self {
|
||||
prompt: StoryActionPrompt::new(prompt)?,
|
||||
prompt: StoryActionPrompt::new(prompt, village)?,
|
||||
result: StoryActionResult::new(result),
|
||||
})
|
||||
}
|
||||
|
|
@ -185,6 +189,7 @@ pub enum StoryActionPrompt {
|
|||
chosen: CharacterId,
|
||||
},
|
||||
WolfPackKill {
|
||||
killing_wolf: CharacterId,
|
||||
chosen: CharacterId,
|
||||
},
|
||||
Shapeshifter {
|
||||
|
|
@ -215,7 +220,7 @@ pub enum StoryActionPrompt {
|
|||
}
|
||||
|
||||
impl StoryActionPrompt {
|
||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||
pub fn new(prompt: ActionPrompt, village: &Village) -> Option<Self> {
|
||||
Some(match prompt {
|
||||
ActionPrompt::BeholderWakes { character_id } => Self::BeholderWakes {
|
||||
character_id: character_id.character_id,
|
||||
|
|
@ -370,7 +375,10 @@ impl StoryActionPrompt {
|
|||
ActionPrompt::WolfPackKill {
|
||||
marked: Some(marked),
|
||||
..
|
||||
} => Self::WolfPackKill { chosen: marked },
|
||||
} => Self::WolfPackKill {
|
||||
chosen: marked,
|
||||
killing_wolf: village.killing_wolf().map(|c| c.character_id())?,
|
||||
},
|
||||
ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter {
|
||||
character_id: character_id.character_id,
|
||||
},
|
||||
|
|
@ -433,8 +441,12 @@ impl StoryActionPrompt {
|
|||
|
||||
pub const fn character_id(&self) -> Option<CharacterId> {
|
||||
match self {
|
||||
StoryActionPrompt::MasonsWake { .. } | StoryActionPrompt::WolfPackKill { .. } => None,
|
||||
StoryActionPrompt::Seer { character_id, .. }
|
||||
StoryActionPrompt::MasonsWake { .. } => None,
|
||||
StoryActionPrompt::WolfPackKill {
|
||||
killing_wolf: character_id,
|
||||
..
|
||||
}
|
||||
| StoryActionPrompt::Seer { character_id, .. }
|
||||
| StoryActionPrompt::Protector { character_id, .. }
|
||||
| StoryActionPrompt::Arcanist { character_id, .. }
|
||||
| StoryActionPrompt::Gravedigger { character_id, .. }
|
||||
|
|
|
|||
|
|
@ -784,6 +784,34 @@ clients {
|
|||
display: flex;
|
||||
flex-basis: content;
|
||||
}
|
||||
|
||||
.story {
|
||||
padding-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (max-width : 799px) {
|
||||
.story-characters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 3px;
|
||||
row-gap: 5px;
|
||||
justify-content: space-between;
|
||||
overflow-x: scroll;
|
||||
padding-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width : 800px) {
|
||||
.story-characters {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 3px;
|
||||
row-gap: 5px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
@media only screen and (min-width : 1900px) {
|
||||
|
|
@ -1321,7 +1349,7 @@ select {
|
|||
background-color: $village_color;
|
||||
border: 1px solid $village_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $village_border;
|
||||
}
|
||||
|
|
@ -1330,7 +1358,7 @@ select {
|
|||
border: 1px solid $village_border_faint;
|
||||
background-color: $village_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $village_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1345,7 +1373,7 @@ select {
|
|||
background-color: $wolves_color;
|
||||
border: 1px solid $wolves_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $wolves_border;
|
||||
}
|
||||
|
|
@ -1354,7 +1382,7 @@ select {
|
|||
border: 1px solid $wolves_border_faint;
|
||||
background-color: $wolves_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $wolves_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1369,7 +1397,7 @@ select {
|
|||
background-color: $intel_color;
|
||||
border: 1px solid $intel_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $intel_border;
|
||||
}
|
||||
|
|
@ -1378,7 +1406,7 @@ select {
|
|||
border: 1px solid $intel_border_faint;
|
||||
background-color: $intel_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $intel_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1393,7 +1421,7 @@ select {
|
|||
background-color: $defensive_color;
|
||||
border: 1px solid $defensive_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $defensive_border;
|
||||
}
|
||||
|
|
@ -1402,7 +1430,7 @@ select {
|
|||
border: 1px solid $defensive_border_faint;
|
||||
background-color: $defensive_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $defensive_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1417,7 +1445,7 @@ select {
|
|||
background-color: $offensive_color;
|
||||
border: 1px solid $offensive_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $offensive_border;
|
||||
}
|
||||
|
|
@ -1426,7 +1454,7 @@ select {
|
|||
border: 1px solid $offensive_border_faint;
|
||||
background-color: $offensive_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $offensive_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1441,7 +1469,7 @@ select {
|
|||
background-color: $starts_as_villager_color;
|
||||
border: 1px solid $starts_as_villager_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $starts_as_villager_border;
|
||||
}
|
||||
|
|
@ -1450,7 +1478,7 @@ select {
|
|||
border: 1px solid $starts_as_villager_border_faint;
|
||||
background-color: $starts_as_villager_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $starts_as_villager_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1465,7 +1493,7 @@ select {
|
|||
background-color: $traitor_color;
|
||||
border: 1px solid $traitor_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $traitor_border;
|
||||
}
|
||||
|
|
@ -1474,7 +1502,7 @@ select {
|
|||
border: 1px solid $traitor_border_faint;
|
||||
background-color: $traitor_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $traitor_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1484,7 +1512,7 @@ select {
|
|||
background-color: $drunk_color;
|
||||
border: 1px solid $drunk_border;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
color: white;
|
||||
background-color: $drunk_border;
|
||||
}
|
||||
|
|
@ -1493,7 +1521,7 @@ select {
|
|||
border: 1px solid $drunk_border_faint;
|
||||
background-color: $drunk_color_faint;
|
||||
|
||||
&:hover {
|
||||
&.hover:hover {
|
||||
background-color: $drunk_border_faint;
|
||||
}
|
||||
}
|
||||
|
|
@ -1543,6 +1571,10 @@ select {
|
|||
}
|
||||
|
||||
.setup-screen {
|
||||
.inactive {
|
||||
filter: brightness(0%);
|
||||
}
|
||||
|
||||
margin-top: 2%;
|
||||
font-size: 1.5vw;
|
||||
|
||||
|
|
@ -1693,11 +1725,6 @@ li.choice {
|
|||
}
|
||||
}
|
||||
|
||||
.inactive {
|
||||
// filter: grayscale(100%) brightness(30%);
|
||||
filter: brightness(0%);
|
||||
}
|
||||
|
||||
.qrcode {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1938,50 +1965,66 @@ li.choice {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
align-content: baseline;
|
||||
justify-content: baseline;
|
||||
justify-items: baseline;
|
||||
gap: 1ch;
|
||||
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
padding-left: 5px;
|
||||
padding-right: 10px;
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
padding-left: 2px;
|
||||
padding-right: 2px;
|
||||
|
||||
&:has(.killer) {
|
||||
border: 1px solid rgba(212, 85, 0, 0.5);
|
||||
.inactive {
|
||||
filter: grayscale(100%);
|
||||
border: none;
|
||||
}
|
||||
|
||||
&:has(.powerful) {
|
||||
border: 1px solid rgba(0, 173, 193, 0.5);
|
||||
}
|
||||
|
||||
&:has(.inactive) {
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: sub;
|
||||
}
|
||||
|
||||
&.execution {
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
|
||||
}
|
||||
// img {
|
||||
// vertical-align: sub;
|
||||
// }
|
||||
}
|
||||
|
||||
.alignment-eq {
|
||||
img {
|
||||
vertical-align: sub;
|
||||
.alignment-eq,
|
||||
.roleblock-span {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 1ch;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.highlight-span {
|
||||
height: max-content;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
word-wrap: normal;
|
||||
|
||||
.name {
|
||||
color: white;
|
||||
}
|
||||
|
||||
border: 1px solid $intel_border;
|
||||
background-color: color.change($intel_color, $alpha: 0.1);
|
||||
padding-top: 5px;
|
||||
padding-bottom: 5px;
|
||||
.dead {
|
||||
text-decoration: line-through;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
padding-left: 10px;
|
||||
padding-right: 10px;
|
||||
&:hover::after {
|
||||
content: attr(role);
|
||||
overflow-y: hidden;
|
||||
position: absolute;
|
||||
margin-top: 60px;
|
||||
color: white;
|
||||
background-color: black;
|
||||
border: 1px solid white;
|
||||
padding: 3px;
|
||||
z-index: 4;
|
||||
align-self: center;
|
||||
justify-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.character-span {
|
||||
|
|
@ -1990,7 +2033,12 @@ li.choice {
|
|||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
gap: 1ch;
|
||||
|
||||
.dead {
|
||||
text-decoration: line-through;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.number {
|
||||
color: rgba(255, 255, 0, 0.7);
|
||||
|
|
@ -2699,6 +2747,29 @@ dialog::backdrop {
|
|||
gap: 5px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
// gap: 3px;
|
||||
margin: 0;
|
||||
align-self: flex-start;
|
||||
width: 100%;
|
||||
|
||||
.tab-button:not(.selected) {
|
||||
flex-grow: 1;
|
||||
// color: white;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
color: white;
|
||||
flex-grow: 1;
|
||||
cursor: pointer;
|
||||
text-shadow: 2px 2px black;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.story {
|
||||
display: flex;
|
||||
|
|
@ -2707,13 +2778,14 @@ dialog::backdrop {
|
|||
// width: 100vw;
|
||||
justify-content: space-evenly;
|
||||
row-gap: 5px;
|
||||
margin: 5vh 10vw 0px 10vw;
|
||||
margin: 5vh 5vw 0px 5vw;
|
||||
|
||||
.character-headline {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 3px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.icon-spacer {
|
||||
height: 32px;
|
||||
|
|
@ -2729,12 +2801,47 @@ dialog::backdrop {
|
|||
}
|
||||
}
|
||||
|
||||
.character-details {
|
||||
display: none;
|
||||
|
||||
&.shown {
|
||||
display: flex;
|
||||
.actions {
|
||||
width: 100%;
|
||||
// min-height: 30vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.no-content {
|
||||
filter: grayscale(60%);
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||
cursor: default;
|
||||
|
||||
// backdrop-filter: brightness(20%);
|
||||
&:hover {
|
||||
background-color: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.sub-headline {
|
||||
display: none;
|
||||
// margin-left: 0.5cm;
|
||||
// font-size: 1.5em;
|
||||
// font-style: italic;
|
||||
}
|
||||
|
||||
.action,
|
||||
.change {
|
||||
font-size: 1.25em;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1ch;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.character-details {
|
||||
display: flex;
|
||||
flex-grow: 1;
|
||||
|
||||
border-top: none;
|
||||
flex-direction: column;
|
||||
|
|
@ -2760,11 +2867,7 @@ dialog::backdrop {
|
|||
}
|
||||
|
||||
.details {
|
||||
display: none;
|
||||
|
||||
&.shown {
|
||||
display: flex;
|
||||
}
|
||||
display: flex;
|
||||
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
|
@ -2805,3 +2908,59 @@ dialog {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.wolves-highlight {
|
||||
color: $wolves_color;
|
||||
|
||||
.number {
|
||||
color: $wolves_border;
|
||||
}
|
||||
}
|
||||
|
||||
.village-highlight {
|
||||
color: $village_color;
|
||||
|
||||
.number {
|
||||
color: $village_border;
|
||||
}
|
||||
}
|
||||
|
||||
.intel-highlight {
|
||||
color: $intel_color;
|
||||
|
||||
.number {
|
||||
color: $intel_border;
|
||||
}
|
||||
}
|
||||
|
||||
.defensive-highlight {
|
||||
color: $defensive_color;
|
||||
|
||||
.number {
|
||||
color: $defensive_border;
|
||||
}
|
||||
}
|
||||
|
||||
.offensive-highlight {
|
||||
color: $offensive_color;
|
||||
|
||||
.number {
|
||||
color: $offensive_border;
|
||||
}
|
||||
}
|
||||
|
||||
.starts-as-villager-highlight {
|
||||
color: $starts_as_villager_color;
|
||||
|
||||
.number {
|
||||
color: $starts_as_villager_border;
|
||||
}
|
||||
}
|
||||
|
||||
.traitor-highlight {
|
||||
color: $traitor_color;
|
||||
|
||||
.number {
|
||||
color: $traitor_color;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2025 Emilis Bliūdžius
|
||||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
|
|
@ -12,6 +12,22 @@
|
|||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use werewolves_proto::{character::Character, game::SetupRole, team::Team};
|
||||
|
||||
pub trait Class {
|
||||
fn class(&self) -> Option<&'static str>;
|
||||
}
|
||||
|
||||
impl Class for Character {
|
||||
fn class(&self) -> Option<&'static str> {
|
||||
if let Team::AnyEvil = self.team() {
|
||||
return Some("traitor");
|
||||
}
|
||||
Some(
|
||||
Into::<SetupRole>::into(self.role_title())
|
||||
.category()
|
||||
.class(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (C) 2025 Emilis Bliūdžius
|
||||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
|
|
@ -29,15 +29,13 @@ pub struct AlignmentSpanProps {
|
|||
#[function_component]
|
||||
pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> Html {
|
||||
let class = match alignment {
|
||||
role::Alignment::Village => "village",
|
||||
role::Alignment::Wolves => "wolves",
|
||||
role::Alignment::Traitor => "traitor",
|
||||
role::Alignment::Village => "village-highlight",
|
||||
role::Alignment::Wolves => "wolves-highlight",
|
||||
role::Alignment::Traitor => "traitor-highlight",
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("attribute-span", "faint", class)}>
|
||||
<div>
|
||||
<Icon source={alignment.icon()} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
<span class={classes!("attribute-span", class)}>
|
||||
<Icon source={alignment.icon()} icon_type={IconType::Fit}/>
|
||||
{alignment.to_string()}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,20 +29,14 @@ pub fn AlignmentComparisonSpan(
|
|||
match comparison {
|
||||
AlignmentEq::Same => html! {
|
||||
<span class="alignment-eq">
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
<Icon source={IconSource::Equal} icon_type={IconType::Fit}/>
|
||||
{"the same"}
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
</span>
|
||||
},
|
||||
AlignmentEq::Different => html! {
|
||||
<span class="alignment-eq">
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
<Icon source={IconSource::NotEqual} icon_type={IconType::Fit}/>
|
||||
{"different"}
|
||||
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||
</span>
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,29 +25,10 @@ pub struct DiedToSpanProps {
|
|||
|
||||
#[function_component]
|
||||
pub fn DiedToSpan(DiedToSpanProps { died_to }: &DiedToSpanProps) -> Html {
|
||||
let class = match died_to {
|
||||
DiedToTitle::Execution => "execution",
|
||||
DiedToTitle::MapleWolfStarved | DiedToTitle::MapleWolf => {
|
||||
SetupRoleTitle::MapleWolf.category().class()
|
||||
}
|
||||
DiedToTitle::Militia => SetupRoleTitle::Militia.category().class(),
|
||||
DiedToTitle::LoneWolf
|
||||
| DiedToTitle::AlphaWolf
|
||||
| DiedToTitle::Shapeshift
|
||||
| DiedToTitle::Wolfpack => SetupRoleTitle::Werewolf.category().class(),
|
||||
DiedToTitle::Hunter => SetupRoleTitle::Hunter.category().class(),
|
||||
DiedToTitle::GuardianProtecting => SetupRoleTitle::Guardian.category().class(),
|
||||
DiedToTitle::PyreMasterLynchMob | DiedToTitle::PyreMaster => {
|
||||
SetupRoleTitle::PyreMaster.category().class()
|
||||
}
|
||||
DiedToTitle::MasonLeaderRecruitFail => SetupRoleTitle::MasonLeader.category().class(),
|
||||
};
|
||||
let icon = died_to.icon().unwrap_or(IconSource::Skull);
|
||||
html! {
|
||||
<span class={classes!("attribute-span", "faint", class)}>
|
||||
<div>
|
||||
<Icon source={icon} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
<span class={classes!("attribute-span",)}>
|
||||
<Icon source={icon} icon_type={IconType::Fit}/>
|
||||
{died_to.to_string().to_case(Case::Title)}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ pub fn KillerSpan(KillerSpanProps { killer }: &KillerSpanProps) -> Html {
|
|||
Killer::NotKiller => "inactive",
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("attribute-span", "faint")}>
|
||||
<div class={classes!(class)}>
|
||||
<Icon source={killer.icon()} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
<span class={classes!("attribute-span")}>
|
||||
<Icon
|
||||
source={killer.icon()}
|
||||
icon_type={IconType::Fit}
|
||||
classes={classes!(class)}
|
||||
/>
|
||||
{killer.to_string()}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ pub fn PowerfulSpan(PowerfulSpanProps { powerful }: &PowerfulSpanProps) -> Html
|
|||
Powerful::NotPowerful => "inactive",
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("attribute-span", "faint")}>
|
||||
<div class={classes!(class)}>
|
||||
<Icon source={powerful.icon()} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
<span class={classes!("attribute-span")}>
|
||||
<Icon
|
||||
source={powerful.icon()}
|
||||
icon_type={IconType::Fit}
|
||||
classes={classes!(class)}
|
||||
/>
|
||||
{powerful.to_string()}
|
||||
</span>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
use core::ops::Not;
|
||||
|
||||
// Copyright (C) 2025 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
|
|
@ -13,10 +15,17 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use convert_case::{Case, Casing};
|
||||
use werewolves_proto::{character::Character, game::SetupRole, message::CharacterIdentity};
|
||||
use werewolves_proto::{
|
||||
character::Character,
|
||||
game::{GameTime, SetupRole},
|
||||
message::CharacterIdentity,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
use crate::{
|
||||
class::Class,
|
||||
components::{Icon, IconSource, IconType, PartialAssociatedIcon},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterCardProps {
|
||||
|
|
@ -89,3 +98,47 @@ pub fn CharacterTargetCard(
|
|||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterHighlightProps {
|
||||
pub char: Character,
|
||||
pub time: GameTime,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CharacterHighlight(
|
||||
CharacterHighlightProps { char, time }: &CharacterHighlightProps,
|
||||
) -> Html {
|
||||
let class = char.class().map(|c| format!("{c}-highlight"));
|
||||
let icon = char
|
||||
.role_title()
|
||||
.icon()
|
||||
.map(|source| {
|
||||
html! {
|
||||
<Icon source={source} icon_type={IconType::Fit}/>
|
||||
}
|
||||
})
|
||||
.unwrap_or(html! {
|
||||
<div class="icon-spacer"/>
|
||||
});
|
||||
let dead = char.died_to().and_then(|died_to| {
|
||||
let night = died_to.night();
|
||||
let day = died_to.day();
|
||||
match (time, day, night) {
|
||||
(GameTime::Day { number }, Some(day), None) => number.get() > day.get(),
|
||||
(GameTime::Night { number }, None, Some(night)) => *number > night,
|
||||
(GameTime::Day { number }, None, Some(night)) => number.get() > night,
|
||||
(GameTime::Night { number }, Some(day), None) => *number >= day.get(),
|
||||
(_, None, None) | (_, Some(_), Some(_)) => true,
|
||||
}
|
||||
.then_some("dead")
|
||||
});
|
||||
let role_text = char.role_title().to_string().to_case(Case::Title);
|
||||
html! {
|
||||
<span class={classes!("highlight-span", class)} role={role_text}>
|
||||
{icon}
|
||||
<span class={classes!("number")}><b>{char.number().get()}</b></span>
|
||||
<span class={classes!("name", dead)}>{char.name()}</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (C) 2026 Emilis Bliūdžius
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as
|
||||
// published by the Free Software Foundation, either version 3 of the
|
||||
// License, or (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use werewolves_proto::{game::SetupRole, role::RoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct RoleSpanProps {
|
||||
pub role: RoleTitle,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn RoleSpan(RoleSpanProps { role }: &RoleSpanProps) -> Html {
|
||||
let class = Into::<SetupRole>::into(*role).category().class();
|
||||
let icon = role
|
||||
.icon()
|
||||
.map(|icon| {
|
||||
html! {
|
||||
<Icon source={icon} icon_type={IconType::Small}/>
|
||||
}
|
||||
})
|
||||
.unwrap_or(html! {<div class="icon-spacer"/>});
|
||||
let role_name = role.to_string().to_case(Case::Title);
|
||||
html! {
|
||||
<span class={classes!("role-span", class, "faint")}>
|
||||
{icon}
|
||||
<span class="role-name">{role_name}</span>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct RoleblockProps {
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Roleblock(RoleblockProps { children }: &RoleblockProps) -> Html {
|
||||
let content = if *children == html! {} {
|
||||
html! {<span>{"Drunk"}</span>}
|
||||
} else {
|
||||
children.clone()
|
||||
};
|
||||
html! {
|
||||
<span class={classes!("roleblock-span")}>
|
||||
<Icon source={IconSource::Roleblock} icon_type={IconType::Fit}/>
|
||||
{content}
|
||||
</span>
|
||||
}
|
||||
}
|
||||
|
|
@ -13,85 +13,152 @@
|
|||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
use core::ops::Not;
|
||||
use std::{collections::HashMap, rc::Rc};
|
||||
|
||||
use werewolves_proto::{
|
||||
aura::AuraTitle,
|
||||
character::{Character, CharacterId},
|
||||
diedto::DiedTo,
|
||||
game::{
|
||||
GameTime, SetupRole,
|
||||
night::changes::NightChange,
|
||||
story::{NightChoice, StoryActionPrompt},
|
||||
story::{NightChoice, StoryActionPrompt, StoryActionResult},
|
||||
},
|
||||
player::Protection,
|
||||
role::{RoleBlock, RoleTitle},
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType, Identity, PartialAssociatedIcon};
|
||||
use crate::{
|
||||
class::Class,
|
||||
components::{
|
||||
AuraSpan, Button, CharacterHighlight, Icon, IconSource, IconType, Identity, IdentitySpan,
|
||||
PartialAssociatedIcon, RoleSpan, Roleblock,
|
||||
attributes::{
|
||||
AlignmentComparisonSpan, AlignmentSpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct CharacterStoryProps {
|
||||
pub all_characters: Rc<[Character]>,
|
||||
pub character: Character,
|
||||
pub actions: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn CharacterStory(CharacterStoryProps { character, actions }: &CharacterStoryProps) -> Html {
|
||||
let mut by_time = actions.clone();
|
||||
pub fn CharacterStory(
|
||||
CharacterStoryProps {
|
||||
all_characters,
|
||||
character,
|
||||
actions,
|
||||
}: &CharacterStoryProps,
|
||||
) -> Html {
|
||||
let class = character.class();
|
||||
let mut by_time = actions
|
||||
.iter()
|
||||
.filter(|(_, changes, choices)| !(changes.is_empty() && choices.is_empty()))
|
||||
.cloned()
|
||||
.collect::<Box<[_]>>();
|
||||
by_time.sort_by_key(|s| s.0);
|
||||
let by_time = by_time
|
||||
let open_time = use_state(|| by_time.iter().next().map(|(t, _, _)| *t));
|
||||
if let Some(current_open_time) = open_time.as_ref()
|
||||
&& let Some(last) = by_time.last().map(|c| c.0)
|
||||
{
|
||||
if last < *current_open_time || !by_time.iter().any(|(t, _, _)| t == current_open_time) {
|
||||
open_time.set(Some(last));
|
||||
} else if let Some(first) = by_time.first().map(|c| c.0)
|
||||
&& first > *current_open_time
|
||||
{
|
||||
open_time.set(Some(first));
|
||||
}
|
||||
}
|
||||
let tabs = by_time
|
||||
.into_iter()
|
||||
.map(|(time, changes, choices)| {
|
||||
.map(|(time, _, _)| {
|
||||
let time_text = match time {
|
||||
GameTime::Day { number } => format!("day {number}"),
|
||||
GameTime::Night { number } => format!("night {number}"),
|
||||
};
|
||||
let on_click = {
|
||||
let open_time = open_time.setter();
|
||||
Callback::from(move |_| open_time.set(Some(time)))
|
||||
};
|
||||
let selected = open_time
|
||||
.as_ref()
|
||||
.map(|open| *open == time)
|
||||
.unwrap_or_default();
|
||||
let faint = selected.not().then_some("faint");
|
||||
let selected = selected.then_some("selected");
|
||||
html! {
|
||||
<CharacterStoryTime
|
||||
character={character.clone()}
|
||||
time={time}
|
||||
changes={changes}
|
||||
choices={choices}
|
||||
/>
|
||||
<button
|
||||
onclick={on_click}
|
||||
class={classes!("tab-button", class, faint, selected, "hover")}
|
||||
>
|
||||
{time_text}
|
||||
</button>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let story_content = open_time
|
||||
.as_ref()
|
||||
.and_then(|time| actions.iter().find(|(t, _, _)| t == time))
|
||||
.map(|(time, changes, choices)| {
|
||||
let choices = choices
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<Choice
|
||||
all_characters={all_characters.clone()}
|
||||
character={character.clone()}
|
||||
choice={c.clone()}
|
||||
all_choices_that_night={choices.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let changes = changes
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<Change all_characters={all_characters.clone()} change={c.clone()} time={*time}/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let changes = (changes == html! {}).not().then_some(html! {
|
||||
<>
|
||||
<span class="sub-headline">{"changes"}</span>
|
||||
<div>
|
||||
{changes}
|
||||
</div>
|
||||
</>
|
||||
});
|
||||
let choices = (choices == html! {}).not().then_some(html! {
|
||||
<>
|
||||
<span class="sub-headline">{"choices"}</span>
|
||||
<div>
|
||||
{choices}
|
||||
</div>
|
||||
</>
|
||||
});
|
||||
html! {
|
||||
<>
|
||||
{choices}
|
||||
{changes}
|
||||
</>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="character-story">
|
||||
<CharacterStoryContainer character={character.clone()}>
|
||||
{by_time}
|
||||
</CharacterStoryContainer>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
struct CharacterStoryTimeProps {
|
||||
pub character: Character,
|
||||
pub time: GameTime,
|
||||
pub changes: Box<[NightChange]>,
|
||||
pub choices: Box<[NightChoice]>,
|
||||
}
|
||||
#[function_component]
|
||||
fn CharacterStoryTime(
|
||||
CharacterStoryTimeProps {
|
||||
character,
|
||||
time,
|
||||
changes,
|
||||
choices,
|
||||
}: &CharacterStoryTimeProps,
|
||||
) -> Html {
|
||||
let open = use_state(|| false);
|
||||
let time_text = match time {
|
||||
GameTime::Day { number } => format!("day {number}"),
|
||||
GameTime::Night { number } => format!("night {number}"),
|
||||
};
|
||||
let shown = open.then_some("shown");
|
||||
let on_click = {
|
||||
let open = open.clone();
|
||||
Callback::from(move |_| open.set(!*open))
|
||||
};
|
||||
html! {
|
||||
<div class="story-time">
|
||||
<span class={classes!("time")} onclick={on_click}>
|
||||
{time_text}
|
||||
</span>
|
||||
<div class={classes!("details", shown)}>
|
||||
{"hello"}
|
||||
<div class="tabs">
|
||||
{tabs}
|
||||
</div>
|
||||
<div class="story-content">
|
||||
{story_content}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -102,6 +169,8 @@ struct ChoiceProps {
|
|||
all_characters: Rc<[Character]>,
|
||||
character: Character,
|
||||
choice: NightChoice,
|
||||
all_choices_that_night: Box<[NightChoice]>,
|
||||
time: GameTime,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -110,45 +179,258 @@ fn Choice(
|
|||
all_characters,
|
||||
character,
|
||||
choice,
|
||||
all_choices_that_night,
|
||||
time,
|
||||
}: &ChoiceProps,
|
||||
) -> Html {
|
||||
let generate_prompt = |chosen: CharacterId| -> Html { todo!() };
|
||||
let generate_prompt = |chosen: CharacterId, wording: &'static str| -> Option<Html> {
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == chosen)
|
||||
.map(|chosen| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{wording}</span>
|
||||
<CharacterHighlight
|
||||
char={chosen.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
})
|
||||
};
|
||||
let prompt = match &choice.prompt {
|
||||
StoryActionPrompt::Guardian {
|
||||
chosen, guarding, ..
|
||||
} => todo!(),
|
||||
StoryActionPrompt::Seer { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Protector { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Arcanist { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Gravedigger { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Hunter { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Militia { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::MapleWolf { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Adjudicator { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::PowerSeer { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Mortician { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Beholder { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::MasonsWake { leader, masons } => todo!(),
|
||||
StoryActionPrompt::MasonLeaderRecruit { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Empath { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Vindicator { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::PyreMaster { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::WolfPackKill { chosen } => todo!(),
|
||||
StoryActionPrompt::Shapeshifter { .. } => todo!(),
|
||||
StoryActionPrompt::AlphaWolf { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::DireWolf { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::LoneWolfKill { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::Insomniac { .. } => todo!(),
|
||||
StoryActionPrompt::Bloodletter { chosen, .. } => todo!(),
|
||||
StoryActionPrompt::BeholderWakes { character_id } => todo!(),
|
||||
} => generate_prompt(*chosen, if *guarding { "guarded" } else { "protected" }),
|
||||
StoryActionPrompt::Seer { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::Protector { chosen, .. } => generate_prompt(*chosen, "protected"),
|
||||
StoryActionPrompt::Arcanist {
|
||||
chosen: (target1, target2),
|
||||
..
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *target1)
|
||||
.and_then(|t1| {
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *target2)
|
||||
.map(|t2| (t1, t2))
|
||||
})
|
||||
.map(|(t1, t2)| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"checked"}</span>
|
||||
<CharacterHighlight
|
||||
char={t1.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"and"}</span>
|
||||
<CharacterHighlight
|
||||
char={t2.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
StoryActionPrompt::Gravedigger { chosen, .. } => generate_prompt(*chosen, "dug"),
|
||||
StoryActionPrompt::Hunter { chosen, .. } => generate_prompt(*chosen, "set a trap for"),
|
||||
StoryActionPrompt::Militia { chosen, .. } => generate_prompt(*chosen, "shot"),
|
||||
StoryActionPrompt::MapleWolf { chosen, .. } => generate_prompt(*chosen, "ate"),
|
||||
StoryActionPrompt::Adjudicator { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::PowerSeer { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::Mortician { chosen, .. } => generate_prompt(*chosen, "examined"),
|
||||
StoryActionPrompt::Beholder { chosen, .. } => generate_prompt(*chosen, "beheld"),
|
||||
StoryActionPrompt::MasonLeaderRecruit { chosen, .. } => {
|
||||
generate_prompt(*chosen, "attempted to recruit")
|
||||
}
|
||||
StoryActionPrompt::WolfPackKill { chosen, .. } => {
|
||||
generate_prompt(*chosen, "took the wolfpack kill and attacked")
|
||||
}
|
||||
StoryActionPrompt::MasonsWake { leader, .. } => all_characters
|
||||
.iter()
|
||||
.find(|l| l.character_id() == *leader)
|
||||
.map(|leader| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"woke with the masons of"}</span>
|
||||
<CharacterHighlight
|
||||
char={leader.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
StoryActionPrompt::Empath { chosen, .. } => generate_prompt(*chosen, "checked"),
|
||||
StoryActionPrompt::Vindicator { chosen, .. } => generate_prompt(*chosen, "protected"),
|
||||
StoryActionPrompt::PyreMaster { chosen, .. } => generate_prompt(*chosen, "burned"),
|
||||
StoryActionPrompt::Shapeshifter { .. } => all_choices_that_night
|
||||
.iter()
|
||||
.find_map(|c| match c.prompt {
|
||||
StoryActionPrompt::WolfPackKill { chosen, .. } => Some(chosen),
|
||||
_ => None,
|
||||
})
|
||||
.and_then(|target| all_characters.iter().find(|c| c.character_id() == target))
|
||||
.map(|target| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"chose to shift into"}</span>
|
||||
<CharacterHighlight
|
||||
char={target.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
StoryActionPrompt::Insomniac { .. } => Some(html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={character.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"woke in the night due to visits from: "}</span>
|
||||
</>
|
||||
}),
|
||||
StoryActionPrompt::AlphaWolf { chosen, .. } => generate_prompt(*chosen, "killed"),
|
||||
StoryActionPrompt::DireWolf { chosen, .. } => {
|
||||
generate_prompt(*chosen, "roleblocked visitors to")
|
||||
}
|
||||
StoryActionPrompt::LoneWolfKill { chosen, .. } => generate_prompt(*chosen, "killed"),
|
||||
StoryActionPrompt::Bloodletter { chosen, .. } => generate_prompt(*chosen, "bloodlet"),
|
||||
StoryActionPrompt::BeholderWakes { character_id } => {
|
||||
generate_prompt(*character_id, "woke again to see")
|
||||
}
|
||||
};
|
||||
html! {}
|
||||
if prompt.is_none() {
|
||||
return html! {};
|
||||
}
|
||||
let result = choice.result.as_ref().map(|result| match result {
|
||||
StoryActionResult::RoleBlocked => html! {
|
||||
<>
|
||||
<span>{"but was"}</span>
|
||||
<Roleblock>{"Roleblocked"}</Roleblock>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Seer(alignment) => html! {
|
||||
<>
|
||||
<span>{"and saw"}</span>
|
||||
<AlignmentSpan alignment={*alignment}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::PowerSeer { powerful } => html! {
|
||||
<>
|
||||
<span>{"and saw"}</span>
|
||||
<PowerfulSpan powerful={*powerful}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Adjudicator { killer } => html! {
|
||||
<>
|
||||
<span>{"and saw"}</span>
|
||||
<KillerSpan killer={*killer}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Arcanist(alignment_eq) => html! {
|
||||
<>
|
||||
<span>{"and saw them as"}</span>
|
||||
<AlignmentComparisonSpan comparison={*alignment_eq}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::GraveDigger(None) => html! {
|
||||
<span>{"but found an empty grave"}</span>
|
||||
},
|
||||
StoryActionResult::GraveDigger(Some(role_title)) => html! {
|
||||
<>
|
||||
<span>{"as"}</span>
|
||||
<RoleSpan role={*role_title}/>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Mortician(died_to_title) => html! {
|
||||
<>
|
||||
<span>{"and found"}</span>
|
||||
<DiedToSpan died_to={*died_to_title}/>
|
||||
<span>{"to be the cause of death"}</span>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::Insomniac { visits } => {
|
||||
let mut visit_chars = vec![];
|
||||
for visit in visits {
|
||||
match all_characters.iter().find(|c| c.character_id() == *visit) {
|
||||
Some(char) => visit_chars.push(char),
|
||||
None => {
|
||||
log::warn!("visit chars missing {visit}");
|
||||
return html! {};
|
||||
}
|
||||
}
|
||||
}
|
||||
visit_chars
|
||||
.into_iter()
|
||||
.map(|visitor| {
|
||||
html! {
|
||||
<CharacterHighlight
|
||||
char={visitor.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>()
|
||||
}
|
||||
StoryActionResult::Empath { scapegoat: true } => {
|
||||
html! {<span>{"and found their scapegoat"}</span>}
|
||||
}
|
||||
StoryActionResult::Empath { scapegoat: false } => {
|
||||
html! {<span>{"who was not a scapegoat"}</span>}
|
||||
}
|
||||
StoryActionResult::BeholderSawNothing => html! {
|
||||
<>
|
||||
{"and saw"}
|
||||
<em>{"nothing"}</em>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::BeholderSawEverything => html! {
|
||||
<>
|
||||
{"and saw"}
|
||||
<em>{"everything"}</em>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::ShiftFailed => html! {
|
||||
{"however, their shift failed"}
|
||||
},
|
||||
StoryActionResult::Drunk => html! {
|
||||
<>
|
||||
<span>{"but got"}</span>
|
||||
<AuraSpan aura={AuraTitle::Drunk} />
|
||||
</>
|
||||
},
|
||||
});
|
||||
html! {
|
||||
<div class="action">
|
||||
{prompt}
|
||||
{result}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
struct ChangeProps {
|
||||
all_characters: Rc<[Character]>,
|
||||
change: NightChange,
|
||||
time: GameTime,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -156,53 +438,236 @@ fn Change(
|
|||
ChangeProps {
|
||||
all_characters,
|
||||
change,
|
||||
time,
|
||||
}: &ChangeProps,
|
||||
) -> Html {
|
||||
match change {
|
||||
NightChange::RoleChange(role_title, ..) => todo!(),
|
||||
NightChange::Kill { target, died_to } => todo!(),
|
||||
NightChange::RoleChange(_, role_title) => Some(html! {
|
||||
<>
|
||||
<span>{"role changed to"}</span>
|
||||
<RoleSpan role={*role_title}/>
|
||||
</>
|
||||
}),
|
||||
NightChange::Kill { died_to, .. } => Some(html! {
|
||||
<>
|
||||
<span>{"died to"}</span>
|
||||
<DiedToSpan died_to={died_to.title()}/>
|
||||
</>
|
||||
}),
|
||||
NightChange::RoleBlock {
|
||||
source,
|
||||
target,
|
||||
block_type,
|
||||
} => todo!(),
|
||||
NightChange::Shapeshift { source, into } => todo!(),
|
||||
NightChange::Protection { target, protection } => todo!(),
|
||||
NightChange::ElderReveal { .. } => todo!(),
|
||||
block_type: RoleBlock::Direwolf,
|
||||
..
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|s| s.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"had visitors role blocked by"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::Shapeshift { source, .. } => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
{"shapeshifted by"}
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
{"and became a werewolf"}
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::ElderReveal { .. } => Some(html! {
|
||||
<>
|
||||
<span>{"learned they are the"}</span>
|
||||
<RoleSpan role={RoleTitle::Elder}/>
|
||||
</>
|
||||
}),
|
||||
NightChange::MasonRecruit {
|
||||
mason_leader,
|
||||
recruiting,
|
||||
} => todo!(),
|
||||
NightChange::ApplyAura { source, aura, .. } => todo!(),
|
||||
NightChange::LostAura { aura, .. } => todo!(),
|
||||
NightChange::EmpathFoundScapegoat { .. } | NightChange::HunterTarget { .. } => {
|
||||
return html! {};
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *mason_leader)
|
||||
.and_then(|mason| {
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *recruiting)
|
||||
.map(|recruiting| (mason, recruiting))
|
||||
})
|
||||
.map(|(mason, recruiting)| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={mason.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"recruited"}</span>
|
||||
<CharacterHighlight
|
||||
char={recruiting.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
<span>{"into the masons"}</span>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::ApplyAura { source, aura, .. } => {
|
||||
let from = all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"from"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
});
|
||||
Some(html! {
|
||||
<>
|
||||
<span>{"received the"}</span>
|
||||
<AuraSpan aura={aura.title()}/>
|
||||
<span>{"aura"}</span>
|
||||
{from}
|
||||
</>
|
||||
})
|
||||
}
|
||||
NightChange::LostAura { aura, .. } => Some(html! {
|
||||
<>
|
||||
<span>{"lost the"}</span>
|
||||
<AuraSpan aura={aura.title()}/>
|
||||
<span>{"aura"}</span>
|
||||
</>
|
||||
}),
|
||||
NightChange::EmpathFoundScapegoat { scapegoat, .. } => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *scapegoat)
|
||||
.map(|scapegoat| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"took on the scapegoat's curse from"}</span>
|
||||
<CharacterHighlight
|
||||
char={scapegoat.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::HunterTarget { source, .. } => all_characters
|
||||
.iter()
|
||||
.find(|c| c.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
{"had the hunter's mark placed on them by"}
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
NightChange::Protection { target, protection } => {
|
||||
let prot = match protection {
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: true,
|
||||
} => all_characters
|
||||
.iter()
|
||||
.find(|s| s.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"was guarded by"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
Protection::Guardian {
|
||||
source,
|
||||
guarding: false,
|
||||
}
|
||||
| Protection::Protector { source }
|
||||
| Protection::Vindicator { source } => all_characters
|
||||
.iter()
|
||||
.find(|s| s.character_id() == *source)
|
||||
.map(|source| {
|
||||
html! {
|
||||
<>
|
||||
<span>{"was protected by"}</span>
|
||||
<CharacterHighlight
|
||||
char={source.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
};
|
||||
all_characters
|
||||
.iter()
|
||||
.find(|t| t.character_id() == *target)
|
||||
.and_then(|target| prot.map(|prot| (target, prot)))
|
||||
.map(|(target, prot)| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterHighlight
|
||||
char={target.clone()}
|
||||
time={*time}
|
||||
/>
|
||||
{prot}
|
||||
</>
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
html! {
|
||||
<div class="change">
|
||||
</div>
|
||||
}
|
||||
.map(|change| {
|
||||
html! {
|
||||
<div class="change">
|
||||
{change}
|
||||
</div>
|
||||
}
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
struct CharacterStoryContainerProps {
|
||||
pub struct CharacterStoryButtonProps {
|
||||
pub character: Character,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub choices_by_time: Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
pub on_click: Callback<(
|
||||
Character,
|
||||
Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
)>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
fn CharacterStoryContainer(
|
||||
CharacterStoryContainerProps {
|
||||
pub fn CharacterStoryButton(
|
||||
CharacterStoryButtonProps {
|
||||
character,
|
||||
children,
|
||||
}: &CharacterStoryContainerProps,
|
||||
choices_by_time,
|
||||
on_click,
|
||||
}: &CharacterStoryButtonProps,
|
||||
) -> Html {
|
||||
let open = use_state(|| true);
|
||||
let role_class = Into::<SetupRole>::into(character.role_title())
|
||||
.category()
|
||||
.class();
|
||||
let clickable = !choices_by_time
|
||||
.iter()
|
||||
.all(|(_, c1, c2)| c1.is_empty() && c2.is_empty());
|
||||
let role_class = character.class();
|
||||
let icon = character
|
||||
.role_title()
|
||||
.icon()
|
||||
|
|
@ -225,21 +690,21 @@ fn CharacterStoryContainer(
|
|||
<div class="icon-spacer"/>
|
||||
});
|
||||
|
||||
let on_click = {
|
||||
let open = open.clone();
|
||||
Callback::from(move |_| open.set(!*open))
|
||||
let on_click = if clickable {
|
||||
let cb = on_click.clone();
|
||||
let char = character.clone();
|
||||
let act = choices_by_time.clone();
|
||||
Callback::from(move |_| cb.emit((char.clone(), act.clone())))
|
||||
} else {
|
||||
Callback::noop()
|
||||
};
|
||||
let shown = open.then_some("shown");
|
||||
let inactive = clickable.not().then_some("no-content");
|
||||
let hover = clickable.then_some("hover");
|
||||
html! {
|
||||
<div class="character-story-card">
|
||||
<div class={classes!("character-headline", role_class, "faint")} onclick={on_click}>
|
||||
{icon}
|
||||
<Identity ident={character.identity().into_public()}/>
|
||||
{dead_icon}
|
||||
</div>
|
||||
<div class={classes!("character-details", role_class, "faint", shown)}>
|
||||
{children.clone()}
|
||||
</div>
|
||||
<div class={classes!("character-headline", role_class, "faint", inactive, hover)} onclick={on_click}>
|
||||
{icon}
|
||||
<Identity ident={character.identity().into_public()}/>
|
||||
{dead_icon}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ pub fn generate_story() -> GameStory {
|
|||
empath,
|
||||
scapegoat,
|
||||
hunter,
|
||||
mason_leader,
|
||||
) = (
|
||||
(SetupRole::Werewolf, players_iter.next().unwrap()),
|
||||
(SetupRole::DireWolf, players_iter.next().unwrap()),
|
||||
|
|
@ -59,6 +60,12 @@ pub fn generate_story() -> GameStory {
|
|||
players_iter.next().unwrap(),
|
||||
),
|
||||
(SetupRole::Hunter, players_iter.next().unwrap()),
|
||||
(
|
||||
SetupRole::MasonLeader {
|
||||
recruits_available: NonZeroU8::new(3).unwrap(),
|
||||
},
|
||||
players_iter.next().unwrap(),
|
||||
),
|
||||
);
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(werewolf.0, werewolf.1);
|
||||
|
|
@ -79,6 +86,7 @@ pub fn generate_story() -> GameStory {
|
|||
settings.add_and_assign(empath.0, empath.1);
|
||||
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||
settings.add_and_assign(hunter.0, hunter.1);
|
||||
settings.add_and_assign(mason_leader.0, mason_leader.1);
|
||||
settings.fill_remaining_slots_with_villagers(players.len());
|
||||
#[allow(unused)]
|
||||
let (
|
||||
|
|
@ -100,6 +108,7 @@ pub fn generate_story() -> GameStory {
|
|||
empath,
|
||||
scapegoat,
|
||||
hunter,
|
||||
mason_leader,
|
||||
) = (
|
||||
werewolf.1,
|
||||
dire_wolf.1,
|
||||
|
|
@ -119,6 +128,7 @@ pub fn generate_story() -> GameStory {
|
|||
empath.1,
|
||||
scapegoat.1,
|
||||
hunter.1,
|
||||
mason_leader.1,
|
||||
);
|
||||
let mut game = Game::new(&players, settings).unwrap();
|
||||
game.r#continue().r#continue();
|
||||
|
|
@ -223,6 +233,13 @@ pub fn generate_story() -> GameStory {
|
|||
assert!(!game.r#continue().empath());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_leader_recruit();
|
||||
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().masons_wake();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().insomniac();
|
||||
game.r#continue().insomniac();
|
||||
game.r#continue().sleep();
|
||||
|
|
@ -303,6 +320,13 @@ pub fn generate_story() -> GameStory {
|
|||
assert!(game.r#continue().empath());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_leader_recruit();
|
||||
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().masons_wake();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().insomniac();
|
||||
game.r#continue().insomniac();
|
||||
game.r#continue().sleep();
|
||||
|
|
@ -379,6 +403,13 @@ pub fn generate_story() -> GameStory {
|
|||
assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack);
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_leader_recruit();
|
||||
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_wake();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().insomniac();
|
||||
game.r#continue().insomniac();
|
||||
game.r#continue().sleep();
|
||||
|
|
@ -435,6 +466,9 @@ pub fn generate_story() -> GameStory {
|
|||
);
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_wake();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().insomniac();
|
||||
game.r#continue().insomniac();
|
||||
game.r#continue().sleep();
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ use crate::components::{
|
|||
attributes::{
|
||||
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||
},
|
||||
story::{CharacterStory, StoryError},
|
||||
story::{CharacterStory, CharacterStoryButton, StoryError},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
|
|
@ -94,603 +94,54 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
|||
})
|
||||
.collect::<Box<[_]>>();
|
||||
|
||||
let selected_char = use_state::<
|
||||
Option<(
|
||||
Character,
|
||||
Box<[(GameTime, Box<[NightChange]>, Box<[NightChoice]>)]>,
|
||||
)>,
|
||||
_,
|
||||
>(|| actions_by_character.first().cloned());
|
||||
let on_char_pick = {
|
||||
let selected_char = selected_char.setter();
|
||||
Callback::from(move |(char, actions)| selected_char.set(Some((char, actions))))
|
||||
};
|
||||
let chars = actions_by_character
|
||||
.into_iter()
|
||||
.map(|(char, actions)| {
|
||||
html! {
|
||||
<CharacterStory character={char} actions={actions} />
|
||||
<CharacterStoryButton character={char} choices_by_time={actions} on_click={on_char_pick.clone()} />
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
let all_characters = village.characters().into_iter().collect::<Rc<_>>();
|
||||
let actions = selected_char.as_ref().map(|(char, actions)| {
|
||||
let class = Into::<SetupRole>::into(char.role_title())
|
||||
.category()
|
||||
.class();
|
||||
|
||||
html! {
|
||||
<>
|
||||
<CharacterCard char={char.clone()} dead={char.died_to().is_some()} faint=true/>
|
||||
<div class={classes!("character-details", class, "faint")}>
|
||||
<CharacterStory
|
||||
character={char.clone()}
|
||||
actions={actions.clone()}
|
||||
all_characters={all_characters}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="story">
|
||||
{chars}
|
||||
<div class="story-characters">
|
||||
{chars}
|
||||
</div>
|
||||
<div class="actions">
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
// #[function_component]
|
||||
// pub fn OldStory(StoryProps { story }: &StoryProps) -> Html {
|
||||
// let final_characters =
|
||||
// story
|
||||
// .final_village()
|
||||
// .unwrap_or_else(|_| story.starting_village.clone())
|
||||
// .characters()
|
||||
// .into_iter()
|
||||
// .map(|c| {
|
||||
// let dead =c.alive().not();
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={c} dead={dead}/>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// let bits = story
|
||||
// .iter()
|
||||
// .map(|(time, changes)| {
|
||||
// let characters = story
|
||||
// .village_at(match time {
|
||||
// GameTime::Day { .. } => {
|
||||
// time
|
||||
// },
|
||||
// GameTime::Night { .. } => {
|
||||
// time.previous().unwrap_or(time)
|
||||
// },
|
||||
// }).ok().flatten()
|
||||
// .map(|v| Rc::new(v.characters().into_iter()
|
||||
// .map(|c| (c.character_id(), c))
|
||||
// .collect::<HashMap<CharacterId, Character>>()))
|
||||
// .unwrap_or_else(||
|
||||
// Rc::new(story.starting_village
|
||||
// .characters().into_iter()
|
||||
// .map(|c| (c.character_id(), c))
|
||||
// .collect::<HashMap<_, _>>()));
|
||||
// let changes = match changes {
|
||||
// GameActions::DayDetails(day_changes) => {
|
||||
// let execute_list =
|
||||
// day_changes
|
||||
// .iter()
|
||||
// .map(|c| match c {
|
||||
// DayDetail::Execute(target) => *target,
|
||||
// })
|
||||
// .filter_map(|c| story.starting_village.character_by_id(c).ok())
|
||||
// .map(|c| {
|
||||
// html! {
|
||||
// <CharacterCard faint=true char={c.clone()}/>
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
|
||||
// day_changes.is_empty().not().then_some(html! {
|
||||
// <div class="day">
|
||||
// <h3>{"village executed"}</h3>
|
||||
// <div class="executed">
|
||||
// {execute_list}
|
||||
// </div>
|
||||
// </div>
|
||||
// })
|
||||
// }
|
||||
// GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({
|
||||
// let choices = details
|
||||
// .choices
|
||||
// .iter()
|
||||
// .map(|c| {
|
||||
// html! {
|
||||
// <StoryNightChoice choice={c.clone()} characters={characters.clone()}/>
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
|
||||
// let changes = details
|
||||
// .changes
|
||||
// .iter()
|
||||
// .map(|c| {
|
||||
// html! {
|
||||
// <li class="change">
|
||||
// <StoryNightChange
|
||||
// change={c.clone()}
|
||||
// characters={characters.clone()}
|
||||
// />
|
||||
// </li>
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
|
||||
// html! {
|
||||
// <div class="night">
|
||||
// <label>{"choices"}</label>
|
||||
// <ul class="choices">
|
||||
// {choices}
|
||||
// </ul>
|
||||
// <label>{"changes"}</label>
|
||||
// <ul class="changes">
|
||||
// {changes}
|
||||
// </ul>
|
||||
// </div>
|
||||
// }
|
||||
// }),
|
||||
// };
|
||||
|
||||
// changes
|
||||
// .map(|changes| {
|
||||
// html! {
|
||||
// <div class="time-period">
|
||||
// <h1>{"on "}{time.to_string()}{"..."}</h1>
|
||||
// {changes}
|
||||
// </div>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default()
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// html! {
|
||||
// <div class="story">
|
||||
// <div class="cast">
|
||||
// {final_characters}
|
||||
// </div>
|
||||
// {bits}
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// struct StoryNightChangeProps {
|
||||
// change: NightChange,
|
||||
// characters: Rc<HashMap<CharacterId, Character>>,
|
||||
// }
|
||||
|
||||
// #[function_component]
|
||||
// fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
||||
// match change {
|
||||
// NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
||||
// <>
|
||||
// <CharacterCard faint=true char={character.clone()}/>
|
||||
// {"lost the"}
|
||||
// <AuraSpan aura={aura.title()}/>
|
||||
// {"aura"}
|
||||
// </>
|
||||
// }).unwrap_or_default(),
|
||||
// NightChange::ApplyAura { source, target, aura } => characters.get(source).and_then(|source| characters.get(target).map(|target| (source, target))).map(|(source, target)| {
|
||||
// html!{
|
||||
// <>
|
||||
// <CharacterCard faint=true char={target.clone()}/>
|
||||
// <span class="story-text">{"gained the"}</span>
|
||||
// <AuraSpan aura={aura.title()}/>
|
||||
// <span class="story-text">{"aura from"}</span>
|
||||
// <CharacterCard faint=true char={source.clone()}/>
|
||||
// </>
|
||||
// }
|
||||
// }).unwrap_or_default(),
|
||||
// NightChange::RoleChange(character_id, role_title) => characters
|
||||
// .get(character_id)
|
||||
// .map(|char| {
|
||||
// let mut new_char = char.clone();
|
||||
// let _ = new_char.role_change(*role_title, GameTime::Night { number: 0 });
|
||||
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={char.clone()}/>
|
||||
// <span class="story-text">{"is now"}</span>
|
||||
// <CharacterCard faint=true char={new_char.clone()}/>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default(),
|
||||
|
||||
// NightChange::Kill { target, died_to } => {
|
||||
|
||||
// characters
|
||||
// .get(target)
|
||||
// .map(|target| {
|
||||
// html! {
|
||||
// <>
|
||||
// <Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
||||
// <CharacterCard faint=true char={target.clone()}/>
|
||||
// <span class="story-text">{"died to"}</span>
|
||||
// <DiedToSpan died_to={died_to.title()}/>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default()
|
||||
// },
|
||||
// NightChange::RoleBlock { source, target, .. } => characters
|
||||
// .get(source)
|
||||
// .and_then(|s| characters.get(target).map(|t| (s, t)))
|
||||
// .map(|(source, target)| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={source.clone()}/>
|
||||
// <span class="story-text">{"role blocked"}</span>
|
||||
// <CharacterCard faint=true char={target.clone()}/>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default(),
|
||||
// NightChange::Shapeshift { source, into } => characters
|
||||
// .get(source)
|
||||
// .and_then(|s| characters.get(into).map(|i| (s, i)))
|
||||
// .map(|(source, into)| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={source.clone()}/>
|
||||
// <span class="story-text">{"shapeshifted into"}</span>
|
||||
// <CharacterCard faint=true char={into.clone()}/>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default(),
|
||||
|
||||
// NightChange::ElderReveal { elder } => characters
|
||||
// .get(elder)
|
||||
// .map(|elder| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={elder.clone()}/>
|
||||
// <span class="story-text">{"learned they are the Elder"}</span>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default(),
|
||||
// NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters
|
||||
// .get(empath)
|
||||
// .and_then(|e| characters.get(scapegoat).map(|s| (e, s)))
|
||||
// .map(|(empath, scapegoat)| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={empath.clone()}/>
|
||||
// <span class="story-text">{"found the scapegoat in"}</span>
|
||||
// <CharacterCard faint=true char={scapegoat.clone()}/>
|
||||
// <span class="story-text">{"and took on their curse"}</span>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default(),
|
||||
|
||||
// NightChange::HunterTarget { .. }
|
||||
// | NightChange::MasonRecruit { .. }
|
||||
// | NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// struct StoryNightResultProps {
|
||||
// result: StoryActionResult,
|
||||
// characters: Rc<HashMap<CharacterId, Character>>,
|
||||
// }
|
||||
|
||||
// #[function_component]
|
||||
// fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
||||
// match result {
|
||||
// StoryActionResult::ShiftFailed => html!{
|
||||
// <span class="story-text">{"but it failed"}</span>
|
||||
// },
|
||||
// StoryActionResult::Drunk => html! {
|
||||
// <>
|
||||
// <span class="story-text">{"but got "}</span>
|
||||
// <AuraSpan aura={AuraTitle::Drunk}/>
|
||||
// <span class="story-text">{" instead"}</span>
|
||||
// </>
|
||||
// },
|
||||
// StoryActionResult::BeholderSawEverything => html!{
|
||||
// <span class="story-text">{"and saw everything 👁️"}</span>
|
||||
// },
|
||||
// StoryActionResult::BeholderSawNothing => html!{
|
||||
// <span class="story-text">{"but saw nothing"}</span>
|
||||
// },
|
||||
// StoryActionResult::RoleBlocked => html! {
|
||||
// <span class="story-text">{"but was role blocked"}</span>
|
||||
// },
|
||||
// StoryActionResult::Seer(alignment) => {
|
||||
// html! {
|
||||
// <span>
|
||||
// <span class="story-text">{"and saw"}</span>
|
||||
// <AlignmentSpan alignment={*alignment}/>
|
||||
// </span>
|
||||
// }
|
||||
// }
|
||||
// StoryActionResult::PowerSeer { powerful } => {
|
||||
// html! {
|
||||
// <span>
|
||||
// <span class="story-text">{"and discovered they are"}</span>
|
||||
// <PowerfulSpan powerful={*powerful}/>
|
||||
// </span>
|
||||
// }
|
||||
// }
|
||||
// StoryActionResult::Adjudicator { killer } => html! {
|
||||
// <span>
|
||||
// <span class="story-text">{"and saw"}</span>
|
||||
// <KillerSpan killer={*killer}/>
|
||||
// </span>
|
||||
// },
|
||||
// StoryActionResult::Arcanist(same) => html! {
|
||||
// <span>
|
||||
// <span class="story-text">{"and saw"}</span>
|
||||
// <AlignmentComparisonSpan comparison={*same}/>
|
||||
// </span>
|
||||
// },
|
||||
// StoryActionResult::GraveDigger(None) => html! {
|
||||
// <span class="story-text">
|
||||
// {"found an empty grave"}
|
||||
// </span>
|
||||
// },
|
||||
// StoryActionResult::GraveDigger(Some(role_title)) => {
|
||||
// let category = Into::<SetupRole>::into(*role_title).category();
|
||||
// html! {
|
||||
// <span>
|
||||
// <span class="story-text">{"found the body of a"}</span>
|
||||
// <CategorySpan category={category} icon={role_title.icon()}>
|
||||
// {role_title.to_string().to_case(Case::Title)}
|
||||
// </CategorySpan>
|
||||
// </span>
|
||||
// }
|
||||
// }
|
||||
// StoryActionResult::Mortician(died_to_title) => html! {
|
||||
// <>
|
||||
// <span class="story-text">{"and found the cause of death to be"}</span>
|
||||
// <DiedToSpan died_to={*died_to_title}/>
|
||||
// </>
|
||||
// },
|
||||
// StoryActionResult::Insomniac { visits } => {
|
||||
// let visitors = visits
|
||||
// .iter()
|
||||
// .filter_map(|c| characters.get(c))
|
||||
// .map(|c| {
|
||||
// html! {
|
||||
// <CharacterCard faint=true char={c.clone()}/>
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// html! {
|
||||
// {visitors}
|
||||
// }
|
||||
// }
|
||||
|
||||
// StoryActionResult::Empath { scapegoat: false } => html! {
|
||||
// <>
|
||||
// <span class="story-text">{"and saw that they are"}</span>
|
||||
// <span class="attribute-span faint">
|
||||
// <div class="inactive">
|
||||
// <Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||
// </div>
|
||||
// {"Not The Scapegoat"}
|
||||
// </span>
|
||||
// </>
|
||||
// },
|
||||
// StoryActionResult::Empath { scapegoat: true } => html! {
|
||||
// <>
|
||||
// <span class="story-text">{"and saw that they are"}</span>
|
||||
// <span class="attribute-span faint wolves">
|
||||
// <div>
|
||||
// <Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||
// </div>
|
||||
// {"The Scapegoat"}
|
||||
// </span>
|
||||
// </>
|
||||
// },
|
||||
// }
|
||||
// }
|
||||
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// struct StoryNightChoiceProps {
|
||||
// choice: NightChoice,
|
||||
// characters: Rc<HashMap<CharacterId, Character>>,
|
||||
// }
|
||||
|
||||
// #[function_component]
|
||||
// fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoiceProps) -> Html {
|
||||
// let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
||||
// characters
|
||||
// .get(character_id)
|
||||
// .and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
||||
// .map(|(char, chosen)| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={char.clone()}/>
|
||||
// <span class="story-text">{action}</span>
|
||||
// <CharacterCard faint=true char={chosen.clone()}/>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// };
|
||||
// let choice_body = match &choice.prompt {
|
||||
// StoryActionPrompt::Arcanist {
|
||||
// character_id,
|
||||
// chosen: (chosen1, chosen2),
|
||||
// } => characters
|
||||
// .get(character_id)
|
||||
// .and_then(|arcanist| characters.get(chosen1).map(|c| (arcanist, c)))
|
||||
// .and_then(|(arcanist, chosen1)| {
|
||||
// characters
|
||||
// .get(chosen2)
|
||||
// .map(|chosen2| (arcanist, chosen1, chosen2))
|
||||
// })
|
||||
// .map(|(arcanist, chosen1, chosen2)| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={arcanist.clone()}/>
|
||||
// <span class="story-text">{"compared"}</span>
|
||||
// <CharacterCard faint=true char={chosen1.clone()}/>
|
||||
// <span class="story-text">{"and"}</span>
|
||||
// <CharacterCard faint=true char={chosen2.clone()}/>
|
||||
// </>
|
||||
// }
|
||||
// }),
|
||||
// StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| {
|
||||
// let masons = masons
|
||||
// .iter()
|
||||
// .filter_map(|m| characters.get(m))
|
||||
// .map(|c| {
|
||||
// html! {
|
||||
// <CharacterCard faint=true char={c.clone()}/>
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={leader.clone()}/>
|
||||
// <span class="story-text">{"'s masons"}</span>
|
||||
// {masons}
|
||||
// <span class="story-text">{"convened in secret"}</span>
|
||||
// </>
|
||||
// }
|
||||
// }),
|
||||
// StoryActionPrompt::Bloodletter { character_id, chosen } => generate(character_id, chosen, "spilt wolf blood on"),
|
||||
|
||||
// StoryActionPrompt::BeholderWakes { character_id }=>characters
|
||||
// .get(character_id)
|
||||
// .map(|char| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={char.clone()}/>
|
||||
// <span class="story-text">{"woke up and saw"}</span>
|
||||
// </>
|
||||
// }
|
||||
// }),
|
||||
// StoryActionPrompt::Vindicator {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// }
|
||||
// | StoryActionPrompt::Protector {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "protected"),
|
||||
// StoryActionPrompt::Gravedigger {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "dug up"),
|
||||
// StoryActionPrompt::Adjudicator {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// }
|
||||
// | StoryActionPrompt::PowerSeer {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// }
|
||||
// | StoryActionPrompt::Empath {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// }
|
||||
// | StoryActionPrompt::Seer {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "checked"),
|
||||
// StoryActionPrompt::Hunter {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "set a trap for"),
|
||||
// StoryActionPrompt::Militia {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "shot"),
|
||||
// StoryActionPrompt::MapleWolf {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => characters
|
||||
// .get(character_id)
|
||||
// .and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
||||
// .map(|(char, chosen)| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={char.clone()}/>
|
||||
// <span class="story-text">{"invited"}</span>
|
||||
// <CharacterCard faint=true char={chosen.clone()}/>
|
||||
// <span class="story-text">{"for dinner"}</span>
|
||||
// </>
|
||||
// }
|
||||
// }),
|
||||
// StoryActionPrompt::Guardian {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// guarding,
|
||||
// } => generate(
|
||||
// character_id,
|
||||
// chosen,
|
||||
// if *guarding { "guarded" } else { "protected" },
|
||||
// ),
|
||||
// StoryActionPrompt::Mortician {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "examined"),
|
||||
// StoryActionPrompt::Beholder {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "👁️"),
|
||||
|
||||
// StoryActionPrompt::MasonLeaderRecruit {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "tried recruiting"),
|
||||
// StoryActionPrompt::PyreMaster {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "torched"),
|
||||
// StoryActionPrompt::WolfPackKill { chosen } => {
|
||||
// characters.get(chosen).map(|chosen: &Character| {
|
||||
// html! {
|
||||
// <>
|
||||
// <AlignmentSpan alignment={Alignment::Wolves}/>
|
||||
// <span class="story-text">{"attempted a kill on"}</span>
|
||||
// <CharacterCard faint=true char={chosen.clone()} />
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// StoryActionPrompt::Shapeshifter { character_id } => {
|
||||
// if choice.result.is_none() {
|
||||
// return html!{};
|
||||
// }
|
||||
// characters.get(character_id).map(|shifter| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={shifter.clone()} />
|
||||
// <span class="story-text">{"decided to shapeshift into the wolf kill target"}</span>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// StoryActionPrompt::AlphaWolf {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "took a stab at"),
|
||||
// StoryActionPrompt::DireWolf {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "roleblocked"),
|
||||
// StoryActionPrompt::LoneWolfKill {
|
||||
// character_id,
|
||||
// chosen,
|
||||
// } => generate(character_id, chosen, "sought vengeance from"),
|
||||
// StoryActionPrompt::Insomniac { character_id } => {
|
||||
// characters.get(character_id).map(|insomniac| {
|
||||
// html! {
|
||||
// <>
|
||||
// <CharacterCard faint=true char={insomniac.clone()} />
|
||||
// <span class="story-text">{"witnessed visits from"}</span>
|
||||
// </>
|
||||
// }
|
||||
// })
|
||||
// }
|
||||
// };
|
||||
// let result = choice.result.as_ref().map(|result| {
|
||||
// html! {
|
||||
// <StoryNightResult result={result.clone()} characters={characters.clone()}/>
|
||||
// }
|
||||
// });
|
||||
// choice_body
|
||||
// .map(|choice_body| {
|
||||
// html! {
|
||||
// <li class="choice">
|
||||
// <Icon
|
||||
// source={IconSource::ListItem}
|
||||
// icon_type={IconType::Small}
|
||||
// classes={classes!("li-icon")}
|
||||
// />
|
||||
// {choice_body}
|
||||
// {result}
|
||||
// </li>
|
||||
// }
|
||||
// })
|
||||
// .unwrap_or_default()
|
||||
// }
|
||||
|
|
|
|||
Loading…
Reference in New Issue