diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index 972cbed..a2359f2 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -32,6 +32,7 @@ use crate::{ Alignment, HunterMut, HunterRef, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful, PreviousGuardianAction, Role, RoleTitle, }, + team::Team, }; type Result = core::result::Result; @@ -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::>() + .join(", ") ); } Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath { diff --git a/werewolves-proto/src/diedto.rs b/werewolves-proto/src/diedto.rs index ff6746d..b92980c 100644 --- a/werewolves-proto/src/diedto.rs +++ b/werewolves-proto/src/diedto.rs @@ -78,6 +78,30 @@ pub enum DiedTo { } impl DiedTo { + pub const fn day(&self) -> Option { + match self { + DiedTo::Execution { day } => Some(*day), + _ => None, + } + } + pub const fn night(&self) -> Option { + 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 { let mut next = self.clone(); match &mut next { diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index d97e094..cd1c0dc 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -192,6 +192,7 @@ impl Game { GameActions::NightDetails(NightDetails::new( &night.used_actions(), recorded_changes, + self.village(), )), )?; self.state = GameState::Day { diff --git a/werewolves-proto/src/game/story.rs b/werewolves-proto/src/game/story.rs index 423a70f..472b14e 100644 --- a/werewolves-proto/src/game/story.rs +++ b/werewolves-proto/src/game/story.rs @@ -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 { + pub fn new(prompt: ActionPrompt, result: ActionResult, village: &Village) -> Option { 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 { + pub fn new(prompt: ActionPrompt, village: &Village) -> Option { 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 { 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, .. } diff --git a/werewolves/index.scss b/werewolves/index.scss index 446249d..f842142 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -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; + } +} diff --git a/werewolves/src/class.rs b/werewolves/src/class.rs index 5b09853..294cf7b 100644 --- a/werewolves/src/class.rs +++ b/werewolves/src/class.rs @@ -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 . + +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::::into(self.role_title()) + .category() + .class(), + ) + } +} diff --git a/werewolves/src/components/attributes/align_span.rs b/werewolves/src/components/attributes/align_span.rs index fbf3f31..6a3e619 100644 --- a/werewolves/src/components/attributes/align_span.rs +++ b/werewolves/src/components/attributes/align_span.rs @@ -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! { - -
- -
+ + {alignment.to_string()} } diff --git a/werewolves/src/components/attributes/comparison.rs b/werewolves/src/components/attributes/comparison.rs index 49f132d..5959c61 100644 --- a/werewolves/src/components/attributes/comparison.rs +++ b/werewolves/src/components/attributes/comparison.rs @@ -29,20 +29,14 @@ pub fn AlignmentComparisonSpan( match comparison { AlignmentEq::Same => html! { -
-
+ {"the same"} -
-
}, AlignmentEq::Different => html! { -
-
+ {"different"} -
-
}, } diff --git a/werewolves/src/components/attributes/death_span.rs b/werewolves/src/components/attributes/death_span.rs index e34c4c6..a80051c 100644 --- a/werewolves/src/components/attributes/death_span.rs +++ b/werewolves/src/components/attributes/death_span.rs @@ -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! { - -
- -
+ + {died_to.to_string().to_case(Case::Title)} } diff --git a/werewolves/src/components/attributes/killer.rs b/werewolves/src/components/attributes/killer.rs index 76dc943..cc03a1b 100644 --- a/werewolves/src/components/attributes/killer.rs +++ b/werewolves/src/components/attributes/killer.rs @@ -29,10 +29,12 @@ pub fn KillerSpan(KillerSpanProps { killer }: &KillerSpanProps) -> Html { Killer::NotKiller => "inactive", }; html! { - -
- -
+ + {killer.to_string()} } diff --git a/werewolves/src/components/attributes/powerful.rs b/werewolves/src/components/attributes/powerful.rs index d75a604..13e3fab 100644 --- a/werewolves/src/components/attributes/powerful.rs +++ b/werewolves/src/components/attributes/powerful.rs @@ -29,10 +29,12 @@ pub fn PowerfulSpan(PowerfulSpanProps { powerful }: &PowerfulSpanProps) -> Html Powerful::NotPowerful => "inactive", }; html! { - -
- -
+ + {powerful.to_string()} } diff --git a/werewolves/src/components/character.rs b/werewolves/src/components/character.rs index 2a8ecc7..0d22fb0 100644 --- a/werewolves/src/components/character.rs +++ b/werewolves/src/components/character.rs @@ -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 . 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(
} } + +#[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! { + + } + }) + .unwrap_or(html! { +
+ }); + 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! { + + {icon} + {char.number().get()} + {char.name()} + + } +} diff --git a/werewolves/src/components/role.rs b/werewolves/src/components/role.rs new file mode 100644 index 0000000..28bcb77 --- /dev/null +++ b/werewolves/src/components/role.rs @@ -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 . + +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::::into(*role).category().class(); + let icon = role + .icon() + .map(|icon| { + html! { + + } + }) + .unwrap_or(html! {
}); + let role_name = role.to_string().to_case(Case::Title); + html! { + + {icon} + {role_name} + + } +} + +#[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! {{"Drunk"}} + } else { + children.clone() + }; + html! { + + + {content} + + } +} diff --git a/werewolves/src/components/story/character.rs b/werewolves/src/components/story/character.rs index b92b0ff..5f4bb31 100644 --- a/werewolves/src/components/story/character.rs +++ b/werewolves/src/components/story/character.rs @@ -13,85 +13,152 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +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::>(); 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! { - + } }) .collect::(); + + 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! { + + } + }) + .collect::(); + let changes = changes + .iter() + .map(|c| { + html! { + + } + }) + .collect::(); + let changes = (changes == html! {}).not().then_some(html! { + <> + {"changes"} +
+ {changes} +
+ + }); + let choices = (choices == html! {}).not().then_some(html! { + <> + {"choices"} +
+ {choices} +
+ + }); + html! { + <> + {choices} + {changes} + + } + }); html! {
- - {by_time} - -
- } -} - -#[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! { -
- - {time_text} - -
- {"hello"} +
+ {tabs} +
+
+ {story_content}
} @@ -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 { + all_characters + .iter() + .find(|c| c.character_id() == chosen) + .map(|chosen| { + html! { + <> + + {wording} + + + } + }) + }; 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! { + <> + + {"checked"} + + {"and"} + + + } + }), + 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! { + <> + + {"woke with the masons of"} + + + } + }), + 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! { + <> + + {"chose to shift into"} + + + } + }), + StoryActionPrompt::Insomniac { .. } => Some(html! { + <> + + {"woke in the night due to visits from: "} + + }), + 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! { + <> + {"but was"} + {"Roleblocked"} + + }, + StoryActionResult::Seer(alignment) => html! { + <> + {"and saw"} + + + }, + StoryActionResult::PowerSeer { powerful } => html! { + <> + {"and saw"} + + + }, + StoryActionResult::Adjudicator { killer } => html! { + <> + {"and saw"} + + + }, + StoryActionResult::Arcanist(alignment_eq) => html! { + <> + {"and saw them as"} + + + }, + StoryActionResult::GraveDigger(None) => html! { + {"but found an empty grave"} + }, + StoryActionResult::GraveDigger(Some(role_title)) => html! { + <> + {"as"} + + + }, + StoryActionResult::Mortician(died_to_title) => html! { + <> + {"and found"} + + {"to be the cause of death"} + + }, + 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! { + + } + }) + .collect::() + } + StoryActionResult::Empath { scapegoat: true } => { + html! {{"and found their scapegoat"}} + } + StoryActionResult::Empath { scapegoat: false } => { + html! {{"who was not a scapegoat"}} + } + StoryActionResult::BeholderSawNothing => html! { + <> + {"and saw"} + {"nothing"} + + }, + StoryActionResult::BeholderSawEverything => html! { + <> + {"and saw"} + {"everything"} + + }, + StoryActionResult::ShiftFailed => html! { + {"however, their shift failed"} + }, + StoryActionResult::Drunk => html! { + <> + {"but got"} + + + }, + }); + html! { +
+ {prompt} + {result} +
+ } } #[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! { + <> + {"role changed to"} + + + }), + NightChange::Kill { died_to, .. } => Some(html! { + <> + {"died to"} + + + }), 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! { + <> + {"had visitors role blocked by"} + + + } + }), + NightChange::Shapeshift { source, .. } => all_characters + .iter() + .find(|c| c.character_id() == *source) + .map(|source| { + html! { + <> + {"shapeshifted by"} + + {"and became a werewolf"} + + } + }), + NightChange::ElderReveal { .. } => Some(html! { + <> + {"learned they are the"} + + + }), 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! { + <> + + {"recruited"} + + {"into the masons"} + + } + }), + NightChange::ApplyAura { source, aura, .. } => { + let from = all_characters + .iter() + .find(|c| c.character_id() == *source) + .map(|source| { + html! { + <> + {"from"} + + + } + }); + Some(html! { + <> + {"received the"} + + {"aura"} + {from} + + }) + } + NightChange::LostAura { aura, .. } => Some(html! { + <> + {"lost the"} + + {"aura"} + + }), + NightChange::EmpathFoundScapegoat { scapegoat, .. } => all_characters + .iter() + .find(|c| c.character_id() == *scapegoat) + .map(|scapegoat| { + html! { + <> + {"took on the scapegoat's curse from"} + + + } + }), + NightChange::HunterTarget { source, .. } => all_characters + .iter() + .find(|c| c.character_id() == *source) + .map(|source| { + html! { + <> + {"had the hunter's mark placed on them by"} + + + } + }), + 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! { + <> + {"was guarded by"} + + + } + }), + Protection::Guardian { + source, + guarding: false, + } + | Protection::Protector { source } + | Protection::Vindicator { source } => all_characters + .iter() + .find(|s| s.character_id() == *source) + .map(|source| { + html! { + <> + {"was protected by"} + + + } + }), + }; + all_characters + .iter() + .find(|t| t.character_id() == *target) + .and_then(|target| prot.map(|prot| (target, prot))) + .map(|(target, prot)| { + html! { + <> + + {prot} + + } + }) } } - html! { -
-
- } + .map(|change| { + html! { +
+ {change} +
+ } + }) + .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::::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(
}); - 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! { -
-
- {icon} - - {dead_icon} -
-
- {children.clone()} -
+
+ {icon} + + {dead_icon}
} } diff --git a/werewolves/src/components/story/gamegen.rs b/werewolves/src/components/story/gamegen.rs index 4835121..b3128ae 100644 --- a/werewolves/src/components/story/gamegen.rs +++ b/werewolves/src/components/story/gamegen.rs @@ -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(); diff --git a/werewolves/src/components/story/story.rs b/werewolves/src/components/story/story.rs index c596080..69bda4f 100644 --- a/werewolves/src/components/story/story.rs +++ b/werewolves/src/components/story/story.rs @@ -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::>(); + 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! { - + } }) .collect::(); + let all_characters = village.characters().into_iter().collect::>(); + let actions = selected_char.as_ref().map(|(char, actions)| { + let class = Into::::into(char.role_title()) + .category() + .class(); + + html! { + <> + +
+ +
+ + } + }); + html! {
- {chars} +
+ {chars} +
+
+ {actions} +
} } - -// #[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! { -// <> -// -// -// } -// }) -// .collect::(); -// 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::>())) -// .unwrap_or_else(|| -// Rc::new(story.starting_village -// .characters().into_iter() -// .map(|c| (c.character_id(), c)) -// .collect::>())); -// 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! { -// -// } -// }) -// .collect::(); - -// day_changes.is_empty().not().then_some(html! { -//
-//

{"village executed"}

-//
-// {execute_list} -//
-//
-// }) -// } -// GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({ -// let choices = details -// .choices -// .iter() -// .map(|c| { -// html! { -// -// } -// }) -// .collect::(); - -// let changes = details -// .changes -// .iter() -// .map(|c| { -// html! { -//
  • -// -//
  • -// } -// }) -// .collect::(); - -// html! { -//
    -// -//
      -// {choices} -//
    -// -//
      -// {changes} -//
    -//
    -// } -// }), -// }; - -// changes -// .map(|changes| { -// html! { -//
    -//

    {"on "}{time.to_string()}{"..."}

    -// {changes} -//
    -// } -// }) -// .unwrap_or_default() -// }) -// .collect::(); -// html! { -//
    -//
    -// {final_characters} -//
    -// {bits} -//
    -// } -// } - -// #[derive(Debug, Clone, PartialEq, Properties)] -// struct StoryNightChangeProps { -// change: NightChange, -// characters: Rc>, -// } - -// #[function_component] -// fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html { -// match change { -// NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{ -// <> -// -// {"lost the"} -// -// {"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!{ -// <> -// -// {"gained the"} -// -// {"aura from"} -// -// -// } -// }).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! { -// <> -// -// {"is now"} -// -// -// } -// }) -// .unwrap_or_default(), - -// NightChange::Kill { target, died_to } => { - -// characters -// .get(target) -// .map(|target| { -// html! { -// <> -// -// -// {"died to"} -// -// -// } -// }) -// .unwrap_or_default() -// }, -// NightChange::RoleBlock { source, target, .. } => characters -// .get(source) -// .and_then(|s| characters.get(target).map(|t| (s, t))) -// .map(|(source, target)| { -// html! { -// <> -// -// {"role blocked"} -// -// -// } -// }) -// .unwrap_or_default(), -// NightChange::Shapeshift { source, into } => characters -// .get(source) -// .and_then(|s| characters.get(into).map(|i| (s, i))) -// .map(|(source, into)| { -// html! { -// <> -// -// {"shapeshifted into"} -// -// -// } -// }) -// .unwrap_or_default(), - -// NightChange::ElderReveal { elder } => characters -// .get(elder) -// .map(|elder| { -// html! { -// <> -// -// {"learned they are the Elder"} -// -// } -// }) -// .unwrap_or_default(), -// NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters -// .get(empath) -// .and_then(|e| characters.get(scapegoat).map(|s| (e, s))) -// .map(|(empath, scapegoat)| { -// html! { -// <> -// -// {"found the scapegoat in"} -// -// {"and took on their curse"} -// -// } -// }) -// .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>, -// } - -// #[function_component] -// fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html { -// match result { -// StoryActionResult::ShiftFailed => html!{ -// {"but it failed"} -// }, -// StoryActionResult::Drunk => html! { -// <> -// {"but got "} -// -// {" instead"} -// -// }, -// StoryActionResult::BeholderSawEverything => html!{ -// {"and saw everything 👁️"} -// }, -// StoryActionResult::BeholderSawNothing => html!{ -// {"but saw nothing"} -// }, -// StoryActionResult::RoleBlocked => html! { -// {"but was role blocked"} -// }, -// StoryActionResult::Seer(alignment) => { -// html! { -// -// {"and saw"} -// -// -// } -// } -// StoryActionResult::PowerSeer { powerful } => { -// html! { -// -// {"and discovered they are"} -// -// -// } -// } -// StoryActionResult::Adjudicator { killer } => html! { -// -// {"and saw"} -// -// -// }, -// StoryActionResult::Arcanist(same) => html! { -// -// {"and saw"} -// -// -// }, -// StoryActionResult::GraveDigger(None) => html! { -// -// {"found an empty grave"} -// -// }, -// StoryActionResult::GraveDigger(Some(role_title)) => { -// let category = Into::::into(*role_title).category(); -// html! { -// -// {"found the body of a"} -// -// {role_title.to_string().to_case(Case::Title)} -// -// -// } -// } -// StoryActionResult::Mortician(died_to_title) => html! { -// <> -// {"and found the cause of death to be"} -// -// -// }, -// StoryActionResult::Insomniac { visits } => { -// let visitors = visits -// .iter() -// .filter_map(|c| characters.get(c)) -// .map(|c| { -// html! { -// -// } -// }) -// .collect::(); -// html! { -// {visitors} -// } -// } - -// StoryActionResult::Empath { scapegoat: false } => html! { -// <> -// {"and saw that they are"} -// -//
    -// -//
    -// {"Not The Scapegoat"} -//
    -// -// }, -// StoryActionResult::Empath { scapegoat: true } => html! { -// <> -// {"and saw that they are"} -// -//
    -// -//
    -// {"The Scapegoat"} -//
    -// -// }, -// } -// } - -// #[derive(Debug, Clone, PartialEq, Properties)] -// struct StoryNightChoiceProps { -// choice: NightChoice, -// characters: Rc>, -// } - -// #[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! { -// <> -// -// {action} -// -// -// } -// }) -// }; -// 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! { -// <> -// -// {"compared"} -// -// {"and"} -// -// -// } -// }), -// StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| { -// let masons = masons -// .iter() -// .filter_map(|m| characters.get(m)) -// .map(|c| { -// html! { -// -// } -// }) -// .collect::(); -// html! { -// <> -// -// {"'s masons"} -// {masons} -// {"convened in secret"} -// -// } -// }), -// StoryActionPrompt::Bloodletter { character_id, chosen } => generate(character_id, chosen, "spilt wolf blood on"), - -// StoryActionPrompt::BeholderWakes { character_id }=>characters -// .get(character_id) -// .map(|char| { -// html! { -// <> -// -// {"woke up and saw"} -// -// } -// }), -// 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! { -// <> -// -// {"invited"} -// -// {"for dinner"} -// -// } -// }), -// 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! { -// <> -// -// {"attempted a kill on"} -// -// -// } -// }) -// } -// StoryActionPrompt::Shapeshifter { character_id } => { -// if choice.result.is_none() { -// return html!{}; -// } -// characters.get(character_id).map(|shifter| { -// html! { -// <> -// -// {"decided to shapeshift into the wolf kill target"} -// -// } -// }) -// } -// 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! { -// <> -// -// {"witnessed visits from"} -// -// } -// }) -// } -// }; -// let result = choice.result.as_ref().map(|result| { -// html! { -// -// } -// }); -// choice_body -// .map(|choice_body| { -// html! { -//
  • -// -// {choice_body} -// {result} -//
  • -// } -// }) -// .unwrap_or_default() -// }