parent
ae71ea4eb0
commit
17f583539d
|
|
@ -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(),
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 { .. }
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
]))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -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()
|
||||||
|
])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 { .. }
|
||||||
|
|
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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"))}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue