diff --git a/werewolves-proto/src/error.rs b/werewolves-proto/src/error.rs index c6280d1..4fe3983 100644 --- a/werewolves-proto/src/error.rs +++ b/werewolves-proto/src/error.rs @@ -73,6 +73,8 @@ pub enum GameError { NightNeedsNext, #[error("night zero actions can only be obtained on night zero")] NotNightZero, + #[error("this action cannot happen on night zero")] + CannotHappenOnNightZero, #[error("wolves intro in progress")] WolvesIntroInProgress, #[error("a game is still ongoing")] @@ -99,4 +101,8 @@ pub enum GameError { MilitiaSpent, #[error("this role doesn't mark anyone")] RoleDoesntMark, + #[error("cannot shapeshift on a non-shapeshifter prompt")] + ShapeshiftingIsForShapeshifters, + #[error("must select a target")] + MustSelectTarget, } diff --git a/werewolves-proto/src/game/kill.rs b/werewolves-proto/src/game/kill.rs index d1223f8..06399db 100644 --- a/werewolves-proto/src/game/kill.rs +++ b/werewolves-proto/src/game/kill.rs @@ -19,7 +19,10 @@ use crate::{ character::CharacterId, diedto::DiedTo, error::GameError, - game::{Village, night::changes::ChangesLookup}, + game::{ + Village, + night::changes::{ChangesLookup, NightChange}, + }, player::Protection, }; @@ -36,7 +39,11 @@ pub enum KillOutcome { } impl KillOutcome { - pub fn apply_to_village(self, village: &mut Village) -> Result<()> { + pub fn apply_to_village( + self, + village: &mut Village, + recorded_changes: Option<&mut Vec>, + ) -> Result<()> { match self { KillOutcome::Single(character_id, died_to) => { village @@ -64,15 +71,22 @@ impl KillOutcome { // check if guardian exists before we mutably borrow killer, which would // prevent us from borrowing village to check after. village.character_by_id(guardian)?; + let guardian_kill = DiedTo::GuardianProtecting { + night, + source: guardian, + protecting: original_target, + protecting_from: original_killer, + protecting_from_cause: Box::new(original_kill.clone()), + }; + if let Some(recorded_changes) = recorded_changes { + recorded_changes.push(NightChange::Kill { + target: original_killer, + died_to: guardian_kill.clone(), + }); + } village .character_by_id_mut(original_killer)? - .kill(DiedTo::GuardianProtecting { - night, - source: guardian, - protecting: original_target, - protecting_from: original_killer, - protecting_from_cause: Box::new(original_kill.clone()), - }); + .kill(guardian_kill); village.character_by_id_mut(guardian)?.kill(original_kill); Ok(()) } diff --git a/werewolves-proto/src/game/mod.rs b/werewolves-proto/src/game/mod.rs index f59b419..ea1f3c7 100644 --- a/werewolves-proto/src/game/mod.rs +++ b/werewolves-proto/src/game/mod.rs @@ -169,12 +169,13 @@ impl Game { Ok(_) => self.process(HostGameMessage::GetState), Err(GameError::NightOver) => { let changes = night.collect_changes()?; - let village = night.village().with_night_changes(&changes)?; + let (village, recorded_changes) = + night.village().with_night_changes(&changes)?; self.history.add( night.village().time(), GameActions::NightDetails(NightDetails::new( &night.used_actions(), - changes, + recorded_changes, )), )?; self.state = GameState::Day { diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index f20c7c1..68c61af 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -706,6 +706,11 @@ impl Night { } pub fn received_response(&mut self, resp: ActionResponse) -> Result { + if let ActionResponse::ContinueToResult = &resp + && let Some(result) = self.current_result() + { + return Ok(ServerAction::Result(result.clone())); + } match self.received_response_with_role_blocks(resp)? { BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state { NightState::Active { diff --git a/werewolves-proto/src/game/night/changes.rs b/werewolves-proto/src/game/night/changes.rs index c1752a7..c90d154 100644 --- a/werewolves-proto/src/game/night/changes.rs +++ b/werewolves-proto/src/game/night/changes.rs @@ -14,6 +14,7 @@ // along with this program. If not, see . use core::ops::Not; +use super::Result; use serde::{Deserialize, Serialize}; use werewolves_macros::Extract; @@ -21,6 +22,7 @@ use crate::{ aura::Aura, character::CharacterId, diedto::DiedTo, + game::{Village, kill}, player::Protection, role::{RoleBlock, RoleTitle}, }; @@ -71,6 +73,29 @@ pub enum NightChange { }, } +impl NightChange { + pub const fn target(&self) -> Option { + match self { + NightChange::HunterTarget { target, .. } + | NightChange::Kill { target, .. } + | NightChange::RoleBlock { target, .. } + | NightChange::Shapeshift { into: target, .. } + | NightChange::Protection { target, .. } + | NightChange::MasonRecruit { + recruiting: target, .. + } + | NightChange::EmpathFoundScapegoat { + scapegoat: target, .. + } + | NightChange::ApplyAura { target, .. } => Some(*target), + + NightChange::ElderReveal { .. } + | NightChange::RoleChange(..) + | NightChange::LostAura { .. } => None, + } + } +} + pub struct ChangesLookup<'a>(&'a [NightChange], Vec); impl<'a> ChangesLookup<'a> { @@ -78,6 +103,30 @@ impl<'a> ChangesLookup<'a> { Self(changes, Vec::new()) } + pub fn collect_remaining(&self) -> Box<[NightChange]> { + self.0 + .iter() + .enumerate() + .filter_map(|(idx, c)| self.1.contains(&idx).not().then_some(c)) + .cloned() + .collect() + } + + pub fn died_to( + &mut self, + character_id: CharacterId, + night: u8, + village: &Village, + ) -> Result> { + if let Some(died_to) = self.killed(character_id) + && kill::resolve_kill(self, character_id, died_to, night, village)?.is_some() + { + Ok(Some(died_to.clone())) + } else { + Ok(None) + } + } + pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> { self.0.iter().enumerate().find_map(|(idx, c)| { self.1 diff --git a/werewolves-proto/src/game/night/next.rs b/werewolves-proto/src/game/night/next.rs index 0af5d2b..107e9ea 100644 --- a/werewolves-proto/src/game/night/next.rs +++ b/werewolves-proto/src/game/night/next.rs @@ -16,16 +16,19 @@ use core::num::NonZeroU8; use crate::{ + character::CharacterId, diedto::DiedTo, error::GameError, game::night::{CurrentResult, Night, NightState, changes::NightChange}, message::night::{ActionPrompt, ActionResult}, + role::RoleBlock, }; use super::Result; impl Night { #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Result<()> { + self.retroactive_role_blocks()?; self.next_state_process_maple_starving()?; match &self.night_state { @@ -145,4 +148,44 @@ impl Night { Ok(()) } + + fn retroactive_role_blocks(&mut self) -> Result<()> { + let blocks = match &self.night_state { + NightState::Active { + current_changes, .. + } => current_changes + .iter() + .filter_map(|c| match c { + NightChange::RoleBlock { + target, block_type, .. + } => Some((*target, *block_type)), + _ => None, + }) + .collect::>(), + NightState::Complete => return Err(GameError::NightOver), + }; + for (target, block_type) in blocks { + match block_type { + RoleBlock::Direwolf => self.apply_direwolf_block_retroactively(target), + } + } + + Ok(()) + } + + fn apply_direwolf_block_retroactively(&mut self, target: CharacterId) { + self.used_actions + .iter_mut() + .filter_map(|(prompt, res, changes)| match prompt.marked() { + Some((marked, None)) => (marked == target).then_some((res, changes)), + Some((marked1, Some(marked2))) => { + (marked1 == target || marked2 == target).then_some((res, changes)) + } + None => None, + }) + .for_each(|(result, changes)| { + changes.clear(); + *result = ActionResult::RoleBlocked; + }); + } } diff --git a/werewolves-proto/src/game/night/process.rs b/werewolves-proto/src/game/night/process.rs index b80849e..a3b7e8d 100644 --- a/werewolves-proto/src/game/night/process.rs +++ b/werewolves-proto/src/game/night/process.rs @@ -80,7 +80,7 @@ impl Night { .ok_or(GameError::InvalidTarget)?, }), })), - _ => Err(GameError::InvalidMessageForGameState), + _ => Err(GameError::ShapeshiftingIsForShapeshifters), }; } ActionResponse::Continue => { @@ -107,6 +107,7 @@ impl Night { })); } } + ActionResponse::ContinueToResult => return self.process(ActionResponse::Continue), }; match current_prompt { @@ -228,7 +229,7 @@ impl Night { died_to: DiedTo::Militia { killer: character_id.character_id, night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, + .ok_or(GameError::CannotHappenOnNightZero)?, }, }), })), @@ -318,7 +319,7 @@ impl Night { .ok_or(GameError::NoWolves)? .character_id(), night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, + .ok_or(GameError::CannotHappenOnNightZero)?, }, }), })), @@ -341,7 +342,7 @@ impl Night { }) .ok_or(GameError::InvalidTarget)?, }), - _ => return Err(GameError::InvalidMessageForGameState), + _ => return Err(GameError::ShapeshiftingIsForShapeshifters), }, })) } @@ -356,7 +357,7 @@ impl Night { died_to: DiedTo::AlphaWolf { killer: character_id.character_id, night: NonZeroU8::new(self.night) - .ok_or(GameError::InvalidMessageForGameState)?, + .ok_or(GameError::CannotHappenOnNightZero)?, }, }), })), @@ -537,7 +538,7 @@ impl Night { | ActionPrompt::Guardian { marked: None, .. } | ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::DireWolf { marked: None, .. } - | ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState), + | ActionPrompt::Seer { marked: None, .. } => Err(GameError::MustSelectTarget), } } diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index eae920d..04e3e75 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -185,7 +185,14 @@ impl SetupRoleTitle { } AuraTitle::Insane => { matches!(self.category(), Category::Intel) - && !matches!(self, Self::MasonLeader | Self::Empath) + && !matches!( + self, + Self::MasonLeader + | Self::Empath + | Self::Insomniac + | Self::Mortician + | Self::Gravedigger + ) } AuraTitle::Bloodlet => false, } diff --git a/werewolves-proto/src/game/story.rs b/werewolves-proto/src/game/story.rs index 646508c..46e4880 100644 --- a/werewolves-proto/src/game/story.rs +++ b/werewolves-proto/src/game/story.rs @@ -455,7 +455,7 @@ impl GameStory { village = match actions { GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?, GameActions::NightDetails(night_details) => { - village.with_night_changes(&night_details.changes)? + village.with_night_changes(&night_details.changes)?.0 } }; } @@ -468,7 +468,7 @@ impl GameStory { village = match actions { GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?, GameActions::NightDetails(night_details) => { - village.with_night_changes(&night_details.changes)? + village.with_night_changes(&night_details.changes)?.0 } }; if time == at_time { diff --git a/werewolves-proto/src/game/village/apply.rs b/werewolves-proto/src/game/village/apply.rs index b01d0e9..2f888a9 100644 --- a/werewolves-proto/src/game/village/apply.rs +++ b/werewolves-proto/src/game/village/apply.rs @@ -12,14 +12,15 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use core::num::NonZeroU8; +use core::{num::NonZeroU8, ops::Not}; use crate::{ aura::{Aura, AuraTitle}, diedto::DiedTo, error::GameError, game::{ - GameTime, Village, kill, + GameTime, Village, + kill::{self, KillOutcome}, night::changes::{ChangesLookup, NightChange}, story::DayDetail, }, @@ -43,7 +44,10 @@ impl Village { Ok(new_village) } - pub fn with_night_changes(&self, all_changes: &[NightChange]) -> Result { + pub fn with_night_changes( + &self, + all_changes: &[NightChange], + ) -> Result<(Self, Box<[NightChange]>)> { let night = match self.time { GameTime::Day { .. } => return Err(GameError::NotNight), GameTime::Night { number } => number, @@ -52,6 +56,9 @@ impl Village { let mut new_village = self.clone(); + // recorded changes: changes sans failed kills, actions failed due to blocks, etc + let mut recorded_changes = all_changes.to_vec(); + // dispose of the current drunk token for every drunk in the village new_village .characters_mut() @@ -82,29 +89,48 @@ impl Village { NightChange::HunterTarget { source, target } => { let hunter_character = new_village.character_by_id_mut(*source).unwrap(); hunter_character.hunter_mut()?.replace(*target); - if changes.killed(*source).is_some() - && changes.protected(source).is_none() - && changes.protected(target).is_none() - { - new_village - .character_by_id_mut(*target) - .unwrap() - .kill(DiedTo::Hunter { + if changes + .died_to(hunter_character.character_id(), night, self)? + .is_some() + && let Some(kill) = kill::resolve_kill( + &mut changes, + *target, + &DiedTo::Hunter { killer: *source, - night: NonZeroU8::new(night).unwrap(), - }) + night: NonZeroU8::new(night) + .ok_or(GameError::CannotHappenOnNightZero)?, + }, + night, + &new_village, + )? + { + kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?; } } NightChange::Kill { target, died_to } => { if let Some(kill) = kill::resolve_kill(&mut changes, *target, died_to, night, self)? { - kill.apply_to_village(&mut new_village)?; + if let KillOutcome::Guarding { + guardian, + original_kill, + .. + } = &kill + { + recorded_changes.retain(|c| c != change); + recorded_changes.push(NightChange::Kill { + target: *guardian, + died_to: original_kill.clone(), + }); + } + kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?; if let DiedTo::MapleWolf { source, .. } = died_to && let Ok(maple) = new_village.character_by_id_mut(*source) { *maple.maple_wolf_mut()? = night; } + } else { + recorded_changes.retain(|c| c != change); } } NightChange::Shapeshift { source, into } => { @@ -122,6 +148,8 @@ impl Village { night: NonZeroU8::new(night).unwrap(), }); // role change pushed in [apply_shapeshift] + } else { + recorded_changes.retain(|c| c != change); } } @@ -149,6 +177,11 @@ impl Village { .character_by_id_mut(*source)? .direwolf_mut()? .replace(*target); + + recorded_changes.retain(|c| { + matches!(c, NightChange::RoleBlock { .. }) + || c.target().map(|t| t == *target).unwrap_or_default().not() + }); } NightChange::MasonRecruit { @@ -162,6 +195,7 @@ impl Village { tried_recruiting: *recruiting, }, ); + recorded_changes.retain(|c| c != change); } else { new_village .character_by_id_mut(*mason_leader)? @@ -253,6 +287,6 @@ impl Village { if new_village.is_game_over().is_none() { new_village.to_day()?; } - Ok(new_village) + Ok((new_village, recorded_changes.into_boxed_slice())) } } diff --git a/werewolves-proto/src/game_test/role/direwolf.rs b/werewolves-proto/src/game_test/role/direwolf.rs new file mode 100644 index 0000000..1ee6803 --- /dev/null +++ b/werewolves-proto/src/game_test/role/direwolf.rs @@ -0,0 +1,123 @@ +// Copyright (C) 2025 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 crate::{ + game::{Game, GameSettings, OrRandom, SetupRole}, + game_test::{ + ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log, + }, +}; + +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + +#[test] +fn block_on_wolf_kill_target_prevents_kill() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let scapegoat = player_ids.next().unwrap(); + let direwolf = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(false), + }, + scapegoat, + ); + settings.add_and_assign(SetupRole::DireWolf, direwolf); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + game.next().title().wolves_intro(); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + let scapegoat_char_id = game.character_by_player_id(scapegoat).character_id(); + game.mark(scapegoat_char_id); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark(scapegoat_char_id); + game.r#continue().sleep(); + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(scapegoat).died_to().cloned(), + None + ); +} + +#[test] +fn block_on_guardian_target_prevents_the_visit() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let scapegoat = player_ids.next().unwrap(); + let guardian = player_ids.next().unwrap(); + let direwolf = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(false), + }, + scapegoat, + ); + settings.add_and_assign(SetupRole::Guardian, guardian); + settings.add_and_assign(SetupRole::DireWolf, direwolf); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + game.next().title().wolves_intro(); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().guardian(); + let scapegoat_char_id = game.character_by_player_id(scapegoat).character_id(); + game.mark(scapegoat_char_id); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark_villager(); + game.r#continue().r#continue(); + + game.next().title().direwolf(); + game.mark(scapegoat_char_id); + game.r#continue().sleep(); + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(guardian) + .guardian() + .unwrap() + .clone(), + None + ); +} diff --git a/werewolves-proto/src/game_test/role/guardian.rs b/werewolves-proto/src/game_test/role/guardian.rs index 9a410fa..bc2d079 100644 --- a/werewolves-proto/src/game_test/role/guardian.rs +++ b/werewolves-proto/src/game_test/role/guardian.rs @@ -14,6 +14,9 @@ // along with this program. If not, see . use core::num::NonZeroU8; +#[allow(unused)] +use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; + use crate::{ diedto::DiedTo, error::GameError, @@ -25,7 +28,7 @@ use crate::{ host::{HostGameMessage, HostNightMessage}, night::{ActionPromptTitle, ActionResponse}, }, - role::{PreviousGuardianAction, Role}, + role::{PreviousGuardianAction, Role, RoleTitle}, }; #[test] @@ -215,3 +218,54 @@ fn cannot_visit_previous_nights_guard_target() { Err(GameError::InvalidTarget) ); } + +#[test] +fn protects_from_militia() { + init_log(); + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let guardian = player_ids.next().unwrap(); + let militia = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign(SetupRole::Militia, militia); + settings.add_and_assign(SetupRole::Guardian, guardian); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + game.next().title().wolves_intro(); + game.r#continue().sleep(); + game.next_expect_day(); + + game.execute().title().guardian(); + let mut villagers = game + .village() + .characters() + .into_iter() + .filter(|c| c.alive() && matches!(c.role().title(), RoleTitle::Villager)); + let protected = villagers.next().unwrap(); + game.mark(protected.character_id()); + game.r#continue().sleep(); + + game.next().title().wolf_pack_kill(); + game.mark(villagers.next().unwrap().character_id()); + game.r#continue().sleep(); + + game.next().title().militia(); + game.mark(protected.character_id()); + game.r#continue().sleep(); + + game.next_expect_day(); + + assert_eq!( + game.character_by_player_id(protected.player_id()) + .died_to() + .cloned(), + None + ); + assert_eq!( + game.character_by_player_id(militia).role().clone(), + Role::Militia { targeted: None } + ); +} diff --git a/werewolves-proto/src/game_test/role/mason.rs b/werewolves-proto/src/game_test/role/mason.rs index 13fa857..5728f8c 100644 --- a/werewolves-proto/src/game_test/role/mason.rs +++ b/werewolves-proto/src/game_test/role/mason.rs @@ -12,13 +12,13 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use core::num::NonZeroU8; +use core::num::{NonZero, NonZeroU8}; #[allow(unused)] use pretty_assertions::{assert_eq, assert_ne, assert_str_eq}; use crate::{ diedto::DiedTo, - game::{Game, GameSettings, SetupRole}, + game::{Game, GameSettings, OrRandom, SetupRole}, game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players}, message::night::{ActionPrompt, ActionPromptTitle}, }; @@ -243,3 +243,63 @@ fn masons_wake_even_if_leader_died() { } ); } + +#[test] +fn masons_get_go_back_to_sleep() { + let players = gen_players(1..21); + let mut player_ids = players.iter().map(|p| p.player_id); + let mason = player_ids.next().unwrap(); + let scapegoat = player_ids.next().unwrap(); + let beholder = player_ids.next().unwrap(); + let wolf = player_ids.next().unwrap(); + let mut settings = GameSettings::empty(); + settings.add_and_assign( + SetupRole::MasonLeader { + recruits_available: NonZeroU8::new(1).unwrap(), + }, + mason, + ); + settings.add_and_assign( + SetupRole::Scapegoat { + redeemed: OrRandom::Determined(false), + }, + scapegoat, + ); + settings.add_and_assign(SetupRole::Beholder, beholder); + settings.add_and_assign(SetupRole::Werewolf, wolf); + settings.fill_remaining_slots_with_villagers(20); + let mut game = Game::new(&players, settings).unwrap(); + game.r#continue().r#continue(); + game.next().title().wolves_intro(); + game.r#continue().sleep(); + game.next_expect_day(); + + assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill); + game.mark_villager(); + 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().beholder(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next_expect_day(); + game.execute().title().wolf_pack_kill(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next().title().masons_wake(); + game.r#continue().sleep(); + + game.next().title().beholder(); + game.mark_villager(); + game.r#continue().sleep(); + + game.next_expect_day(); +} diff --git a/werewolves-proto/src/game_test/role/mod.rs b/werewolves-proto/src/game_test/role/mod.rs index 0675b10..1f33ccb 100644 --- a/werewolves-proto/src/game_test/role/mod.rs +++ b/werewolves-proto/src/game_test/role/mod.rs @@ -16,6 +16,7 @@ mod apprentice; mod beholder; mod black_knight; mod bloodletter; +mod direwolf; mod diseased; mod elder; mod empath; diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 0555d85..fcfa455 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -15,7 +15,7 @@ use core::{num::NonZeroU8, ops::Deref}; use serde::{Deserialize, Serialize}; -use werewolves_macros::{ChecksAs, Titles}; +use werewolves_macros::{ChecksAs, Extract, Titles}; use crate::{ character::CharacterId, @@ -56,7 +56,7 @@ pub enum ActionType { Beholder, } -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)] +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)] pub enum ActionPrompt { #[checks(ActionType::Cover)] CoverOfDarkness, @@ -215,6 +215,58 @@ pub enum ActionPrompt { } impl ActionPrompt { + pub(crate) const fn marked(&self) -> Option<(CharacterId, Option)> { + match self { + ActionPrompt::Seer { marked, .. } + | ActionPrompt::Protector { marked, .. } + | ActionPrompt::Gravedigger { marked, .. } + | ActionPrompt::Hunter { marked, .. } + | ActionPrompt::Militia { marked, .. } + | ActionPrompt::MapleWolf { marked, .. } + | ActionPrompt::Guardian { marked, .. } + | ActionPrompt::Adjudicator { marked, .. } + | ActionPrompt::PowerSeer { marked, .. } + | ActionPrompt::Mortician { marked, .. } + | ActionPrompt::Beholder { marked, .. } + | ActionPrompt::MasonLeaderRecruit { marked, .. } + | ActionPrompt::Empath { marked, .. } + | ActionPrompt::Vindicator { marked, .. } + | ActionPrompt::PyreMaster { marked, .. } + | ActionPrompt::WolfPackKill { marked, .. } + | ActionPrompt::AlphaWolf { marked, .. } + | ActionPrompt::DireWolf { marked, .. } + | ActionPrompt::LoneWolfKill { marked, .. } + | ActionPrompt::Bloodletter { marked, .. } => match *marked { + Some(marked) => Some((marked, None)), + None => None, + }, + ActionPrompt::Arcanist { + marked: (None, Some(marked)), + .. + } + | ActionPrompt::Arcanist { + marked: (Some(marked), None), + .. + } => Some((*marked, None)), + ActionPrompt::Arcanist { + marked: (Some(marked1), Some(marked2)), + .. + } => Some((*marked1, Some(*marked2))), + + ActionPrompt::Arcanist { + marked: (None, None), + .. + } + | ActionPrompt::CoverOfDarkness + | ActionPrompt::WolvesIntro { .. } + | ActionPrompt::RoleChange { .. } + | ActionPrompt::ElderReveal { .. } + | ActionPrompt::MasonsWake { .. } + | ActionPrompt::Shapeshifter { .. } + | ActionPrompt::Insomniac { .. } + | ActionPrompt::TraitorIntro { .. } => None, + } + } pub(crate) const fn character_id(&self) -> Option { match self { ActionPrompt::TraitorIntro { character_id } @@ -283,6 +335,17 @@ impl ActionPrompt { | ActionPrompt::LoneWolfKill { .. } => false, } } + + pub fn interactive(&self) -> bool { + match self { + Self::Shapeshifter { .. } => true, + _ => !matches!( + self.with_mark(CharacterId::new()), + Err(GameError::RoleDoesntMark) + ), + } + } + pub(crate) fn with_mark(&self, mark: CharacterId) -> Result { let mut prompt = self.clone(); match &mut prompt { @@ -490,6 +553,7 @@ pub enum ActionResponse { MarkTarget(CharacterId), Shapeshift, Continue, + ContinueToResult, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index e0ed7fc..7ec07d8 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -133,31 +133,37 @@ pub enum Role { #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] Seer, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] Arcanist, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] Adjudicator, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] PowerSeer, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] Mortician, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] Beholder, #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] @@ -199,6 +205,7 @@ pub enum Role { #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] #[checks("is_mentor")] + #[checks("doesnt_wake_if_died_tonight")] Gravedigger, #[checks(Alignment::Village)] #[checks(Killer::Killer)] @@ -243,6 +250,7 @@ pub enum Role { #[checks(Alignment::Village)] #[checks(Powerful::Powerful)] #[checks(Killer::NotKiller)] + #[checks("doesnt_wake_if_died_tonight")] Insomniac, #[checks(Alignment::Wolves)] @@ -487,7 +495,7 @@ pub enum ArcanistCheck { pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap(); pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap(); -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum RoleBlock { Direwolf, } diff --git a/werewolves-server/src/game.rs b/werewolves-server/src/game.rs index 934cdd7..8c0872d 100644 --- a/werewolves-server/src/game.rs +++ b/werewolves-server/src/game.rs @@ -296,7 +296,7 @@ impl GameRunner { HostMessage::GetState => self.game.process(HostGameMessage::GetState), HostMessage::InGame(msg) => self.game.process(msg), HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => { - Err(GameError::InvalidMessageForGameState) + Err(GameError::GameOngoing) } HostMessage::Echo(echo) => Ok(echo), } diff --git a/werewolves/img/apprentice.svg b/werewolves/img/apprentice.svg index ce83ee9..cd83b05 100644 --- a/werewolves/img/apprentice.svg +++ b/werewolves/img/apprentice.svg @@ -2,9 +2,9 @@ + transform="translate(-29.360037,-825.92694)"> diff --git a/werewolves/img/equal.svg b/werewolves/img/equal.svg index 97909e7..b1af6fb 100644 --- a/werewolves/img/equal.svg +++ b/werewolves/img/equal.svg @@ -12,15 +12,16 @@ xmlns:svg="http://www.w3.org/2000/svg"> + transform="translate(-151.13398,-751.15207)"> diff --git a/werewolves/img/insomniac.svg b/werewolves/img/insomniac.svg index cd00413..86140f9 100644 --- a/werewolves/img/insomniac.svg +++ b/werewolves/img/insomniac.svg @@ -2,9 +2,9 @@ + inkscape:export-ydpi="900.08" /> diff --git a/werewolves/img/not-equal.svg b/werewolves/img/not-equal.svg index b91b4e8..18a92e5 100644 --- a/werewolves/img/not-equal.svg +++ b/werewolves/img/not-equal.svg @@ -3,8 +3,8 @@ + id="path15-9" + transform="translate(348.34768,68.803695)" + d="m 74.180658,260.24033 c -0.269428,0.0674 -0.796295,-0.78347 -1.034437,-0.92636 -0.238143,-0.14288 -1.236814,-0.20738 -1.304171,-0.4768 -0.06736,-0.26943 0.783475,-0.7963 0.92636,-1.03444 0.142886,-0.23814 0.207377,-1.23681 0.476805,-1.30417 0.269427,-0.0674 0.796295,0.78347 1.034437,0.92636 0.238143,0.14289 1.236814,0.20738 1.304171,0.4768 0.06736,0.26943 -0.783475,0.7963 -0.926361,1.03444 -0.142885,0.23814 -0.207376,1.23682 -0.476804,1.30417 z" /> diff --git a/werewolves/index.scss b/werewolves/index.scss index 7a03892..8d9f6a8 100644 --- a/werewolves/index.scss +++ b/werewolves/index.scss @@ -411,6 +411,7 @@ button { border: 3px solid rgba(0, 0, 0, 0.4); // min-width: 20%; flex-shrink: 1; + flex-grow: 1; .headline { display: flex; @@ -663,19 +664,22 @@ clients { justify-content: space-evenly; color: black; align-items: center; + align-content: center; gap: 10px; + max-height: 100vh; } .role-reveal-card { justify-content: center; - min-width: 5cm; + // min-width: 5cm; + flex-grow: 1; display: flex; align-items: center; align-content: center; flex-direction: column; gap: 10px; - padding: 1cm; + padding: 20px; border: 1px solid $wolves_color; background-color: color.change($wolves_color, $alpha: 0.1); min-width: 100px; @@ -1436,6 +1440,23 @@ input { } } +li.change, +li.choice { + width: fit-content; + + &:hover { + .li-icon { + filter: brightness(5000%); + } + + backdrop-filter: invert(15%); + } +} + +.li-icon { + filter: grayscale(100%) brightness(150%); +} + .icon { width: 32px; height: 32px; @@ -1853,6 +1874,19 @@ input { } } +.info-icon-list-grow { + padding: 20px 0 20px 0; + flex-grow: 1; + width: 100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + + img { + flex-grow: 1; + } +} + .info-player-list { display: flex; flex-direction: column; @@ -1862,7 +1896,8 @@ input { justify-content: center; gap: 10px; - &.masons { + &.masons, + &.large { font-size: 2rem; } } @@ -1870,13 +1905,15 @@ input { .two-column { display: grid; grid-template-columns: 3fr 2fr; + height: 100%; } .seer-check { display: flex; flex-direction: column; flex-wrap: nowrap; - zoom: 90%; + height: 100%; + justify-content: space-around; } .role-title-span { @@ -1923,6 +1960,7 @@ input { display: flex; flex-direction: column; justify-content: center; + gap: 10px; } .add-player { @@ -2005,10 +2043,28 @@ input { display: flex; flex-direction: column; flex-wrap: nowrap; - justify-content: center; + justify-content: space-around; align-items: center; + gap: 10px; height: var(--information-height); + + h1, + h2, + h3, + h4, + h5, + h6 { + margin: 0; + + &:first-child { + padding-top: 10px; + } + + &:last-child { + padding-bottom: 10px; + } + } } .setup-aura { @@ -2042,3 +2098,11 @@ input { flex-shrink: 1; } } + +.guardian-select { + max-height: 100vh; +} + +.story-text { + font-size: 1.7em; +} diff --git a/werewolves/src/components/action/prompt.rs b/werewolves/src/components/action/prompt.rs index d07cf7b..d1ee15b 100644 --- a/werewolves/src/components/action/prompt.rs +++ b/werewolves/src/components/action/prompt.rs @@ -66,9 +66,16 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { if let Some(page) = props.pages.get(props.page_idx).map(|page| { let next = props.big_screen.not().then(|| { let send = props.on_complete.clone(); + let page_idx = props.page_idx; + let pages_total = props.pages.len(); + let prompt = props.prompt.clone(); let on_page_next = Callback::from(move |_| { send.emit(HostMessage::InGame(HostGameMessage::Night( - HostNightMessage::NextPage, + if page_idx + 1 >= pages_total && !prompt.interactive() { + HostNightMessage::ActionResponse(ActionResponse::ContinueToResult) + } else { + HostNightMessage::NextPage + }, ))) }); html! { @@ -152,41 +159,29 @@ pub fn Prompt(props: &ActionPromptProps) -> Html { ActionPrompt::RoleChange { .. } | ActionPrompt::ElderReveal { .. } | ActionPrompt::Insomniac { .. } => { - if let Some(cb) = continue_callback { - cb.emit(()); + if !props.big_screen { + props + .on_complete + .emit(HostMessage::InGame(HostGameMessage::Night( + HostNightMessage::ActionResponse(ActionResponse::ContinueToResult), + ))); } - return html! {}; + return html! { + + }; } ActionPrompt::Guardian { character_id, - previous, living_players, marked, + .. } => { - let last_protect = previous.as_ref().map(|prev| match prev { - PreviousGuardianAction::Protect(target) => { - html! { - <> - {"last night you protected: "} - ::into(target)}/> - - } - } - PreviousGuardianAction::Guard(target) => html! { - <> - {"last night you guarded: "} - ::into(target)}/> - - }, - }); let marked = marked.iter().cloned().collect::>(); return html! { -
+
{identity_html(props, Some(character_id))} -

{"guardian"}

- {last_protect} . +use convert_case::{Case, Casing}; use werewolves_proto::{ message::{CharacterIdentity, PublicIdentity}, role::RoleTitle, @@ -42,7 +43,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html { { props.wolves.iter().map(|w| html!{
-

{w.1.to_string()}

+

{w.1.to_string().to_case(Case::Title)}

::into(&w.0)} />
}).collect::() diff --git a/werewolves/src/components/icon.rs b/werewolves/src/components/icon.rs index 447302b..a8e2976 100644 --- a/werewolves/src/components/icon.rs +++ b/werewolves/src/components/icon.rs @@ -124,6 +124,8 @@ pub struct IconProps { pub inactive: bool, #[prop_or_default] pub icon_type: IconType, + #[prop_or_default] + pub classes: Classes, } #[function_component] pub fn Icon( @@ -131,12 +133,13 @@ pub fn Icon( source, inactive, icon_type, + classes, }: &IconProps, ) -> Html { html! { } } diff --git a/werewolves/src/components/story.rs b/werewolves/src/components/story.rs index d705d2b..45d478f 100644 --- a/werewolves/src/components/story.rs +++ b/werewolves/src/components/story.rs @@ -77,9 +77,7 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html { .collect::>())); let changes = match changes { GameActions::DayDetails(day_changes) => { - let execute_list = if day_changes.is_empty() { - html! {{"no one was executed"}} - } else { + let execute_list = day_changes .iter() .map(|c| match c { @@ -88,15 +86,12 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html { .filter_map(|c| story.starting_village.character_by_id(c).ok()) .map(|c| { html! { - // - // } }) - .collect::() - }; + .collect::(); - Some(html! { + day_changes.is_empty().not().then_some(html! {

{"village executed"}

@@ -117,16 +112,19 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html { .collect::(); let changes = details - .changes - .iter() - .map(|c| { - html! { -
  • - -
  • - } - }) - .collect::(); + .changes + .iter() + .map(|c| { + html! { +
  • + +
  • + } + }) + .collect::(); html! {
    @@ -174,7 +172,6 @@ struct StoryNightChangeProps { #[function_component] fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html { match change { - NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{ <> @@ -187,9 +184,9 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha html!{ <> - {"gained the"} + {"gained the"} - {"aura from"} + {"aura from"} } @@ -203,26 +200,29 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha html! { <> - {"is now"} + {"is now"} } }) .unwrap_or_default(), - NightChange::Kill { target, died_to } => characters + NightChange::Kill { target, died_to } => { + + characters .get(target) .map(|target| { html! { <> - {"died to"} + {"died to"} } }) - .unwrap_or_default(), + .unwrap_or_default() + }, NightChange::RoleBlock { source, target, .. } => characters .get(source) .and_then(|s| characters.get(target).map(|t| (s, t))) @@ -230,7 +230,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha html! { <> - {"role blocked"} + {"role blocked"} } @@ -243,7 +243,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha html! { <> - {"shapeshifted into"} + {"shapeshifted into"} } @@ -256,7 +256,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha html! { <> - {"learned they are the Elder"} + {"learned they are the Elder"} } }) @@ -268,9 +268,9 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha html! { <> - {"found the scapegoat in"} + {"found the scapegoat in"} - {"and took on their curse"} + {"and took on their curse"} } }) @@ -292,28 +292,28 @@ struct StoryNightResultProps { fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html { match result { StoryActionResult::ShiftFailed => html!{ - {"but it failed"} + {"but it failed"} }, StoryActionResult::Drunk => html! { - - {"but got "} - - {" instead"} - + <> + {"but got "} + + {" instead"} + }, StoryActionResult::BeholderSawEverything => html!{ - {"and saw everything 👁️"} + {"and saw everything 👁️"} }, StoryActionResult::BeholderSawNothing => html!{ - {"but saw nothing"} + {"but saw nothing"} }, StoryActionResult::RoleBlocked => html! { - {"but was role blocked"} + {"but was role blocked"} }, StoryActionResult::Seer(alignment) => { html! { - {"and saw"} + {"and saw"} } @@ -321,25 +321,25 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes StoryActionResult::PowerSeer { powerful } => { html! { - {"and discovered they are"} + {"and discovered they are"} } } StoryActionResult::Adjudicator { killer } => html! { - {"and saw"} + {"and saw"} }, StoryActionResult::Arcanist(same) => html! { - {"and saw"} + {"and saw"} }, StoryActionResult::GraveDigger(None) => html! { - + {"found an empty grave"} }, @@ -347,7 +347,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes let category = Into::::into(*role_title).category(); html! { - {"found the body of a"} + {"found the body of a"} {role_title.to_string().to_case(Case::Title)} @@ -356,7 +356,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes } StoryActionResult::Mortician(died_to_title) => html! { <> - {"and found the cause of death to be"} + {"and found the cause of death to be"} }, @@ -377,7 +377,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes StoryActionResult::Empath { scapegoat: false } => html! { <> - {"and saw that they are"} + {"and saw that they are"}
    @@ -388,7 +388,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes }, StoryActionResult::Empath { scapegoat: true } => html! { <> - {"and saw that they are"} + {"and saw that they are"}
    @@ -416,7 +416,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {action} + {action} } @@ -438,9 +438,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {"compared"} + {"compared"} - {"and"} + {"and"} } @@ -458,9 +458,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {"'s masons"} + {"'s masons"} {masons} - {"convened in secret"} + {"convened in secret"} } }), @@ -511,9 +511,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {"invited"} + {"invited"} - {"for dinner"} + {"for dinner"} } }), @@ -548,7 +548,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {"attempted a kill on"} + {"attempted a kill on"} } @@ -562,7 +562,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {"decided to shapeshift into the wolf kill target"} + {"decided to shapeshift into the wolf kill target"} } }) @@ -584,7 +584,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi html! { <> - {"witnessed visits from"} + {"witnessed visits from"} } }) @@ -598,8 +598,12 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoi choice_body .map(|choice_body| { html! { -
  • - +
  • + {choice_body} {result}
  • diff --git a/werewolves/src/pages/drunk_page.rs b/werewolves/src/pages/drunk_page.rs index c0a27f7..06a0441 100644 --- a/werewolves/src/pages/drunk_page.rs +++ b/werewolves/src/pages/drunk_page.rs @@ -11,9 +11,9 @@ pub fn DrunkPage() -> Html {

    {"DRUNK"}

    {"YOU GOT DRUNK INSTEAD"}

    -

    - -

    +
    + +

    {"YOUR NIGHT ACTION DID NOT TAKE PLACE"}

    diff --git a/werewolves/src/pages/role_page/adjudicator.rs b/werewolves/src/pages/role_page/adjudicator.rs index 46dd54f..5a1d97d 100644 --- a/werewolves/src/pages/role_page/adjudicator.rs +++ b/werewolves/src/pages/role_page/adjudicator.rs @@ -12,8 +12,6 @@ // // 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 werewolves_proto::role::Killer; use yew::prelude::*; @@ -26,10 +24,9 @@ pub fn AdjudicatorPage1() -> Html {

    {"ADJUDICATOR"}

    {"PICK A PLAYER"}

    -

    - - -

    +
    + +

    {"YOU WILL CHECK IF THEY APPEAR AS A KILLER"}

    @@ -51,13 +48,11 @@ pub fn AdjudicatorResult(AdjudicatorResultProps { killer }: &AdjudicatorResultPr Killer::Killer => html! { }, Killer::NotKiller => html! { }, }; @@ -66,7 +61,7 @@ pub fn AdjudicatorResult(AdjudicatorResultProps { killer }: &AdjudicatorResultPr

    {"ADJUDICATOR"}

    {"YOUR TARGET"}

    -

    {icon}

    +
    {icon}

    {text}

    diff --git a/werewolves/src/pages/role_page/alpha_wolf.rs b/werewolves/src/pages/role_page/alpha_wolf.rs index b64e328..9e6c0a0 100644 --- a/werewolves/src/pages/role_page/alpha_wolf.rs +++ b/werewolves/src/pages/role_page/alpha_wolf.rs @@ -25,10 +25,10 @@ pub fn AlphaWolfPage1() -> Html { {"ONCE PER GAME"} {" KILL ABILITY"} -

    +

    {"POINT AT YOUR TARGET "} {"OR GO BACK TO SLEEP"} -

    +
    } diff --git a/werewolves/src/pages/role_page/arcanist.rs b/werewolves/src/pages/role_page/arcanist.rs index 3908b9d..857fdb7 100644 --- a/werewolves/src/pages/role_page/arcanist.rs +++ b/werewolves/src/pages/role_page/arcanist.rs @@ -49,22 +49,12 @@ pub fn ArcanistResult(ArcanistResultProps { alignment_eq }: &ArcanistResultProps }; let icons = match alignment_eq { AlignmentEq::Same => html! { - <> - // - // - // {"OR"} - // - // - - + }, AlignmentEq::Different => html! { <> - - - // {"OR"} - // - // + + }, }; @@ -73,9 +63,9 @@ pub fn ArcanistResult(ArcanistResultProps { alignment_eq }: &ArcanistResultProps

    {"ARCANIST"}

    {"YOUR TARGETS APPEAR AS"}

    -

    +
    {icons} -

    +

    {text}

    diff --git a/werewolves/src/pages/role_page/bloodletter.rs b/werewolves/src/pages/role_page/bloodletter.rs index ed53863..cedb5af 100644 --- a/werewolves/src/pages/role_page/bloodletter.rs +++ b/werewolves/src/pages/role_page/bloodletter.rs @@ -24,9 +24,8 @@ pub fn BloodletterPage1() -> Html {

    {"PICK A PLAYER"}

    {"THEY WILL BE COVERED IN WOLF BLOOD"}

    -

    {"AND"}

    - {"APPEAR AS A WOLF "} + {"AND APPEAR AS A WOLF "} {" KILLER "} diff --git a/werewolves/src/pages/role_page/empath.rs b/werewolves/src/pages/role_page/empath.rs index 0b50d08..53e9f36 100644 --- a/werewolves/src/pages/role_page/empath.rs +++ b/werewolves/src/pages/role_page/empath.rs @@ -26,7 +26,7 @@ pub fn EmpathPage1() -> Html {

    {"PICK A PLAYER"}

    {"YOU WILL CHECK IF THEY ARE THE SCAPEGOAT"}

    -

    {"AND IF THEY ARE, TAKE ON THEIR CURSE"}

    +

    {"IF THEY ARE, YOU WILL TAKE ON THEIR CURSE"}

    } @@ -43,18 +43,26 @@ pub fn EmpathResult(EmpathResultProps { scapegoat }: &EmpathResultProps) -> Html true => "THE SCAPEGOAT", false => "NOT THE SCAPEGOAT", }; + let icon = match scapegoat { + true => html! { + + }, + false => html! { + + }, + }; html! {

    {"EMPATH"}

    {"YOUR TARGET IS"}

    -

    - -

    +
    + {icon} +

    {text}

    diff --git a/werewolves/src/pages/role_page/gravedigger.rs b/werewolves/src/pages/role_page/gravedigger.rs index 66cd487..a18c151 100644 --- a/werewolves/src/pages/role_page/gravedigger.rs +++ b/werewolves/src/pages/role_page/gravedigger.rs @@ -29,8 +29,8 @@ pub fn GravediggerPage1() -> Html { {"DEAD"} {" PLAYER"} -
    - +
    +

    {"YOU WILL LEARN THEIR ROLE"} @@ -54,7 +54,7 @@ pub fn GravediggerResultPage( .map(|r| { html! {

    - {"YOU DIG UP THE BODY OF A "} + {"AND FIND A "} {r.to_string().to_case(Case::Upper)} @@ -75,12 +75,9 @@ pub fn GravediggerResultPage(

    {"GRAVEDIGGER"}

    {"YOU CHECK THE ROLE OF YOUR TARGET"}

    -

    - -

    +
    + +
    {text}
    diff --git a/werewolves/src/pages/role_page/guardian.rs b/werewolves/src/pages/role_page/guardian.rs index 81d8076..e088c28 100644 --- a/werewolves/src/pages/role_page/guardian.rs +++ b/werewolves/src/pages/role_page/guardian.rs @@ -29,9 +29,9 @@ pub fn GuardianPageNoPrevProtect() -> Html {

    {"GUARDIAN"}

    {"PICK A PLAYER"}

    -

    - -

    +
    + +

    {"CHOOSE SOMEONE TO PROTECT FROM DEATH"}

    @@ -90,12 +90,12 @@ pub fn GuardianPagePreviousGuard(GuardianPageProps { previous }: &GuardianPagePr

    {"GUARDIAN"}

    {"LAST TIME YOU GUARDED"}

    +
    + +
    -

    - -

    {"YOU CANNOT PROTECT THEM TONIGHT"}

    diff --git a/werewolves/src/pages/role_page/hunter.rs b/werewolves/src/pages/role_page/hunter.rs index aed7dcc..d3968ea 100644 --- a/werewolves/src/pages/role_page/hunter.rs +++ b/werewolves/src/pages/role_page/hunter.rs @@ -23,12 +23,12 @@ pub fn HunterPage1() -> Html {

    {"HUNTER"}

    {"SET A HUNTER'S TRAP ON A PLAYER"}

    -
    - +
    +

    - {"IF YOU DIE TONIGHT, OR ARE EXECUTED TOMORROW"} - {"THIS PLAYER WILL DIE AT NIGHT AS WELL"} + {"IF YOU DIE TONIGHT, OR ARE EXECUTED TOMORROW "} + {"THIS PLAYER WILL DIE AT NIGHT"}

    diff --git a/werewolves/src/pages/role_page/insomniac.rs b/werewolves/src/pages/role_page/insomniac.rs index ad66d11..992ee32 100644 --- a/werewolves/src/pages/role_page/insomniac.rs +++ b/werewolves/src/pages/role_page/insomniac.rs @@ -15,7 +15,7 @@ use werewolves_proto::message::night::Visits; use yew::prelude::*; -use crate::components::CharacterTargetCard; +use crate::components::{CharacterTargetCard, Icon, IconSource}; #[function_component] pub fn InsomniacPage1() -> Html { @@ -23,9 +23,12 @@ pub fn InsomniacPage1() -> Html {

    {"INSOMNIAC"}

    -

    {"YOUR POOR SLEEP RESULTS IN BEING WOKEN BY VISITORS IN THE NIGHT"}

    +

    {"YOUR SLEEP IS INTERRUPTED"}

    +
    + +

    - {"YOU WILL REMEMBER WHO VISITED YOU TONIGHT"} + {"THE FOLLOWING PEOPLE VISITED YOU TONIGHT"}

    @@ -52,7 +55,7 @@ pub fn InsomniacResult(InsomniacResultProps { visits }: &InsomniacResultProps) -

    {"INSOMNIAC"}

    {"YOU WERE VISITED IN THE NIGHT BY:"}

    -
    +
    {visitors}
    diff --git a/werewolves/src/pages/role_page/mason.rs b/werewolves/src/pages/role_page/mason.rs index f47c45a..00dcc5d 100644 --- a/werewolves/src/pages/role_page/mason.rs +++ b/werewolves/src/pages/role_page/mason.rs @@ -53,7 +53,7 @@ pub fn MasonRecruitPage1( {"EVERY NIGHT"} {", AS LONG AS THEY ARE ALIVE AND REMAIN VILLAGE ALIGNED"} -

    {"WOULD YOU LIKE TO RECRUIT TONIGHT?"}

    +

    {"WOULD YOU LIKE TO RECRUIT TONIGHT?"}

    } diff --git a/werewolves/src/pages/role_page/militia.rs b/werewolves/src/pages/role_page/militia.rs index 3c9716f..170aa80 100644 --- a/werewolves/src/pages/role_page/militia.rs +++ b/werewolves/src/pages/role_page/militia.rs @@ -27,8 +27,8 @@ pub fn MilitiaPage1() -> Html { {"ONCE PER GAME"} {" KILL ABILITY"} -
    - +
    +

    {"POINT AT YOUR TARGET "} diff --git a/werewolves/src/pages/role_page/power_seer.rs b/werewolves/src/pages/role_page/power_seer.rs index cc75d45..101f882 100644 --- a/werewolves/src/pages/role_page/power_seer.rs +++ b/werewolves/src/pages/role_page/power_seer.rs @@ -16,7 +16,7 @@ use werewolves_proto::role::Powerful; use yew::prelude::*; -use crate::components::{Icon, IconSource, IconType}; +use crate::components::{Icon, IconSource}; #[function_component] pub fn PowerSeerPage1() -> Html { @@ -26,7 +26,7 @@ pub fn PowerSeerPage1() -> Html {

    {"PICK A PLAYER"}

    - +

    {"YOU WILL CHECK IF THEY ARE POWERFUL"}

    @@ -49,13 +49,13 @@ pub fn PowerSeerResult(PowerSeerResultProps { powerful }: &PowerSeerResultProps) Powerful::Powerful => html! { }, Powerful::NotPowerful => html! { }, }; @@ -64,7 +64,7 @@ pub fn PowerSeerResult(PowerSeerResultProps { powerful }: &PowerSeerResultProps)

    {"POWER SEER"}

    {"YOUR TARGET APPEARS AS"}

    -

    {icon}

    +
    {icon}

    {text}

    diff --git a/werewolves/src/pages/role_page/pyremaster.rs b/werewolves/src/pages/role_page/pyremaster.rs index 6d3909e..1fc1b5a 100644 --- a/werewolves/src/pages/role_page/pyremaster.rs +++ b/werewolves/src/pages/role_page/pyremaster.rs @@ -22,12 +22,14 @@ pub fn PyremasterPage1() -> Html {

    {"PYREMASTER"}

    -

    {"IF YOU WISH TO THROW A PLAYER ON THE PYRE"}

    -
    - +

    {"YOU CAN CHOOSE TO THROW A PLAYER ON THE PYRE"}

    +
    +
    -

    - {"IF YOU KILL TWO GOOD VILLAGERS LIKE THIS "} +

    + {"IF YOU KILL "} + {"TWO"} + {" GOOD VILLAGERS LIKE THIS "} {"YOU WILL DIE AS WELL"}

    diff --git a/werewolves/src/pages/role_page/seer.rs b/werewolves/src/pages/role_page/seer.rs index fcc3be6..4b20378 100644 --- a/werewolves/src/pages/role_page/seer.rs +++ b/werewolves/src/pages/role_page/seer.rs @@ -26,10 +26,10 @@ pub fn SeerPage1() -> Html {

    {"SEER"}

    {"PICK A PLAYER"}

    -

    - - -

    +
    + + +

    {"YOU WILL CHECK IF THEY APPEAR AS A VILLAGER OR PART OF THE WOLFPACK"}

    diff --git a/werewolves/src/pages/role_page/vindicator.rs b/werewolves/src/pages/role_page/vindicator.rs index 997f499..f8e1e81 100644 --- a/werewolves/src/pages/role_page/vindicator.rs +++ b/werewolves/src/pages/role_page/vindicator.rs @@ -24,9 +24,9 @@ pub fn VindicatorPage1() -> Html {

    {"A VILLAGER WAS EXECUTED"}

    {"PICK A PLAYER"}

    -

    - -

    +
    + +

    {"YOU WILL PROTECT THEM FROM A DEATH TONIGHT"}

    diff --git a/werewolves/src/pages/wolf_pack.rs b/werewolves/src/pages/wolf_pack.rs index 7043baa..b4379d5 100644 --- a/werewolves/src/pages/wolf_pack.rs +++ b/werewolves/src/pages/wolf_pack.rs @@ -26,9 +26,9 @@ pub fn WolfpackKillPage() -> Html {

    {"WOLF PACK KILL"}

    {"CHOOSE A TARGET TO EAT TONIGHT"}

    -

    - -

    +
    + +

    {"WOLVES MUST BE UNANIMOUS"}