insomniac role + mason fixes

also styling improvements
This commit is contained in:
emilis 2025-10-07 17:45:21 +01:00
parent ae71ea4eb0
commit 17f583539d
No known key found for this signature in database
20 changed files with 826 additions and 127 deletions

View File

@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize};
use crate::{ use crate::{
diedto::DiedTo, diedto::DiedTo,
error::GameError, error::GameError,
game::{DateTime, Village}, game::{DateTime, Village, night::NightChange},
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
modifier::Modifier, modifier::Modifier,
player::{PlayerId, RoleChange}, player::{PlayerId, RoleChange},
@ -217,7 +217,58 @@ impl Character {
) )
} }
fn mason_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
if !self.role.wakes(village) {
return Ok(Box::new([]));
}
let (recruits, recruits_available) = match &self.role {
Role::MasonLeader {
recruits,
recruits_available,
} => (recruits, *recruits_available),
_ => {
return Err(GameError::InvalidRole {
expected: RoleTitle::MasonLeader,
got: self.role_title(),
});
}
};
let recruits = recruits
.iter()
.filter_map(|r| village.character_by_id(*r).ok())
.filter_map(|c| c.is_village().then_some(c.identity()))
.chain((self.is_village() && self.alive()).then_some(self.identity()))
.collect::<Box<[_]>>();
Ok(recruits
.is_empty()
.not()
.then_some(ActionPrompt::MasonsWake {
leader: self.character_id(),
masons: recruits.clone(),
})
.into_iter()
.chain(
self.alive()
.then_some(())
.and_then(|_| NonZeroU8::new(recruits_available))
.map(|recruits_available| ActionPrompt::MasonLeaderRecruit {
character_id: self.identity(),
recruits_left: recruits_available,
potential_recruits: village
.living_players_excluding(self.character_id())
.into_iter()
.filter(|c| !recruits.iter().any(|r| r.character_id == c.character_id))
.collect(),
marked: None,
}),
)
.collect())
}
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> { pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
if self.mason_leader().is_ok() {
return self.mason_prompts(village);
}
if !self.alive() || !self.role.wakes(village) { if !self.alive() || !self.role.wakes(village) {
return Ok(Box::new([])); return Ok(Box::new([]));
} }
@ -241,6 +292,11 @@ impl Character {
.. ..
} }
| Role::Villager => return Ok(Box::new([])), | Role::Villager => return Ok(Box::new([])),
Role::Insomniac => ActionPrompt::Insomniac {
character_id: self.identity(),
},
Role::Scapegoat { redeemed: true } => { Role::Scapegoat { redeemed: true } => {
let mut dead = village.dead_characters(); let mut dead = village.dead_characters();
dead.shuffle(&mut rand::rng()); dead.shuffle(&mut rand::rng());
@ -414,36 +470,11 @@ impl Character {
living_players: village.living_players_excluding(self.character_id()), living_players: village.living_players_excluding(self.character_id()),
marked: None, marked: None,
}, },
Role::MasonLeader { Role::MasonLeader { .. } => {
recruits_available, log::error!(
recruits, "night_action_prompts got to MasonLeader, should be handled before the living check"
} => { );
return Ok(recruits return Ok(Box::new([]));
.is_empty()
.not()
.then_some(ActionPrompt::MasonsWake {
character_id: self.identity(),
masons: recruits
.iter()
.map(|r| village.character_by_id(*r).map(|c| c.identity()))
.collect::<Result<Box<[CharacterIdentity]>>>()?,
})
.into_iter()
.chain(
NonZeroU8::new(*recruits_available).map(|recruits_available| {
ActionPrompt::MasonLeaderRecruit {
character_id: self.identity(),
recruits_left: recruits_available,
potential_recruits: village
.living_players_excluding(self.character_id())
.into_iter()
.filter(|c| !recruits.contains(&c.character_id))
.collect(),
marked: None,
}
}),
)
.collect());
} }
Role::Empath { cursed: false } => ActionPrompt::Empath { Role::Empath { cursed: false } => ActionPrompt::Empath {
character_id: self.identity(), character_id: self.identity(),

View File

@ -13,7 +13,10 @@ use crate::{
DateTime, Village, DateTime, Village,
kill::{self, ChangesLookup}, kill::{self, ChangesLookup},
}, },
message::night::{ActionPrompt, ActionResponse, ActionResult}, message::{
CharacterIdentity,
night::{ActionPrompt, ActionResponse, ActionResult, Visits},
},
player::Protection, player::Protection,
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle}, role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle},
}; };
@ -78,7 +81,8 @@ impl From<ActionComplete> for ResponseOutcome {
impl ActionPrompt { impl ActionPrompt {
fn unless(&self) -> Option<Unless> { fn unless(&self) -> Option<Unless> {
match &self { match &self {
ActionPrompt::MasonsWake { .. } ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. } | ActionPrompt::RoleChange { .. }
| ActionPrompt::Shapeshifter { .. } | ActionPrompt::Shapeshifter { .. }
@ -513,10 +517,9 @@ impl Night {
) -> Result<ActionResult> { ) -> Result<ActionResult> {
if self.village.character_by_id(recruiting)?.is_village() { if self.village.character_by_id(recruiting)?.is_village() {
if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a { if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a {
ActionPrompt::MasonsWake { ActionPrompt::MasonsWake { leader, masons, .. } => {
character_id, (*leader == mason_leader).then_some(masons)
masons, }
} => (character_id.character_id == mason_leader).then_some(masons),
_ => None, _ => None,
}) { }) {
let mut ext_masons = masons.to_vec(); let mut ext_masons = masons.to_vec();
@ -524,7 +527,7 @@ impl Night {
*masons = ext_masons.into_boxed_slice(); *masons = ext_masons.into_boxed_slice();
} else { } else {
self.action_queue.push_front(ActionPrompt::MasonsWake { self.action_queue.push_front(ActionPrompt::MasonsWake {
character_id: self.village.character_by_id(mason_leader)?.identity(), leader: self.village.character_by_id(mason_leader)?.character_id(),
masons: Box::new([self.village.character_by_id(recruiting)?.identity()]), masons: Box::new([self.village.character_by_id(recruiting)?.identity()]),
}); });
} }
@ -794,6 +797,15 @@ impl Night {
}; };
} }
ActionResponse::Continue => { ActionResponse::Continue => {
if let ActionPrompt::Insomniac { character_id } = current_prompt {
return Ok(ActionComplete {
result: ActionResult::Insomniac(
self.get_visits_for(character_id.character_id),
),
change: None,
}
.into());
}
if let ActionPrompt::RoleChange { if let ActionPrompt::RoleChange {
character_id, character_id,
new_role, new_role,
@ -813,8 +825,8 @@ impl Night {
match current_prompt { match current_prompt {
ActionPrompt::LoneWolfKill { ActionPrompt::LoneWolfKill {
character_id, character_id,
living_players,
marked: Some(marked), marked: Some(marked),
..
} => Ok(ActionComplete { } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep, result: ActionResult::GoBackToSleep,
change: Some(NightChange::Kill { change: Some(NightChange::Kill {
@ -1144,6 +1156,11 @@ impl Night {
}), }),
} }
.into()), .into()),
ActionPrompt::Insomniac { .. } => Ok(ActionComplete {
result: ActionResult::GoBackToSleep,
change: None,
}
.into()),
ActionPrompt::PyreMaster { ActionPrompt::PyreMaster {
character_id, character_id,
marked: Some(marked), marked: Some(marked),
@ -1226,7 +1243,8 @@ impl Night {
current_prompt, current_prompt,
current_result: _, current_result: _,
} => match current_prompt { } => match current_prompt {
ActionPrompt::LoneWolfKill { character_id, .. } ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::LoneWolfKill { character_id, .. }
| ActionPrompt::ElderReveal { character_id } | ActionPrompt::ElderReveal { character_id }
| ActionPrompt::RoleChange { character_id, .. } | ActionPrompt::RoleChange { character_id, .. }
| ActionPrompt::Seer { character_id, .. } | ActionPrompt::Seer { character_id, .. }
@ -1243,13 +1261,14 @@ impl Night {
| ActionPrompt::PowerSeer { character_id, .. } | ActionPrompt::PowerSeer { character_id, .. }
| ActionPrompt::Mortician { character_id, .. } | ActionPrompt::Mortician { character_id, .. }
| ActionPrompt::Beholder { character_id, .. } | ActionPrompt::Beholder { character_id, .. }
| ActionPrompt::MasonsWake { character_id, .. }
| ActionPrompt::MasonLeaderRecruit { character_id, .. } | ActionPrompt::MasonLeaderRecruit { character_id, .. }
| ActionPrompt::Empath { character_id, .. } | ActionPrompt::Empath { character_id, .. }
| ActionPrompt::Vindicator { character_id, .. } | ActionPrompt::Vindicator { character_id, .. }
| ActionPrompt::PyreMaster { character_id, .. } | ActionPrompt::PyreMaster { character_id, .. }
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id), | ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
ActionPrompt::WolvesIntro { wolves: _ } ActionPrompt::WolvesIntro { wolves: _ }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::WolfPackKill { .. } | ActionPrompt::WolfPackKill { .. }
| ActionPrompt::CoverOfDarkness => None, | ActionPrompt::CoverOfDarkness => None,
}, },
@ -1282,6 +1301,13 @@ impl Night {
NightState::Complete => return Err(GameError::NightOver), NightState::Complete => return Err(GameError::NightOver),
} }
if let Some(prompt) = self.action_queue.pop_front() { if let Some(prompt) = self.action_queue.pop_front() {
if let ActionPrompt::Insomniac { character_id } = &prompt
&& self.get_visits_for(character_id.character_id).is_empty()
{
// skip!
self.used_actions.pop(); // it will be re-added
return self.next();
}
self.night_state = NightState::Active { self.night_state = NightState::Active {
current_prompt: prompt, current_prompt: prompt,
current_result: None, current_result: None,
@ -1296,6 +1322,147 @@ impl Night {
pub const fn changes(&self) -> &[NightChange] { pub const fn changes(&self) -> &[NightChange] {
self.changes.as_slice() self.changes.as_slice()
} }
pub fn get_visits_for(&self, visit_char: CharacterId) -> Visits {
Visits::new(
self.used_actions
.iter()
.filter_map(|(prompt, _)| match prompt {
ActionPrompt::Arcanist {
character_id,
marked: (Some(marked1), Some(marked2)),
..
} => (*marked1 == visit_char || *marked2 == visit_char)
.then_some(character_id.clone()),
ActionPrompt::WolfPackKill {
marked: Some(marked),
..
} => (*marked == visit_char)
.then(|| self.village.killing_wolf().map(|c| c.identity()))
.flatten(),
ActionPrompt::Seer {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Protector {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Gravedigger {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Hunter {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Militia {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::MapleWolf {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Guardian {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Adjudicator {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::PowerSeer {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Mortician {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Beholder {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::MasonLeaderRecruit {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Empath {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::Vindicator {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::PyreMaster {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::AlphaWolf {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::DireWolf {
character_id,
marked: Some(marked),
..
}
| ActionPrompt::LoneWolfKill {
character_id,
marked: Some(marked),
..
} => (*marked == visit_char).then(|| character_id.clone()),
ActionPrompt::WolfPackKill { marked: None, .. }
| ActionPrompt::Arcanist { marked: _, .. }
| ActionPrompt::LoneWolfKill { marked: None, .. }
| ActionPrompt::Seer { marked: None, .. }
| ActionPrompt::Protector { marked: None, .. }
| ActionPrompt::Gravedigger { marked: None, .. }
| ActionPrompt::Hunter { marked: None, .. }
| ActionPrompt::Militia { marked: None, .. }
| ActionPrompt::MapleWolf { marked: None, .. }
| ActionPrompt::Guardian { marked: None, .. }
| ActionPrompt::Adjudicator { marked: None, .. }
| ActionPrompt::PowerSeer { marked: None, .. }
| ActionPrompt::Mortician { marked: None, .. }
| ActionPrompt::Beholder { marked: None, .. }
| ActionPrompt::MasonLeaderRecruit { marked: None, .. }
| ActionPrompt::Empath { marked: None, .. }
| ActionPrompt::Vindicator { marked: None, .. }
| ActionPrompt::PyreMaster { marked: None, .. }
| ActionPrompt::AlphaWolf { marked: None, .. }
| ActionPrompt::DireWolf { marked: None, .. }
| ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. }
| ActionPrompt::ElderReveal { .. }
| ActionPrompt::Shapeshifter { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::Insomniac { .. } => None,
})
.collect(),
)
}
} }
pub enum ServerAction { pub enum ServerAction {

View File

@ -117,6 +117,8 @@ pub enum SetupRole {
#[checks(Category::Intel)] #[checks(Category::Intel)]
Adjudicator, Adjudicator,
#[checks(Category::Intel)] #[checks(Category::Intel)]
Insomniac,
#[checks(Category::Intel)]
PowerSeer, PowerSeer,
#[checks(Category::Intel)] #[checks(Category::Intel)]
Mortician, Mortician,
@ -141,6 +143,7 @@ pub enum SetupRole {
impl SetupRoleTitle { impl SetupRoleTitle {
pub fn into_role(self) -> Role { pub fn into_role(self) -> Role {
match self { match self {
SetupRoleTitle::Insomniac => Role::Insomniac,
SetupRoleTitle::LoneWolf => Role::LoneWolf, SetupRoleTitle::LoneWolf => Role::LoneWolf,
SetupRoleTitle::Villager => Role::Villager, SetupRoleTitle::Villager => Role::Villager,
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false }, SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
@ -191,6 +194,7 @@ impl SetupRoleTitle {
impl Display for SetupRole { impl Display for SetupRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self { f.write_str(match self {
SetupRole::Insomniac => "Insomniac",
SetupRole::LoneWolf => "Lone Wolf", SetupRole::LoneWolf => "Lone Wolf",
SetupRole::Villager => "Villager", SetupRole::Villager => "Villager",
SetupRole::Scapegoat { .. } => "Scapegoat", SetupRole::Scapegoat { .. } => "Scapegoat",
@ -226,6 +230,7 @@ impl Display for SetupRole {
impl SetupRole { impl SetupRole {
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> { pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
Ok(match self { Ok(match self {
SetupRole::Insomniac => Role::Insomniac,
SetupRole::LoneWolf => Role::LoneWolf, SetupRole::LoneWolf => Role::LoneWolf,
SetupRole::Villager => Role::Villager, SetupRole::Villager => Role::Villager,
SetupRole::Scapegoat { redeemed } => Role::Scapegoat { SetupRole::Scapegoat { redeemed } => Role::Scapegoat {
@ -289,34 +294,12 @@ impl SetupRole {
impl From<SetupRole> for RoleTitle { impl From<SetupRole> for RoleTitle {
fn from(value: SetupRole) -> Self { fn from(value: SetupRole) -> Self {
match value { match value {
SetupRole::LoneWolf => RoleTitle::LoneWolf,
SetupRole::Villager => RoleTitle::Villager,
SetupRole::Scapegoat { .. } => RoleTitle::Scapegoat, SetupRole::Scapegoat { .. } => RoleTitle::Scapegoat,
SetupRole::Seer => RoleTitle::Seer,
SetupRole::Arcanist => RoleTitle::Arcanist,
SetupRole::Gravedigger => RoleTitle::Gravedigger,
SetupRole::Hunter => RoleTitle::Hunter,
SetupRole::Militia => RoleTitle::Militia,
SetupRole::MapleWolf => RoleTitle::MapleWolf,
SetupRole::Guardian => RoleTitle::Guardian,
SetupRole::Protector => RoleTitle::Protector,
SetupRole::Apprentice { .. } => RoleTitle::Apprentice, SetupRole::Apprentice { .. } => RoleTitle::Apprentice,
SetupRole::Elder { .. } => RoleTitle::Elder, other => other
SetupRole::Werewolf => RoleTitle::Werewolf, .into_role(&[])
SetupRole::AlphaWolf => RoleTitle::AlphaWolf, .map(|r| r.title())
SetupRole::DireWolf => RoleTitle::DireWolf, .unwrap_or(RoleTitle::Villager),
SetupRole::Shapeshifter => RoleTitle::Shapeshifter,
SetupRole::Adjudicator => RoleTitle::Adjudicator,
SetupRole::PowerSeer => RoleTitle::PowerSeer,
SetupRole::Mortician => RoleTitle::Mortician,
SetupRole::Beholder => RoleTitle::Beholder,
SetupRole::MasonLeader { .. } => RoleTitle::MasonLeader,
SetupRole::Empath => RoleTitle::Empath,
SetupRole::Vindicator => RoleTitle::Vindicator,
SetupRole::Diseased => RoleTitle::Diseased,
SetupRole::BlackKnight => RoleTitle::BlackKnight,
SetupRole::Weightlifter => RoleTitle::Weightlifter,
SetupRole::PyreMaster => RoleTitle::PyreMaster,
} }
} }
} }
@ -324,6 +307,7 @@ impl From<SetupRole> for RoleTitle {
impl From<RoleTitle> for SetupRole { impl From<RoleTitle> for SetupRole {
fn from(value: RoleTitle) -> Self { fn from(value: RoleTitle) -> Self {
match value { match value {
RoleTitle::Insomniac => SetupRole::Insomniac,
RoleTitle::LoneWolf => SetupRole::LoneWolf, RoleTitle::LoneWolf => SetupRole::LoneWolf,
RoleTitle::Villager => SetupRole::Villager, RoleTitle::Villager => SetupRole::Villager,
RoleTitle::Scapegoat => SetupRole::Scapegoat { RoleTitle::Scapegoat => SetupRole::Scapegoat {

View File

@ -286,6 +286,7 @@ impl Village {
impl RoleTitle { impl RoleTitle {
pub fn title_to_role_excl_apprentice(self) -> Role { pub fn title_to_role_excl_apprentice(self) -> Role {
match self { match self {
RoleTitle::Insomniac => Role::Insomniac,
RoleTitle::LoneWolf => Role::LoneWolf, RoleTitle::LoneWolf => Role::LoneWolf,
RoleTitle::Villager => Role::Villager, RoleTitle::Villager => Role::Villager,
RoleTitle::Scapegoat => Role::Scapegoat { RoleTitle::Scapegoat => Role::Scapegoat {

View File

@ -8,7 +8,7 @@ use crate::{
message::{ message::{
CharacterState, Identification, PublicIdentity, CharacterState, Identification, PublicIdentity,
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage}, host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult}, night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
}, },
player::PlayerId, player::PlayerId,
role::{Alignment, RoleTitle}, role::{Alignment, RoleTitle},
@ -63,6 +63,7 @@ pub trait ActionPromptTitleExt {
fn empath(&self); fn empath(&self);
fn adjudicator(&self); fn adjudicator(&self);
fn lone_wolf(&self); fn lone_wolf(&self);
fn insomniac(&self);
} }
impl ActionPromptTitleExt for ActionPromptTitle { impl ActionPromptTitleExt for ActionPromptTitle {
@ -135,12 +136,17 @@ impl ActionPromptTitleExt for ActionPromptTitle {
fn lone_wolf(&self) { fn lone_wolf(&self) {
assert_eq!(*self, ActionPromptTitle::LoneWolfKill) assert_eq!(*self, ActionPromptTitle::LoneWolfKill)
} }
fn insomniac(&self) {
assert_eq!(*self, ActionPromptTitle::Insomniac)
}
} }
pub trait ActionResultExt { pub trait ActionResultExt {
fn sleep(&self); fn sleep(&self);
fn r#continue(&self); fn r#continue(&self);
fn seer(&self) -> Alignment; fn seer(&self) -> Alignment;
fn insomniac(&self) -> Visits;
fn arcanist(&self) -> bool;
} }
impl ActionResultExt for ActionResult { impl ActionResultExt for ActionResult {
@ -158,6 +164,20 @@ impl ActionResultExt for ActionResult {
_ => panic!("expected a seer result"), _ => panic!("expected a seer result"),
} }
} }
fn arcanist(&self) -> bool {
match self {
ActionResult::Arcanist { same } => *same,
_ => panic!("expected an arcanist result"),
}
}
fn insomniac(&self) -> Visits {
match self {
ActionResult::Insomniac(v) => v.clone(),
_ => panic!("expected an insomniac result"),
}
}
} }
pub trait AlignmentExt { pub trait AlignmentExt {
@ -304,7 +324,8 @@ impl GameExt for Game {
fn mark_and_check(&mut self, mark: CharacterId) { fn mark_and_check(&mut self, mark: CharacterId) {
let prompt = self.mark(mark); let prompt = self.mark(mark);
match prompt { match prompt {
ActionPrompt::MasonsWake { .. } ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. } | ActionPrompt::ElderReveal { .. }
| ActionPrompt::CoverOfDarkness | ActionPrompt::CoverOfDarkness
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }

View File

@ -0,0 +1,76 @@
#[allow(unused)]
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
use crate::{
diedto::DiedTo,
game::{Game, GameSettings, OrRandom, SetupRole},
game_test::{
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
},
message::night::{ActionPromptTitle, Visits},
role::{Role, RoleTitle},
};
#[test]
fn is_told_theyre_villager() {
assert_eq!(Role::Insomniac.initial_shown_role(), RoleTitle::Villager);
}
#[test]
fn sees_visits() {
let players = gen_players(1..21);
let insomniac_player_id = players[0].player_id;
let wolf_player_id = players[1].player_id;
let seer_player_id = players[2].player_id;
let arcanist_player_id = players[3].player_id;
let mut settings = GameSettings::empty();
settings.add_and_assign(SetupRole::Insomniac, insomniac_player_id);
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
settings.add_and_assign(SetupRole::Seer, seer_player_id);
settings.add_and_assign(SetupRole::Arcanist, arcanist_player_id);
settings.fill_remaining_slots_with_villagers(20);
let mut game = Game::new(&players, settings).unwrap();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next().title().seer();
game.mark(game.living_villager().character_id());
game.r#continue().seer().village();
game.next().title().arcanist();
let mut villagers = game.villager_character_ids().into_iter();
game.mark(villagers.next().unwrap());
game.mark(villagers.next().unwrap());
assert_eq!(game.r#continue().arcanist(), true);
game.next_expect_day();
game.execute().title().wolf_pack_kill();
game.mark(game.living_villager().character_id());
game.r#continue().sleep();
game.next().title().seer();
game.mark(
game.character_by_player_id(insomniac_player_id)
.character_id(),
);
game.r#continue().seer().village();
game.next().title().arcanist();
game.mark(game.character_by_player_id(seer_player_id).character_id());
game.mark(
game.character_by_player_id(insomniac_player_id)
.character_id(),
);
assert_eq!(game.r#continue().arcanist(), true);
game.next().title().insomniac();
assert_eq!(
game.r#continue().insomniac(),
Visits::new(Box::new([
game.character_by_player_id(seer_player_id).identity(),
game.character_by_player_id(arcanist_player_id).identity()
]))
);
}

View File

@ -45,12 +45,15 @@ fn recruits_decrement() {
assert_eq!( assert_eq!(
game.next(), game.next(),
ActionPrompt::MasonsWake { ActionPrompt::MasonsWake {
character_id: game leader: game
.character_by_player_id(mason_leader_player_id) .character_by_player_id(mason_leader_player_id)
.character_id(),
masons: Box::new([
game.character_by_player_id(mason_leader_player_id)
.identity(), .identity(),
masons: Box::new([game game.character_by_player_id(recruited.player_id())
.character_by_player_id(recruited.player_id()) .identity(),
.identity()]) ])
} }
); );
game.r#continue().sleep(); game.r#continue().sleep();
@ -97,6 +100,19 @@ fn dies_recruiting_wolf() {
game.mark(game.character_by_player_id(wolf_player_id).character_id()); game.mark(game.character_by_player_id(wolf_player_id).character_id());
game.r#continue().sleep(); game.r#continue().sleep();
assert_eq!(
game.next(),
ActionPrompt::MasonsWake {
leader: game
.character_by_player_id(mason_leader_player_id)
.character_id(),
masons: Box::new([game
.character_by_player_id(mason_leader_player_id)
.identity()])
}
);
game.r#continue().sleep();
game.next_expect_day(); game.next_expect_day();
assert_eq!( assert_eq!(
@ -111,3 +127,105 @@ fn dies_recruiting_wolf() {
} }
// todo: masons wake even if leader dead // todo: masons wake even if leader dead
#[test]
fn masons_wake_even_if_leader_died() {
let players = gen_players(1..10);
let mason_leader_player_id = players[0].player_id;
let wolf_player_id = players[1].player_id;
let black_knight_player_id = players[2].player_id;
let mut settings = GameSettings::empty();
settings.add_and_assign(
SetupRole::MasonLeader {
recruits_available: NonZeroU8::new(3).unwrap(),
},
mason_leader_player_id,
);
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
settings.add_and_assign(SetupRole::BlackKnight, black_knight_player_id);
settings.fill_remaining_slots_with_villagers(9);
let mut game = Game::new(&players, settings).unwrap();
let blk_knight_cid = game
.character_by_player_id(black_knight_player_id)
.character_id();
game.r#continue().r#continue();
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
game.r#continue().sleep();
game.next_expect_day();
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
game.mark(blk_knight_cid);
game.r#continue().sleep();
assert_eq!(game.next().title(), ActionPromptTitle::MasonLeaderRecruit);
game.mark(blk_knight_cid);
game.r#continue().r#continue();
assert_eq!(
game.next(),
ActionPrompt::MasonsWake {
leader: game
.character_by_player_id(mason_leader_player_id)
.character_id(),
masons: Box::new([
game.character_by_player_id(mason_leader_player_id)
.identity(),
game.character_by_player_id(black_knight_player_id)
.identity()
])
}
);
game.r#continue().sleep();
game.next_expect_day();
game.execute().title().wolf_pack_kill();
game.mark(blk_knight_cid);
game.r#continue().sleep();
let second_recruit = game.living_villager();
assert_eq!(game.next().title(), ActionPromptTitle::MasonLeaderRecruit);
game.mark(second_recruit.character_id());
game.r#continue().r#continue();
assert_eq!(
game.next(),
ActionPrompt::MasonsWake {
leader: game
.character_by_player_id(mason_leader_player_id)
.character_id(),
masons: Box::new([
game.character_by_player_id(black_knight_player_id)
.identity(),
game.character_by_player_id(mason_leader_player_id)
.identity(),
second_recruit.identity()
])
}
);
game.r#continue().sleep();
game.next_expect_day();
game.mark_for_execution(
game.character_by_player_id(mason_leader_player_id)
.character_id(),
);
game.execute().title().wolf_pack_kill();
game.mark(blk_knight_cid);
game.r#continue().sleep();
assert_eq!(
game.next(),
ActionPrompt::MasonsWake {
leader: game
.character_by_player_id(mason_leader_player_id)
.character_id(),
masons: Box::new([
game.character_by_player_id(black_knight_player_id)
.identity(),
second_recruit.identity()
])
}
);
}

View File

@ -3,6 +3,7 @@ mod black_knight;
mod diseased; mod diseased;
mod elder; mod elder;
mod empath; mod empath;
mod insomniac;
mod lone_wolf; mod lone_wolf;
mod mason; mod mason;
mod mortician; mod mortician;

View File

@ -1,4 +1,4 @@
use core::num::NonZeroU8; use core::{num::NonZeroU8, ops::Deref};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use werewolves_macros::{ChecksAs, Titles}; use werewolves_macros::{ChecksAs, Titles};
@ -27,6 +27,7 @@ pub enum ActionType {
Other, Other,
MasonRecruit, MasonRecruit,
MasonsWake, MasonsWake,
Insomniac,
Beholder, Beholder,
RoleChange, RoleChange,
} }
@ -136,7 +137,7 @@ pub enum ActionPrompt {
}, },
#[checks(ActionType::MasonsWake)] #[checks(ActionType::MasonsWake)]
MasonsWake { MasonsWake {
character_id: CharacterIdentity, leader: CharacterId,
masons: Box<[CharacterIdentity]>, masons: Box<[CharacterIdentity]>,
}, },
#[checks(ActionType::MasonRecruit)] #[checks(ActionType::MasonRecruit)]
@ -190,12 +191,15 @@ pub enum ActionPrompt {
living_players: Box<[CharacterIdentity]>, living_players: Box<[CharacterIdentity]>,
marked: Option<CharacterId>, marked: Option<CharacterId>,
}, },
#[checks(ActionType::Insomniac)]
Insomniac { character_id: CharacterIdentity },
} }
impl ActionPrompt { impl ActionPrompt {
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool { pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
match self { match self {
ActionPrompt::Seer { character_id, .. } ActionPrompt::Insomniac { character_id, .. }
| ActionPrompt::Seer { character_id, .. }
| ActionPrompt::Arcanist { character_id, .. } | ActionPrompt::Arcanist { character_id, .. }
| ActionPrompt::Gravedigger { character_id, .. } | ActionPrompt::Gravedigger { character_id, .. }
| ActionPrompt::Adjudicator { character_id, .. } | ActionPrompt::Adjudicator { character_id, .. }
@ -227,7 +231,8 @@ impl ActionPrompt {
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> { pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
let mut prompt = self.clone(); let mut prompt = self.clone();
match &mut prompt { match &mut prompt {
ActionPrompt::MasonsWake { .. } ActionPrompt::Insomniac { .. }
| ActionPrompt::MasonsWake { .. }
| ActionPrompt::ElderReveal { .. } | ActionPrompt::ElderReveal { .. }
| ActionPrompt::WolvesIntro { .. } | ActionPrompt::WolvesIntro { .. }
| ActionPrompt::RoleChange { .. } | ActionPrompt::RoleChange { .. }
@ -435,7 +440,25 @@ pub enum ActionResult {
Arcanist { same: bool }, Arcanist { same: bool },
GraveDigger(Option<RoleTitle>), GraveDigger(Option<RoleTitle>),
Mortician(DiedToTitle), Mortician(DiedToTitle),
Insomniac(Visits),
Empath { scapegoat: bool }, Empath { scapegoat: bool },
GoBackToSleep, GoBackToSleep,
Continue, Continue,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Visits(Box<[CharacterIdentity]>);
impl Visits {
pub(crate) const fn new(visits: Box<[CharacterIdentity]>) -> Self {
Self(visits)
}
}
impl Deref for Visits {
type Target = [CharacterIdentity];
fn deref(&self) -> &Self::Target {
&self.0
}
}

View File

@ -44,14 +44,12 @@ pub enum Role {
Beholder, Beholder,
#[checks(Alignment::Village)] #[checks(Alignment::Village)]
#[checks("powerful")] #[checks("powerful")]
#[checks("is_mentor")]
MasonLeader { MasonLeader {
recruits_available: u8, recruits_available: u8,
recruits: Box<[CharacterId]>, recruits: Box<[CharacterId]>,
}, },
#[checks(Alignment::Village)] #[checks(Alignment::Village)]
#[checks("powerful")] #[checks("powerful")]
#[checks("is_mentor")]
Empath { cursed: bool }, Empath { cursed: bool },
#[checks(Alignment::Village)] #[checks(Alignment::Village)]
#[checks("powerful")] #[checks("powerful")]
@ -115,6 +113,9 @@ pub enum Role {
woken_for_reveal: bool, woken_for_reveal: bool,
lost_protection_night: Option<NonZeroU8>, lost_protection_night: Option<NonZeroU8>,
}, },
#[checks(Alignment::Village)]
#[checks("powerful")]
Insomniac,
#[checks(Alignment::Wolves)] #[checks(Alignment::Wolves)]
#[checks("killer")] #[checks("killer")]
@ -147,14 +148,15 @@ impl Role {
/// [RoleTitle] as shown to the player on role assignment /// [RoleTitle] as shown to the player on role assignment
pub const fn initial_shown_role(&self) -> RoleTitle { pub const fn initial_shown_role(&self) -> RoleTitle {
match self { match self {
Role::Apprentice(_) | Role::Elder { .. } => RoleTitle::Villager, Role::Apprentice(_) | Role::Elder { .. } | Role::Insomniac => RoleTitle::Villager,
_ => self.title(), _ => self.title(),
} }
} }
pub const fn wakes_night_zero(&self) -> bool { pub const fn wakes_night_zero(&self) -> bool {
match self { match self {
Role::PowerSeer Role::Insomniac
| Role::PowerSeer
| Role::Beholder | Role::Beholder
| Role::Adjudicator | Role::Adjudicator
| Role::DireWolf | Role::DireWolf
@ -211,7 +213,8 @@ impl Role {
.map(|execs| execs.iter().any(|e| e.is_wolf())) .map(|execs| execs.iter().any(|e| e.is_wolf()))
.unwrap_or_default(), .unwrap_or_default(),
Role::PowerSeer Role::Insomniac
| Role::PowerSeer
| Role::Mortician | Role::Mortician
| Role::Beholder | Role::Beholder
| Role::MasonLeader { .. } | Role::MasonLeader { .. }

View File

@ -228,7 +228,24 @@ impl GameRunner {
} }
pub async fn next(&mut self) -> Option<GameOver> { pub async fn next(&mut self) -> Option<GameOver> {
let msg = self.comms.host().recv().await.expect("host channel closed"); let msg = match self.comms.message().await {
Ok(Message::ConnectedList(_)) => return None,
Ok(Message::Client(IdentifiedClientMessage {
identity: Identification { player_id, .. },
..
})) => {
if let Some(send) = self.joined_players.get_sender(player_id).await {
send.send(ServerMessage::GameInProgress).log_debug();
}
return None;
}
Ok(Message::Host(msg)) => msg,
Err(err) => {
log::error!("game next message: {err}");
return None;
}
};
match self.host_message(msg) { match self.host_message(msg) {
Ok(resp) => { Ok(resp) => {
self.comms.host().send(resp).log_warn(); self.comms.host().send(resp).log_warn();

View File

@ -67,6 +67,19 @@ body {
background: black; background: black;
} }
.big-screen {
align-content: center;
align-items: center;
justify-content: center;
height: 100vh;
width: 100%;
position: fixed;
left: 0;
top: 0;
margin: 0;
font-size: 2rem;
}
$link_color: #432054; $link_color: #432054;
$link_hover_color: hsl(280, 55%, 61%); $link_hover_color: hsl(280, 55%, 61%);
$link_bg_color: #fff6d5; $link_bg_color: #fff6d5;
@ -936,6 +949,11 @@ input {
} }
&.dead { &.dead {
filter: saturate(0%);
border: 1px solid rgba(255, 255, 255, 0.05);
}
&.recent-death {
$bg: rgba(128, 128, 128, 0.5); $bg: rgba(128, 128, 128, 0.5);
background-color: $bg; background-color: $bg;
border: 1px solid color.change($bg, $alpha: 1.0); border: 1px solid color.change($bg, $alpha: 1.0);
@ -955,14 +973,6 @@ input {
} }
} }
.big-screen {
align-content: center;
align-items: center;
justify-content: center;
height: 100vh;
position: fixed;
}
.align-start { .align-start {
align-self: flex-start; align-self: flex-start;
} }
@ -994,6 +1004,18 @@ input {
color: white; color: white;
cursor: pointer; cursor: pointer;
} }
&>.submenu {
min-width: 5cm;
.assign-list {
min-width: 5cm;
& .submenu button {
width: inherit;
}
}
}
} }
.add-role { .add-role {
@ -1105,6 +1127,7 @@ input {
position: fixed; position: fixed;
left: 10%; left: 10%;
top: 10%; top: 10%;
font-size: 1rem;
.setup { .setup {
display: flex; display: flex;
@ -1122,6 +1145,10 @@ input {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
&.final {
margin-top: 1cm;
}
& .title { & .title {
margin-bottom: 10px; margin-bottom: 10px;
} }
@ -1162,10 +1189,6 @@ input {
filter: contrast(120%) brightness(120%); filter: contrast(120%) brightness(120%);
} }
} }
.inactive {
filter: grayscale(100%) brightness(30%);
}
} }
.role { .role {
@ -1183,10 +1206,15 @@ input {
} }
} }
.inactive {
filter: grayscale(100%) brightness(30%);
}
.qrcode { .qrcode {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex-wrap: nowrap; flex-wrap: nowrap;
z-index: 100;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@ -1196,7 +1224,7 @@ input {
gap: 1cm; gap: 1cm;
img { img {
height: 100%; height: 70%;
width: 100%; width: 100%;
} }
@ -1205,5 +1233,74 @@ input {
// width: 100%; // width: 100%;
border: 1px solid $village_border; border: 1px solid $village_border;
background-color: color.change($village_color, $alpha: 0.3); background-color: color.change($village_color, $alpha: 0.3);
text-align: center;
// width: fit-content;
&>* {
margin-top: 0.5cm;
margin-bottom: 0.5cm;
// padding: 0;
}
}
}
.result {
display: flex;
justify-content: center;
align-content: center;
align-items: center;
flex-direction: column;
width: 100%;
height: 100%;
}
.result-list {
display: flex;
flex-direction: row;
flex-wrap: wrap;
width: 100%;
justify-content: space-evenly;
row-gap: 0.5cm;
.identity {
padding: 1cm;
border: 1px solid white;
font-size: 2em;
text-align: center;
}
}
.check-icon {
width: 40vw;
height: 40vh;
// margin-top: 10%;
align-self: center;
}
.insomniac {
display: flex;
width: 100%;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
&.prompt {
font-size: 2em;
}
}
.arcanist-result {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
width: 100%;
gap: 10px;
img {
// flex-shrink: 1 !important;
width: max-content !important;
height: max-content !important;
} }
} }

View File

@ -550,10 +550,8 @@ impl Component for Host {
} }
HostEvent::SetBigScreenState(state) => { HostEvent::SetBigScreenState(state) => {
self.big_screen = state; self.big_screen = state;
if self.big_screen if self.big_screen {
&& let Ok(Some(root)) = gloo::utils::document().query_selector(".content") gloo::utils::document_element().set_class_name("big-screen")
{
root.set_class_name("content big-screen")
} }
if state { if state {

View File

@ -88,6 +88,15 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
</div> </div>
}; };
} }
ActionPrompt::Insomniac { character_id } => {
return html! {
<div class="insomniac prompt">
{identity_html(props, Some(character_id))}
<h2>{"you are the insomniac"}</h2>
{cont}
</div>
};
}
ActionPrompt::RoleChange { ActionPrompt::RoleChange {
character_id, character_id,
new_role, new_role,
@ -102,22 +111,19 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
}; };
} }
ActionPrompt::MasonsWake { ActionPrompt::MasonsWake { leader, masons } => {
character_id,
masons,
} => {
let masons = masons let masons = masons
.into_iter() .into_iter()
.map(|c| { .map(|c| {
let leader = (c.character_id == *leader).then_some("leader");
let ident: PublicIdentity = c.into(); let ident: PublicIdentity = c.into();
html! { html! {
<Identity ident={ident}/> <Identity class={classes!(leader)} ident={ident}/>
} }
}) })
.collect::<Html>(); .collect::<Html>();
return html! { return html! {
<div class="masons"> <div class="masons">
{identity_html(props, Some(character_id))}
<h2>{"these are the masons"}</h2> <h2>{"these are the masons"}</h2>
<div class="mason-list"> <div class="mason-list">
{masons} {masons}

View File

@ -11,7 +11,7 @@ use werewolves_proto::{
}; };
use yew::prelude::*; use yew::prelude::*;
use crate::components::{Button, CoverOfDarkness, Identity}; use crate::components::{Button, CoverOfDarkness, Icon, IconSource, Identity};
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct ActionResultProps { pub struct ActionResultProps {
@ -56,12 +56,16 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
} }
} }
ActionResult::Adjudicator { killer } => { ActionResult::Adjudicator { killer } => {
let inactive = killer.not().then_some("inactive"); let text = if *killer {
let text = if *killer { "killer" } else { "not a killer" }; "is a killer"
} else {
"is NOT a killer"
};
html! { html! {
<> <>
<img src="/img/killer.svg" class={classes!(inactive)}/> <h1>{"your target..."}</h1>
<h3>{text}</h3> <Icon source={IconSource::Killer} inactive={!*killer}/>
<h2>{text}</h2>
</> </>
} }
} }
@ -77,6 +81,25 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
ActionResult::Empath { scapegoat: false } => html! { ActionResult::Empath { scapegoat: false } => html! {
<h2>{"not the scapegoat"}</h2> <h2>{"not the scapegoat"}</h2>
}, },
ActionResult::Insomniac(visits) => {
let visits = visits
.iter()
.map(|v| {
let ident: PublicIdentity = v.clone().into();
html! {
<Identity ident={ident}/>
}
})
.collect::<Html>();
html! {
<>
<h1>{"tonight you were visited by..."}</h1>
<div class="result-list">
{visits}
</div>
</>
}
}
ActionResult::RoleBlocked => { ActionResult::RoleBlocked => {
html! { html! {
<h2>{"you were role blocked"}</h2> <h2>{"you were role blocked"}</h2>
@ -85,17 +108,49 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
ActionResult::Seer(alignment) => html! { ActionResult::Seer(alignment) => html! {
<> <>
<h2>{"the alignment was"}</h2> <h2>{"the alignment was"}</h2>
<p>{match alignment { {match alignment {
Alignment::Village => "village", Alignment::Village => html!{
Alignment::Wolves => "wolfpack", <>
}}</p> <Icon source={IconSource::Village}/>
<h3>{"village"}</h3>
</>
},
Alignment::Wolves => html!{
<>
<Icon source={IconSource::Wolves}/>
<h3>{"wolves"}</h3>
</>
},
}}
</> </>
}, },
ActionResult::Arcanist { same } => { ActionResult::Arcanist { same } => {
let outcome = if *same { "same" } else { "different" }; let outcome = if *same {
html! { html! {
<> <>
<h2>{"the alignments are:"}</h2> <div class="arcanist-result">
<Icon source={IconSource::Village}/>
<Icon source={IconSource::Village}/>
<Icon source={IconSource::Wolves}/>
<Icon source={IconSource::Wolves}/>
</div>
<h2>{"the same"}</h2>
</>
}
} else {
html! {
<>
<div class="arcanist-result">
<Icon source={IconSource::Village}/>
<Icon source={IconSource::Wolves}/>
</div>
<h2>{"different"}</h2>
</>
}
};
html! {
<>
<h1>{"the alignments are:"}</h1>
<p>{outcome}</p> <p>{outcome}</p>
</> </>
} }

View File

@ -2,6 +2,7 @@ use core::{num::NonZeroU8, ops::Not};
use werewolves_proto::{ use werewolves_proto::{
character::CharacterId, character::CharacterId,
game::DateTime,
message::{CharacterState, PublicIdentity}, message::{CharacterState, PublicIdentity},
}; };
use yew::prelude::*; use yew::prelude::*;
@ -35,10 +36,25 @@ pub fn DaytimePlayerList(
let chars = characters let chars = characters
.iter() .iter()
.map(|c| { .map(|c| {
let mark_state = match c.died_to.as_ref() {
None => marked
.contains(&c.identity.character_id)
.then_some(MarkState::Marked),
Some(died_to) => match died_to.date_time() {
DateTime::Day { .. } => Some(MarkState::Dead),
DateTime::Night { number } => {
if number == day.get() - 1 {
Some(MarkState::DiedLastNight)
} else {
Some(MarkState::Dead)
}
}
},
};
html! { html! {
<DaytimePlayer <DaytimePlayer
character={c.clone()} character={c.clone()}
on_the_block={marked.contains(&c.identity.character_id)} mark_state={mark_state}
on_select={on_select.clone()} on_select={on_select.clone()}
/> />
} }
@ -66,11 +82,28 @@ pub fn DaytimePlayerList(
</div> </div>
} }
} }
#[derive(Debug, Clone, PartialEq)]
pub enum MarkState {
Marked,
DiedLastNight,
Dead,
}
impl MarkState {
pub const fn class(&self) -> &'static str {
match self {
MarkState::Marked => "marked",
MarkState::DiedLastNight => "recent-death",
MarkState::Dead => "dead",
}
}
}
#[derive(Debug, Clone, PartialEq, Properties)] #[derive(Debug, Clone, PartialEq, Properties)]
pub struct DaytimePlayerProps { pub struct DaytimePlayerProps {
pub character: CharacterState, pub character: CharacterState,
pub on_the_block: bool, #[prop_or_default]
pub mark_state: Option<MarkState>,
pub on_select: Option<Callback<CharacterId>>, pub on_select: Option<Callback<CharacterId>>,
} }
@ -78,7 +111,7 @@ pub struct DaytimePlayerProps {
pub fn DaytimePlayer( pub fn DaytimePlayer(
DaytimePlayerProps { DaytimePlayerProps {
on_select, on_select,
on_the_block, mark_state,
character: character:
CharacterState { CharacterState {
player_id: _, player_id: _,
@ -88,8 +121,7 @@ pub fn DaytimePlayer(
}, },
}: &DaytimePlayerProps, }: &DaytimePlayerProps,
) -> Html { ) -> Html {
let dead = died_to.is_some().then_some("dead"); let class = mark_state.as_ref().map(|s| s.class());
let marked = on_the_block.then_some("marked");
let character_id = identity.character_id; let character_id = identity.character_id;
let on_click: Callback<_> = died_to let on_click: Callback<_> = died_to
.is_none() .is_none()
@ -102,7 +134,7 @@ pub fn DaytimePlayer(
.unwrap_or_default(); .unwrap_or_default();
let identity: PublicIdentity = identity.into(); let identity: PublicIdentity = identity.into();
html! { html! {
<Button on_click={on_click} classes={classes!(marked, dead, "character")}> <Button on_click={on_click} classes={classes!(class, "character")}>
<Identity ident={identity}/> <Identity ident={identity}/>
</Button> </Button>
} }

View File

@ -67,7 +67,7 @@ pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
<div class="setup"> <div class="setup">
{categories} {categories}
</div> </div>
<div class="category village"> <div class="category village final">
<span class="count">{power_roles_count}</span> <span class="count">{power_roles_count}</span>
<span class="title">{"Power roles from..."}</span> <span class="title">{"Power roles from..."}</span>
</div> </div>

View File

@ -0,0 +1,67 @@
use yew::prelude::*;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum IconSource {
Village,
Wolves,
Killer,
Powerful,
}
impl IconSource {
pub const fn source(&self) -> &'static str {
match self {
IconSource::Village => "/img/village.svg",
IconSource::Wolves => "/img/wolf.svg",
IconSource::Killer => "/img/killer.svg",
IconSource::Powerful => "/img/powerful.svg",
}
}
pub const fn class(&self) -> Option<&'static str> {
match self {
IconSource::Village | IconSource::Wolves => None,
IconSource::Killer => Some("killer"),
IconSource::Powerful => Some("powerful"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum IconType {
SetupList,
#[default]
RoleCheck,
}
impl IconType {
pub const fn class(&self) -> &'static str {
match self {
IconType::SetupList => "icon",
IconType::RoleCheck => "check-icon",
}
}
}
#[derive(Debug, Clone, PartialEq, Properties)]
pub struct IconProps {
pub source: IconSource,
#[prop_or_default]
pub inactive: bool,
#[prop_or_default]
pub icon_type: IconType,
}
#[function_component]
pub fn Icon(
IconProps {
source,
inactive,
icon_type,
}: &IconProps,
) -> Html {
html! {
<img
src={source.source()}
class={classes!(source.class(), icon_type.class(), inactive.then_some("inactive"))}
/>
}
}

View File

@ -5,7 +5,7 @@ use yew::prelude::*;
pub struct IdentityProps { pub struct IdentityProps {
pub ident: PublicIdentity, pub ident: PublicIdentity,
#[prop_or_default] #[prop_or_default]
pub class: Option<String>, pub class: Classes,
} }
#[function_component] #[function_component]
@ -29,7 +29,7 @@ pub fn Identity(props: &IdentityProps) -> Html {
.map(|n| n.to_string()) .map(|n| n.to_string())
.unwrap_or_else(|| String::from("???")); .unwrap_or_else(|| String::from("???"));
html! { html! {
<div class={classes!("identity", class)}> <div class={classes!("identity", class.clone())}>
<p class={classes!("number", not_set)}><b>{number}</b></p> <p class={classes!("number", not_set)}><b>{number}</b></p>
<p>{name}</p> <p>{name}</p>
{pronouns} {pronouns}

View File

@ -47,6 +47,8 @@ fn main() {
let error_callback = let error_callback =
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err)); Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
gloo::utils::document().set_title("werewolves");
if path.starts_with("/host") { if path.starts_with("/host") {
let host = yew::Renderer::<Host>::with_root(app_element).render(); let host = yew::Renderer::<Host>::with_root(app_element).render();
host.send_message(HostEvent::SetErrorCallback(error_callback)); host.send_message(HostEvent::SetErrorCallback(error_callback));