more roles, no longer exposing role through char
This commit is contained in:
parent
0889acca6a
commit
a8506c5881
|
|
@ -9,9 +9,12 @@ use proc_macro2::Span;
|
||||||
use quote::{ToTokens, quote};
|
use quote::{ToTokens, quote};
|
||||||
use syn::{parse::Parse, parse_macro_input};
|
use syn::{parse::Parse, parse_macro_input};
|
||||||
|
|
||||||
|
use crate::ref_and_mut::RefAndMut;
|
||||||
|
|
||||||
mod all;
|
mod all;
|
||||||
mod checks;
|
mod checks;
|
||||||
pub(crate) mod hashlist;
|
pub(crate) mod hashlist;
|
||||||
|
mod ref_and_mut;
|
||||||
mod targets;
|
mod targets;
|
||||||
|
|
||||||
struct IncludePath {
|
struct IncludePath {
|
||||||
|
|
@ -551,3 +554,9 @@ pub fn all(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
|
||||||
quote! {#all}.into()
|
quote! {#all}.into()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn ref_and_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
let ref_and_mut = parse_macro_input!(input as RefAndMut);
|
||||||
|
quote! {#ref_and_mut}.into()
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
use quote::{ToTokens, quote};
|
||||||
|
use syn::{parse::Parse, spanned::Spanned};
|
||||||
|
|
||||||
|
pub struct RefAndMut {
|
||||||
|
name: syn::Ident,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for RefAndMut {
|
||||||
|
fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
|
||||||
|
// let type_path = input.parse::<syn::TypePath>()?;
|
||||||
|
// let matching = input.parse::<syn::PatStruct>()?;
|
||||||
|
let name = input.parse::<syn::Ident>()?;
|
||||||
|
// panic!("{type_path:?}\n\n{matching:?}");
|
||||||
|
Ok(Self { name })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ToTokens for RefAndMut {
|
||||||
|
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
|
||||||
|
tokens.extend(quote! {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,674 @@
|
||||||
|
use core::{fmt::Display, num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::{DateTime, Village},
|
||||||
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
|
modifier::Modifier,
|
||||||
|
player::{PlayerId, RoleChange},
|
||||||
|
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
pub struct CharacterId(uuid::Uuid);
|
||||||
|
|
||||||
|
impl CharacterId {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(uuid::Uuid::new_v4())
|
||||||
|
}
|
||||||
|
pub const fn from_u128(v: u128) -> Self {
|
||||||
|
Self(uuid::Uuid::from_u128(v))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CharacterId {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
self.0.fmt(f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct Character {
|
||||||
|
player_id: PlayerId,
|
||||||
|
identity: CharacterIdentity,
|
||||||
|
role: Role,
|
||||||
|
modifier: Option<Modifier>,
|
||||||
|
died_to: Option<DiedTo>,
|
||||||
|
role_changes: Vec<RoleChange>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Character {
|
||||||
|
pub fn new(
|
||||||
|
Identification {
|
||||||
|
player_id,
|
||||||
|
public:
|
||||||
|
PublicIdentity {
|
||||||
|
name,
|
||||||
|
pronouns,
|
||||||
|
number,
|
||||||
|
},
|
||||||
|
}: Identification,
|
||||||
|
role: Role,
|
||||||
|
) -> Option<Self> {
|
||||||
|
Some(Self {
|
||||||
|
role,
|
||||||
|
identity: CharacterIdentity {
|
||||||
|
character_id: CharacterId::new(),
|
||||||
|
name,
|
||||||
|
pronouns,
|
||||||
|
number: number?,
|
||||||
|
},
|
||||||
|
player_id,
|
||||||
|
modifier: None,
|
||||||
|
died_to: None,
|
||||||
|
role_changes: Vec::new(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_power_role(&self) -> bool {
|
||||||
|
match &self.role {
|
||||||
|
Role::Scapegoat { .. } | Role::Villager => false,
|
||||||
|
|
||||||
|
_ => true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn identity(&self) -> CharacterIdentity {
|
||||||
|
self.identity.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn name(&self) -> &str {
|
||||||
|
self.identity.name.as_str()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn number(&self) -> NonZeroU8 {
|
||||||
|
self.identity.number
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn pronouns(&self) -> Option<&str> {
|
||||||
|
match self.identity.pronouns.as_ref() {
|
||||||
|
Some(p) => Some(p.as_str()),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn died_to(&self) -> Option<&DiedTo> {
|
||||||
|
self.died_to.as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn kill(&mut self, died_to: DiedTo) {
|
||||||
|
match (&mut self.role, died_to.date_time()) {
|
||||||
|
(
|
||||||
|
Role::Elder {
|
||||||
|
lost_protection_night: Some(_),
|
||||||
|
..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
) => {}
|
||||||
|
(
|
||||||
|
Role::Elder {
|
||||||
|
lost_protection_night,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
DateTime::Night { number: night },
|
||||||
|
) => {
|
||||||
|
*lost_protection_night = lost_protection_night
|
||||||
|
.is_none()
|
||||||
|
.then_some(night)
|
||||||
|
.and_then(NonZeroU8::new);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
match &self.died_to {
|
||||||
|
Some(_) => {}
|
||||||
|
None => self.died_to = Some(died_to),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn alive(&self) -> bool {
|
||||||
|
self.died_to.is_none()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn execute(&mut self, day: NonZeroU8) -> Result<()> {
|
||||||
|
if self.died_to.is_some() {
|
||||||
|
return Err(GameError::CharacterAlreadyDead);
|
||||||
|
}
|
||||||
|
self.died_to = Some(DiedTo::Execution { day });
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn character_id(&self) -> CharacterId {
|
||||||
|
self.identity.character_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn player_id(&self) -> PlayerId {
|
||||||
|
self.player_id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn role_title(&self) -> RoleTitle {
|
||||||
|
self.role.title()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn gravedigger_dig(&self) -> Option<RoleTitle> {
|
||||||
|
match &self.role {
|
||||||
|
Role::Shapeshifter {
|
||||||
|
shifted_into: Some(_),
|
||||||
|
} => None,
|
||||||
|
_ => Some(self.role.title()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn alignment(&self) -> Alignment {
|
||||||
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
|
return Alignment::Wolves;
|
||||||
|
}
|
||||||
|
self.role.alignment()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn elder_reveal(&mut self) {
|
||||||
|
if let Role::Elder {
|
||||||
|
woken_for_reveal, ..
|
||||||
|
} = &mut self.role
|
||||||
|
{
|
||||||
|
*woken_for_reveal = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<()> {
|
||||||
|
let mut role = new_role.title_to_role_excl_apprentice();
|
||||||
|
core::mem::swap(&mut role, &mut self.role);
|
||||||
|
self.role_changes.push(RoleChange {
|
||||||
|
role,
|
||||||
|
new_role,
|
||||||
|
changed_on_night: match at {
|
||||||
|
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
|
DateTime::Night { number } => number,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_wolf(&self) -> bool {
|
||||||
|
self.role.wolf()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn is_village(&self) -> bool {
|
||||||
|
!self.is_wolf()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn known_elder(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self.role,
|
||||||
|
Role::Elder {
|
||||||
|
woken_for_reveal: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
||||||
|
if !self.alive() || !self.role.wakes(village) {
|
||||||
|
return Ok(Box::new([]));
|
||||||
|
}
|
||||||
|
let night = match village.date_time() {
|
||||||
|
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
|
DateTime::Night { number } => number,
|
||||||
|
};
|
||||||
|
Ok(Box::new([match &self.role {
|
||||||
|
Role::Empath { cursed: true }
|
||||||
|
| Role::Diseased
|
||||||
|
| Role::Weightlifter
|
||||||
|
| Role::BlackKnight { .. }
|
||||||
|
| Role::Shapeshifter {
|
||||||
|
shifted_into: Some(_),
|
||||||
|
}
|
||||||
|
| Role::AlphaWolf { killed: Some(_) }
|
||||||
|
| Role::Militia { targeted: Some(_) }
|
||||||
|
| Role::Scapegoat { redeemed: false }
|
||||||
|
| Role::Elder {
|
||||||
|
woken_for_reveal: true,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| Role::Villager => return Ok(Box::new([])),
|
||||||
|
Role::Scapegoat { redeemed: true } => {
|
||||||
|
let mut dead = village.dead_characters();
|
||||||
|
dead.shuffle(&mut rand::rng());
|
||||||
|
if let Some(pr) = dead
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|d| (d.is_village() && d.is_power_role()).then_some(d.role_title()))
|
||||||
|
{
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: self.identity(),
|
||||||
|
new_role: pr,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(Box::new([]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Role::Seer => ActionPrompt::Seer {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Arcanist => ActionPrompt::Arcanist {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: (None, None),
|
||||||
|
},
|
||||||
|
Role::Protector {
|
||||||
|
last_protected: Some(last_protected),
|
||||||
|
} => ActionPrompt::Protector {
|
||||||
|
character_id: self.identity(),
|
||||||
|
targets: village.living_players_excluding(*last_protected),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Protector {
|
||||||
|
last_protected: None,
|
||||||
|
} => ActionPrompt::Protector {
|
||||||
|
character_id: self.identity(),
|
||||||
|
targets: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Apprentice(role) => {
|
||||||
|
let current_night = match village.date_time() {
|
||||||
|
DateTime::Day { number: _ } => return Ok(Box::new([])),
|
||||||
|
DateTime::Night { number } => number,
|
||||||
|
};
|
||||||
|
return Ok(village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| c.role_title() == *role)
|
||||||
|
.filter_map(|char| char.died_to)
|
||||||
|
.any(|died_to| match died_to.date_time() {
|
||||||
|
DateTime::Day { number } => number.get() + 1 >= current_night,
|
||||||
|
DateTime::Night { number } => number + 1 >= current_night,
|
||||||
|
})
|
||||||
|
.then(|| ActionPrompt::RoleChange {
|
||||||
|
character_id: self.identity(),
|
||||||
|
new_role: *role,
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
Role::Elder {
|
||||||
|
knows_on_night,
|
||||||
|
woken_for_reveal: false,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let current_night = match village.date_time() {
|
||||||
|
DateTime::Day { number: _ } => return Ok(Box::new([])),
|
||||||
|
DateTime::Night { number } => number,
|
||||||
|
};
|
||||||
|
return Ok((current_night >= knows_on_night.get())
|
||||||
|
.then_some({
|
||||||
|
ActionPrompt::ElderReveal {
|
||||||
|
character_id: self.identity(),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Werewolf => ActionPrompt::WolfPackKill {
|
||||||
|
living_villagers: village.living_players(),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_villagers: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::DireWolf => ActionPrompt::DireWolf {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players(),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
||||||
|
character_id: self.identity(),
|
||||||
|
},
|
||||||
|
Role::Gravedigger => {
|
||||||
|
let dead = village.dead_targets();
|
||||||
|
if dead.is_empty() {
|
||||||
|
return Ok(Box::new([]));
|
||||||
|
}
|
||||||
|
ActionPrompt::Gravedigger {
|
||||||
|
character_id: self.identity(),
|
||||||
|
dead_players: village.dead_targets(),
|
||||||
|
marked: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Role::Hunter { target } => ActionPrompt::Hunter {
|
||||||
|
character_id: self.identity(),
|
||||||
|
current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
|
||||||
|
character_id: self.identity(),
|
||||||
|
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||||
|
} => ActionPrompt::Guardian {
|
||||||
|
character_id: self.identity(),
|
||||||
|
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||||
|
living_players: village.living_players_excluding(prev_target.character_id),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
||||||
|
} => ActionPrompt::Guardian {
|
||||||
|
character_id: self.identity(),
|
||||||
|
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
||||||
|
living_players: village.living_players(),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: None,
|
||||||
|
} => ActionPrompt::Guardian {
|
||||||
|
character_id: self.identity(),
|
||||||
|
previous: None,
|
||||||
|
living_players: village.living_players(),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Adjudicator => ActionPrompt::Adjudicator {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::PowerSeer => ActionPrompt::PowerSeer {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Mortician => ActionPrompt::Mortician {
|
||||||
|
character_id: self.identity(),
|
||||||
|
dead_players: village.dead_targets(),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Beholder => ActionPrompt::Beholder {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::MasonLeader {
|
||||||
|
recruits_available,
|
||||||
|
recruits,
|
||||||
|
} => {
|
||||||
|
return Ok(recruits
|
||||||
|
.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 {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
Role::Vindicator => {
|
||||||
|
let last_day = match village.date_time() {
|
||||||
|
DateTime::Day { .. } => {
|
||||||
|
log::error!(
|
||||||
|
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
||||||
|
);
|
||||||
|
return Ok(Box::new([]));
|
||||||
|
}
|
||||||
|
DateTime::Night { number } => {
|
||||||
|
if number == 0 {
|
||||||
|
return Ok(Box::new([]));
|
||||||
|
}
|
||||||
|
NonZeroU8::new(number).unwrap()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return Ok(village
|
||||||
|
.executions_on_day(last_day)
|
||||||
|
.iter()
|
||||||
|
.any(|c| c.is_village())
|
||||||
|
.then(|| ActionPrompt::Vindicator {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
})
|
||||||
|
.into_iter()
|
||||||
|
.collect());
|
||||||
|
}
|
||||||
|
Role::PyreMaster { .. } => ActionPrompt::PyreMaster {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
|
}]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
pub const fn role(&self) -> &Role {
|
||||||
|
&self.role
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn killer(&self) -> bool {
|
||||||
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.role.killer()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn powerful(&self) -> bool {
|
||||||
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
self.role.powerful()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn hunter<'a>(&'a self) -> Result<Hunter<'a>> {
|
||||||
|
match &self.role {
|
||||||
|
Role::Hunter { target } => Ok(Hunter(target)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Hunter,
|
||||||
|
got: self.role_title(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn hunter_mut<'a>(&'a mut self) -> Result<HunterMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Hunter { target } => Ok(HunterMut(target)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Hunter,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn shapeshifter<'a>(&'a self) -> Result<Shapeshifter<'a>> {
|
||||||
|
match &self.role {
|
||||||
|
Role::Shapeshifter { shifted_into } => Ok(Shapeshifter(shifted_into)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Shapeshifter,
|
||||||
|
got: self.role_title(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn shapeshifter_mut<'a>(&'a mut self) -> Result<ShapeshifterMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Shapeshifter { shifted_into } => Ok(ShapeshifterMut(shifted_into)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Shapeshifter,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn mason_leader<'a>(&'a self) -> Result<MasonLeader<'a>> {
|
||||||
|
match &self.role {
|
||||||
|
Role::MasonLeader {
|
||||||
|
recruits_available,
|
||||||
|
recruits,
|
||||||
|
} => Ok(MasonLeader(recruits_available, recruits)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::MasonLeader,
|
||||||
|
got: self.role_title(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn mason_leader_mut<'a>(&'a mut self) -> Result<MasonLeaderMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::MasonLeader {
|
||||||
|
recruits_available,
|
||||||
|
recruits,
|
||||||
|
} => Ok(MasonLeaderMut(recruits_available, recruits)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::MasonLeader,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn scapegoat<'a>(&'a self) -> Result<Scapegoat<'a>> {
|
||||||
|
match &self.role {
|
||||||
|
Role::Scapegoat { redeemed } => Ok(Scapegoat(redeemed)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Scapegoat,
|
||||||
|
got: self.role_title(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn scapegoat_mut<'a>(&'a mut self) -> Result<ScapegoatMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Scapegoat { redeemed } => Ok(ScapegoatMut(redeemed)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Scapegoat,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn empath<'a>(&'a self) -> Result<Empath<'a>> {
|
||||||
|
match &self.role {
|
||||||
|
Role::Empath { cursed } => Ok(Empath(cursed)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Empath,
|
||||||
|
got: self.role_title(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn empath_mut<'a>(&'a mut self) -> Result<EmpathMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Empath { cursed } => Ok(EmpathMut(cursed)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Empath,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn initial_shown_role(&self) -> RoleTitle {
|
||||||
|
self.role.initial_shown_role()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! decl_ref_and_mut {
|
||||||
|
($($name:ident, $name_mut:ident: $contains:ty;)*) => {
|
||||||
|
$(
|
||||||
|
pub struct $name<'a>(&'a $contains);
|
||||||
|
impl core::ops::Deref for $name<'_> {
|
||||||
|
type Target = $contains;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub struct $name_mut<'a>(&'a mut $contains);
|
||||||
|
impl core::ops::Deref for $name_mut<'_> {
|
||||||
|
type Target = $contains;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl core::ops::DerefMut for $name_mut<'_> {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)*
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
decl_ref_and_mut!(
|
||||||
|
Hunter, HunterMut: Option<CharacterId>;
|
||||||
|
Shapeshifter, ShapeshifterMut: Option<CharacterId>;
|
||||||
|
Scapegoat, ScapegoatMut: bool;
|
||||||
|
Empath, EmpathMut: bool;
|
||||||
|
);
|
||||||
|
|
||||||
|
pub struct MasonLeader<'a>(&'a u8, &'a [CharacterId]);
|
||||||
|
impl MasonLeader<'_> {
|
||||||
|
pub const fn remaining_recruits(&self) -> u8 {
|
||||||
|
*self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn recruits(&self) -> usize {
|
||||||
|
self.1.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct MasonLeaderMut<'a>(&'a mut u8, &'a mut Box<[CharacterId]>);
|
||||||
|
impl MasonLeaderMut<'_> {
|
||||||
|
pub const fn remaining_recruits(&self) -> u8 {
|
||||||
|
*self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recruit(self, target: CharacterId) {
|
||||||
|
let mut recruits = self.1.to_vec();
|
||||||
|
recruits.push(target);
|
||||||
|
*self.1 = recruits.into_boxed_slice();
|
||||||
|
if let Some(new) = self.0.checked_sub(1) {
|
||||||
|
*self.0 = new;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
use core::{fmt::Debug, num::NonZeroU8};
|
use core::{fmt::Debug, num::NonZeroU8};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::Extract;
|
use werewolves_macros::Titles;
|
||||||
|
|
||||||
use crate::{game::DateTime, player::CharacterId};
|
use crate::{character::CharacterId, game::DateTime};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Extract)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
||||||
pub enum DiedTo {
|
pub enum DiedTo {
|
||||||
Execution {
|
Execution {
|
||||||
day: NonZeroU8,
|
day: NonZeroU8,
|
||||||
},
|
},
|
||||||
#[extract(source as killer)]
|
|
||||||
MapleWolf {
|
MapleWolf {
|
||||||
source: CharacterId,
|
source: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
|
|
@ -19,17 +18,14 @@ pub enum DiedTo {
|
||||||
MapleWolfStarved {
|
MapleWolfStarved {
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
#[extract(killer as killer)]
|
|
||||||
Militia {
|
Militia {
|
||||||
killer: CharacterId,
|
killer: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
#[extract(killing_wolf as killer)]
|
|
||||||
Wolfpack {
|
Wolfpack {
|
||||||
killing_wolf: CharacterId,
|
killing_wolf: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
#[extract(killer as killer)]
|
|
||||||
AlphaWolf {
|
AlphaWolf {
|
||||||
killer: CharacterId,
|
killer: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
|
|
@ -38,12 +34,10 @@ pub enum DiedTo {
|
||||||
into: CharacterId,
|
into: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
#[extract(killer as killer)]
|
|
||||||
Hunter {
|
Hunter {
|
||||||
killer: CharacterId,
|
killer: CharacterId,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
#[extract(source as killer)]
|
|
||||||
GuardianProtecting {
|
GuardianProtecting {
|
||||||
source: CharacterId,
|
source: CharacterId,
|
||||||
protecting: CharacterId,
|
protecting: CharacterId,
|
||||||
|
|
@ -51,13 +45,44 @@ pub enum DiedTo {
|
||||||
protecting_from_cause: Box<DiedTo>,
|
protecting_from_cause: Box<DiedTo>,
|
||||||
night: NonZeroU8,
|
night: NonZeroU8,
|
||||||
},
|
},
|
||||||
|
PyreMaster {
|
||||||
|
killer: CharacterId,
|
||||||
|
night: NonZeroU8,
|
||||||
|
},
|
||||||
|
MasonLeaderRecruitFail {
|
||||||
|
tried_recruiting: CharacterId,
|
||||||
|
night: u8,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DiedTo {
|
impl DiedTo {
|
||||||
|
pub const fn killer(&self) -> Option<CharacterId> {
|
||||||
|
match self {
|
||||||
|
DiedTo::Execution { .. }
|
||||||
|
| DiedTo::MapleWolfStarved { .. }
|
||||||
|
| DiedTo::Shapeshift { .. } => None,
|
||||||
|
DiedTo::MapleWolf { source: killer, .. }
|
||||||
|
| DiedTo::Militia { killer, .. }
|
||||||
|
| DiedTo::Wolfpack {
|
||||||
|
killing_wolf: killer,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| DiedTo::AlphaWolf { killer, .. }
|
||||||
|
| DiedTo::Hunter { killer, .. }
|
||||||
|
| DiedTo::GuardianProtecting {
|
||||||
|
protecting_from: killer,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| DiedTo::MasonLeaderRecruitFail {
|
||||||
|
tried_recruiting: killer,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| DiedTo::PyreMaster { killer, .. } => Some(*killer),
|
||||||
|
}
|
||||||
|
}
|
||||||
pub const fn date_time(&self) -> DateTime {
|
pub const fn date_time(&self) -> DateTime {
|
||||||
match self {
|
match self {
|
||||||
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
||||||
|
|
||||||
DiedTo::GuardianProtecting {
|
DiedTo::GuardianProtecting {
|
||||||
source: _,
|
source: _,
|
||||||
protecting: _,
|
protecting: _,
|
||||||
|
|
@ -78,9 +103,11 @@ impl DiedTo {
|
||||||
}
|
}
|
||||||
| DiedTo::AlphaWolf { killer: _, night }
|
| DiedTo::AlphaWolf { killer: _, night }
|
||||||
| DiedTo::Shapeshift { into: _, night }
|
| DiedTo::Shapeshift { into: _, night }
|
||||||
|
| DiedTo::PyreMaster { night, .. }
|
||||||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
||||||
number: night.get(),
|
number: night.get(),
|
||||||
},
|
},
|
||||||
|
DiedTo::MasonLeaderRecruitFail { night, .. } => DateTime::Night { number: *night },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,11 @@ use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Village, night::NightChange},
|
game::{Village, night::NightChange},
|
||||||
player::{CharacterId, Protection},
|
player::Protection,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
|
@ -24,10 +25,7 @@ impl KillOutcome {
|
||||||
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
|
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
|
||||||
match self {
|
match self {
|
||||||
KillOutcome::Single(character_id, died_to) => {
|
KillOutcome::Single(character_id, died_to) => {
|
||||||
village
|
village.character_by_id_mut(character_id)?.kill(died_to);
|
||||||
.character_by_id_mut(character_id)
|
|
||||||
.ok_or(GameError::InvalidTarget)?
|
|
||||||
.kill(died_to);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
KillOutcome::Guarding {
|
KillOutcome::Guarding {
|
||||||
|
|
@ -39,12 +37,9 @@ impl KillOutcome {
|
||||||
} => {
|
} => {
|
||||||
// check if guardian exists before we mutably borrow killer, which would
|
// check if guardian exists before we mutably borrow killer, which would
|
||||||
// prevent us from borrowing village to check after.
|
// prevent us from borrowing village to check after.
|
||||||
|
village.character_by_id(guardian)?;
|
||||||
village
|
village
|
||||||
.character_by_id(guardian)
|
.character_by_id_mut(original_killer)?
|
||||||
.ok_or(GameError::InvalidTarget)?;
|
|
||||||
village
|
|
||||||
.character_by_id_mut(original_killer)
|
|
||||||
.ok_or(GameError::InvalidTarget)?
|
|
||||||
.kill(DiedTo::GuardianProtecting {
|
.kill(DiedTo::GuardianProtecting {
|
||||||
night,
|
night,
|
||||||
source: guardian,
|
source: guardian,
|
||||||
|
|
@ -52,10 +47,7 @@ impl KillOutcome {
|
||||||
protecting_from: original_killer,
|
protecting_from: original_killer,
|
||||||
protecting_from_cause: Box::new(original_kill.clone()),
|
protecting_from_cause: Box::new(original_kill.clone()),
|
||||||
});
|
});
|
||||||
village
|
village.character_by_id_mut(guardian)?.kill(original_kill);
|
||||||
.character_by_id_mut(guardian)
|
|
||||||
.ok_or(GameError::InvalidTarget)?
|
|
||||||
.kill(original_kill);
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -84,6 +76,7 @@ fn resolve_protection(
|
||||||
source: _,
|
source: _,
|
||||||
guarding: false,
|
guarding: false,
|
||||||
}
|
}
|
||||||
|
| Protection::Vindicator { .. }
|
||||||
| Protection::Protector { source: _ } => None,
|
| Protection::Protector { source: _ } => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,9 +107,7 @@ pub fn resolve_kill(
|
||||||
} = died_to
|
} = died_to
|
||||||
&& let Some(ss_source) = changes.shapeshifter()
|
&& let Some(ss_source) = changes.shapeshifter()
|
||||||
{
|
{
|
||||||
let killing_wolf = village
|
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
||||||
.character_by_id(*killing_wolf)
|
|
||||||
.ok_or(GameError::InvalidTarget)?;
|
|
||||||
|
|
||||||
match changes.protected_take(target) {
|
match changes.protected_take(target) {
|
||||||
Some(protection) => {
|
Some(protection) => {
|
||||||
|
|
@ -151,7 +142,7 @@ pub fn resolve_kill(
|
||||||
source,
|
source,
|
||||||
guarding: true,
|
guarding: true,
|
||||||
} => Ok(Some(KillOutcome::Guarding {
|
} => Ok(Some(KillOutcome::Guarding {
|
||||||
original_killer: *died_to
|
original_killer: died_to
|
||||||
.killer()
|
.killer()
|
||||||
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
||||||
original_target: *target,
|
original_target: *target,
|
||||||
|
|
@ -160,10 +151,10 @@ pub fn resolve_kill(
|
||||||
night: NonZeroU8::new(night).unwrap(),
|
night: NonZeroU8::new(night).unwrap(),
|
||||||
})),
|
})),
|
||||||
Protection::Guardian {
|
Protection::Guardian {
|
||||||
source: _,
|
guarding: false, ..
|
||||||
guarding: false,
|
|
||||||
}
|
}
|
||||||
| Protection::Protector { source: _ } => Ok(None),
|
| Protection::Vindicator { .. }
|
||||||
|
| Protection::Protector { .. } => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,13 +13,13 @@ use rand::{Rng, seq::SliceRandom};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{Night, ServerAction},
|
game::night::{Night, ServerAction},
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification,
|
CharacterState, Identification,
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
},
|
},
|
||||||
player::CharacterId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use {
|
pub use {
|
||||||
|
|
@ -98,7 +98,7 @@ impl Game {
|
||||||
.map(|c| CharacterState {
|
.map(|c| CharacterState {
|
||||||
player_id: c.player_id(),
|
player_id: c.player_id(),
|
||||||
identity: c.identity(),
|
identity: c.identity(),
|
||||||
role: c.role().title(),
|
role: c.role_title(),
|
||||||
died_to: c.died_to().cloned(),
|
died_to: c.died_to().cloned(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ use werewolves_macros::Extract;
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
|
|
@ -13,8 +14,8 @@ use crate::{
|
||||||
kill::{self, ChangesLookup},
|
kill::{self, ChangesLookup},
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
player::{Character, CharacterId, Protection},
|
player::Protection,
|
||||||
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
role::{PreviousGuardianAction, RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Extract)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Extract)]
|
||||||
|
|
@ -43,6 +44,14 @@ pub enum NightChange {
|
||||||
ElderReveal {
|
ElderReveal {
|
||||||
elder: CharacterId,
|
elder: CharacterId,
|
||||||
},
|
},
|
||||||
|
MasonRecruit {
|
||||||
|
mason_leader: CharacterId,
|
||||||
|
recruiting: CharacterId,
|
||||||
|
},
|
||||||
|
EmpathFoundScapegoat {
|
||||||
|
empath: CharacterId,
|
||||||
|
scapegoat: CharacterId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
enum BlockResolvedOutcome {
|
enum BlockResolvedOutcome {
|
||||||
|
|
@ -58,7 +67,134 @@ enum ResponseOutcome {
|
||||||
struct ActionComplete {
|
struct ActionComplete {
|
||||||
pub result: ActionResult,
|
pub result: ActionResult,
|
||||||
pub change: Option<NightChange>,
|
pub change: Option<NightChange>,
|
||||||
pub unless: Option<Unless>,
|
}
|
||||||
|
|
||||||
|
impl From<ActionComplete> for ResponseOutcome {
|
||||||
|
fn from(value: ActionComplete) -> Self {
|
||||||
|
ResponseOutcome::ActionComplete(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionPrompt {
|
||||||
|
fn unless(&self) -> Option<Unless> {
|
||||||
|
match &self {
|
||||||
|
ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness => None,
|
||||||
|
|
||||||
|
ActionPrompt::Arcanist {
|
||||||
|
marked: (Some(marked1), Some(marked2)),
|
||||||
|
..
|
||||||
|
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
||||||
|
|
||||||
|
ActionPrompt::Seer {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Protector {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Gravedigger {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Hunter {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Militia {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::MapleWolf {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Guardian {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Adjudicator {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::PowerSeer {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Mortician {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Beholder {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::MasonLeaderRecruit {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Empath {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Vindicator {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::PyreMaster {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::WolfPackKill {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::AlphaWolf {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::DireWolf {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Some(Unless::TargetBlocked(*marked)),
|
||||||
|
|
||||||
|
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::WolfPackKill { marked: None, .. }
|
||||||
|
| ActionPrompt::AlphaWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::DireWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::Arcanist {
|
||||||
|
marked: (Some(_), None),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Arcanist {
|
||||||
|
marked: (None, Some(_)),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Arcanist {
|
||||||
|
marked: (None, None),
|
||||||
|
..
|
||||||
|
} => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ActionComplete {
|
impl Default for ActionComplete {
|
||||||
|
|
@ -66,7 +202,6 @@ impl Default for ActionComplete {
|
||||||
Self {
|
Self {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
unless: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,7 +234,7 @@ pub struct Night {
|
||||||
village: Village,
|
village: Village,
|
||||||
night: u8,
|
night: u8,
|
||||||
action_queue: VecDeque<ActionPrompt>,
|
action_queue: VecDeque<ActionPrompt>,
|
||||||
used_actions: Vec<ActionPrompt>,
|
used_actions: Vec<(ActionPrompt, ActionResult)>,
|
||||||
changes: Vec<NightChange>,
|
changes: Vec<NightChange>,
|
||||||
night_state: NightState,
|
night_state: NightState,
|
||||||
}
|
}
|
||||||
|
|
@ -122,7 +257,7 @@ impl Night {
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(filter)
|
.filter(filter)
|
||||||
.map(|c| c.night_action_prompt(&village))
|
.map(|c| c.night_action_prompts(&village))
|
||||||
.collect::<Result<Box<[_]>>>()?
|
.collect::<Result<Box<[_]>>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
|
|
@ -143,7 +278,7 @@ impl Night {
|
||||||
wolves: village
|
wolves: village
|
||||||
.living_wolf_pack_players()
|
.living_wolf_pack_players()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|w| (w.identity(), w.role().title()))
|
.map(|w| (w.identity(), w.role_title()))
|
||||||
.collect(),
|
.collect(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -177,10 +312,7 @@ impl Night {
|
||||||
.dead_characters()
|
.dead_characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||||
.filter_map(|(c, d)| match c.role() {
|
.filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d)))
|
||||||
Role::Hunter { target } => (*target).map(|t| (c, t, d)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.filter_map(|(c, t, d)| match d.date_time() {
|
.filter_map(|(c, t, d)| match d.date_time() {
|
||||||
DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)),
|
DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)),
|
||||||
DateTime::Night { number: _ } => None,
|
DateTime::Night { number: _ } => None,
|
||||||
|
|
@ -199,7 +331,8 @@ impl Night {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_state(&mut self) -> Result<()> {
|
pub fn previous_state(&mut self) -> Result<()> {
|
||||||
let prev_act = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
|
return Err(GameError::NoPreviousState);
|
||||||
|
let (prev_act, prev_result) = self.used_actions.pop().ok_or(GameError::NoPreviousState)?;
|
||||||
log::info!("loading previous prompt: {prev_act:?}");
|
log::info!("loading previous prompt: {prev_act:?}");
|
||||||
match &self.night_state {
|
match &self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
|
|
@ -246,20 +379,15 @@ impl Night {
|
||||||
let mut changes = ChangesLookup::new(&self.changes);
|
let mut changes = ChangesLookup::new(&self.changes);
|
||||||
for change in self.changes.iter() {
|
for change in self.changes.iter() {
|
||||||
match change {
|
match change {
|
||||||
NightChange::ElderReveal { elder } => new_village
|
NightChange::ElderReveal { elder } => {
|
||||||
.character_by_id_mut(*elder)
|
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
||||||
.ok_or(GameError::InvalidTarget)?
|
}
|
||||||
.elder_reveal(),
|
|
||||||
NightChange::RoleChange(character_id, role_title) => new_village
|
NightChange::RoleChange(character_id, role_title) => new_village
|
||||||
.character_by_id_mut(*character_id)
|
.character_by_id_mut(*character_id)?
|
||||||
.ok_or(GameError::InvalidTarget)?
|
|
||||||
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
||||||
NightChange::HunterTarget { source, target } => {
|
NightChange::HunterTarget { source, target } => {
|
||||||
if let Role::Hunter { target: t } =
|
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
||||||
new_village.character_by_id_mut(*source).unwrap().role_mut()
|
hunter_character.hunter_mut()?.replace(*target);
|
||||||
{
|
|
||||||
t.replace(*target);
|
|
||||||
}
|
|
||||||
if changes.killed(source).is_some()
|
if changes.killed(source).is_some()
|
||||||
&& changes.protected(source).is_none()
|
&& changes.protected(source).is_none()
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(target).is_none()
|
||||||
|
|
@ -289,12 +417,7 @@ impl Night {
|
||||||
&& changes.protected(target).is_none()
|
&& changes.protected(target).is_none()
|
||||||
{
|
{
|
||||||
let ss = new_village.character_by_id_mut(*source).unwrap();
|
let ss = new_village.character_by_id_mut(*source).unwrap();
|
||||||
match ss.role_mut() {
|
ss.shapeshifter_mut().unwrap().replace(*target);
|
||||||
Role::Shapeshifter { shifted_into } => {
|
|
||||||
*shifted_into = Some(*target)
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
}
|
|
||||||
ss.kill(DiedTo::Shapeshift {
|
ss.kill(DiedTo::Shapeshift {
|
||||||
into: *target,
|
into: *target,
|
||||||
night: NonZeroU8::new(self.night).unwrap(),
|
night: NonZeroU8::new(self.night).unwrap(),
|
||||||
|
|
@ -311,6 +434,30 @@ impl Night {
|
||||||
target: _,
|
target: _,
|
||||||
protection: _,
|
protection: _,
|
||||||
} => {}
|
} => {}
|
||||||
|
NightChange::MasonRecruit {
|
||||||
|
mason_leader,
|
||||||
|
recruiting,
|
||||||
|
} => {
|
||||||
|
if new_village.character_by_id(*recruiting)?.is_wolf() {
|
||||||
|
new_village.character_by_id_mut(*mason_leader)?.kill(
|
||||||
|
DiedTo::MasonLeaderRecruitFail {
|
||||||
|
tried_recruiting: *recruiting,
|
||||||
|
night: self.night,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*mason_leader)?
|
||||||
|
.mason_leader_mut()?
|
||||||
|
.recruit(*recruiting);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NightChange::EmpathFoundScapegoat { empath, scapegoat } => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*scapegoat)?
|
||||||
|
.role_change(RoleTitle::Villager, DateTime::Night { number: self.night })?;
|
||||||
|
*new_village.character_by_id_mut(*empath)?.empath_mut()? = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if new_village.is_game_over().is_none() {
|
if new_village.is_game_over().is_none() {
|
||||||
|
|
@ -319,6 +466,34 @@ impl Night {
|
||||||
Ok(new_village)
|
Ok(new_village)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn apply_mason_recruit(
|
||||||
|
&mut self,
|
||||||
|
mason_leader: CharacterId,
|
||||||
|
recruiting: CharacterId,
|
||||||
|
) -> Result<ActionResult> {
|
||||||
|
if self.village.character_by_id(recruiting)?.is_village() {
|
||||||
|
if let Some(masons) = self.action_queue.iter_mut().find_map(|a| match a {
|
||||||
|
ActionPrompt::MasonsWake {
|
||||||
|
character_id,
|
||||||
|
masons,
|
||||||
|
} => (character_id.character_id == mason_leader).then_some(masons),
|
||||||
|
_ => None,
|
||||||
|
}) {
|
||||||
|
let mut ext_masons = masons.to_vec();
|
||||||
|
ext_masons.push(self.village.character_by_id(recruiting)?.identity());
|
||||||
|
*masons = ext_masons.into_boxed_slice();
|
||||||
|
} else {
|
||||||
|
self.action_queue.push_front(ActionPrompt::MasonsWake {
|
||||||
|
character_id: self.village.character_by_id(mason_leader)?.identity(),
|
||||||
|
masons: Box::new([self.village.character_by_id(recruiting)?.identity()]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Ok(ActionResult::Continue)
|
||||||
|
} else {
|
||||||
|
Ok(ActionResult::GoBackToSleep)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
|
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
|
||||||
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
|
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
|
||||||
NightChange::Kill {
|
NightChange::Kill {
|
||||||
|
|
@ -362,16 +537,11 @@ impl Night {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
self.changes.push(NightChange::Shapeshift {
|
self.changes
|
||||||
source: *source,
|
.push(NightChange::Shapeshift { source: *source });
|
||||||
});
|
|
||||||
self.action_queue.push_front(ActionPrompt::RoleChange {
|
self.action_queue.push_front(ActionPrompt::RoleChange {
|
||||||
new_role: RoleTitle::Werewolf,
|
new_role: RoleTitle::Werewolf,
|
||||||
character_id: self
|
character_id: self.village.character_by_id(kill_target)?.identity(),
|
||||||
.village
|
|
||||||
.character_by_id(kill_target)
|
|
||||||
.ok_or(GameError::NoMatchingCharacterFound)?
|
|
||||||
.identity(),
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Remove any further shapeshift prompts from the queue
|
// Remove any further shapeshift prompts from the queue
|
||||||
|
|
@ -399,7 +569,7 @@ impl Night {
|
||||||
}
|
}
|
||||||
NightState::Complete => Err(GameError::NightOver),
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
},
|
},
|
||||||
BlockResolvedOutcome::ActionComplete(result, Some(change)) => {
|
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
||||||
match &mut self.night_state {
|
match &mut self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt: _,
|
current_prompt: _,
|
||||||
|
|
@ -419,6 +589,13 @@ impl Night {
|
||||||
.unwrap_or(ActionResult::GoBackToSleep),
|
.unwrap_or(ActionResult::GoBackToSleep),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
if let NightChange::MasonRecruit {
|
||||||
|
mason_leader,
|
||||||
|
recruiting,
|
||||||
|
} = &change
|
||||||
|
{
|
||||||
|
result = self.apply_mason_recruit(*mason_leader, *recruiting)?;
|
||||||
|
}
|
||||||
self.changes.push(change);
|
self.changes.push(change);
|
||||||
Ok(ServerAction::Result(result))
|
Ok(ServerAction::Result(result))
|
||||||
}
|
}
|
||||||
|
|
@ -461,7 +638,6 @@ impl Night {
|
||||||
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change: None,
|
change: None,
|
||||||
unless: None,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -475,27 +651,23 @@ impl Night {
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: Some(NightChange::Shapeshift { source }),
|
change: Some(NightChange::Shapeshift { source }),
|
||||||
unless,
|
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
_,
|
_,
|
||||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change: Some(NightChange::Shapeshift { source }),
|
change: Some(NightChange::Shapeshift { source }),
|
||||||
unless,
|
|
||||||
})),
|
})),
|
||||||
(
|
(
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change,
|
change,
|
||||||
unless,
|
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change,
|
change,
|
||||||
unless,
|
|
||||||
})),
|
})),
|
||||||
(outcome, _, _) => Ok(outcome),
|
(outcome, _, _) => Ok(outcome),
|
||||||
}
|
}
|
||||||
|
|
@ -507,11 +679,9 @@ impl Night {
|
||||||
) -> Result<BlockResolvedOutcome> {
|
) -> Result<BlockResolvedOutcome> {
|
||||||
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
|
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
|
||||||
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
|
||||||
result,
|
match self.current_prompt().ok_or(GameError::NightOver)?.unless() {
|
||||||
change,
|
Some(Unless::TargetBlocked(unless_blocked)) => {
|
||||||
unless: Some(Unless::TargetBlocked(unless_blocked)),
|
|
||||||
}) => {
|
|
||||||
if self.changes.iter().any(|c| match c {
|
if self.changes.iter().any(|c| match c {
|
||||||
NightChange::RoleBlock {
|
NightChange::RoleBlock {
|
||||||
source: _,
|
source: _,
|
||||||
|
|
@ -528,11 +698,7 @@ impl Night {
|
||||||
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
|
||||||
result,
|
|
||||||
change,
|
|
||||||
unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)),
|
|
||||||
}) => {
|
|
||||||
if self.changes.iter().any(|c| match c {
|
if self.changes.iter().any(|c| match c {
|
||||||
NightChange::RoleBlock {
|
NightChange::RoleBlock {
|
||||||
source: _,
|
source: _,
|
||||||
|
|
@ -549,12 +715,9 @@ impl Night {
|
||||||
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
None => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
}
|
||||||
result,
|
}
|
||||||
change,
|
|
||||||
unless: None,
|
|
||||||
}) => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -586,7 +749,6 @@ impl Night {
|
||||||
change: Some(NightChange::Shapeshift {
|
change: Some(NightChange::Shapeshift {
|
||||||
source: source.character_id,
|
source: source.character_id,
|
||||||
}),
|
}),
|
||||||
unless: None,
|
|
||||||
})),
|
})),
|
||||||
_ => Err(GameError::InvalidMessageForGameState),
|
_ => Err(GameError::InvalidMessageForGameState),
|
||||||
};
|
};
|
||||||
|
|
@ -603,7 +765,6 @@ impl Night {
|
||||||
character_id.character_id,
|
character_id.character_id,
|
||||||
*new_role,
|
*new_role,
|
||||||
)),
|
)),
|
||||||
unless: None,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -616,7 +777,6 @@ impl Night {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
unless: None,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::ElderReveal { character_id } => {
|
ActionPrompt::ElderReveal { character_id } => {
|
||||||
|
|
@ -625,22 +785,16 @@ impl Night {
|
||||||
change: Some(NightChange::ElderReveal {
|
change: Some(NightChange::ElderReveal {
|
||||||
elder: character_id.character_id,
|
elder: character_id.character_id,
|
||||||
}),
|
}),
|
||||||
unless: None,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Seer {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let alignment = self
|
let alignment = self.village.character_by_id(*marked)?.alignment();
|
||||||
.village
|
|
||||||
.character_by_id(*marked)
|
|
||||||
.ok_or(GameError::InvalidTarget)?
|
|
||||||
.alignment();
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Seer(alignment),
|
result: ActionResult::Seer(alignment),
|
||||||
change: None,
|
change: None,
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Protector {
|
ActionPrompt::Protector {
|
||||||
|
|
@ -655,42 +809,27 @@ impl Night {
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Arcanist {
|
ActionPrompt::Arcanist {
|
||||||
marked: (Some(marked1), Some(marked2)),
|
marked: (Some(marked1), Some(marked2)),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let same = self
|
let same = self.village.character_by_id(*marked1)?.alignment()
|
||||||
.village
|
== self.village.character_by_id(*marked2)?.alignment();
|
||||||
.character_by_id(*marked1)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?
|
|
||||||
.alignment()
|
|
||||||
== self
|
|
||||||
.village
|
|
||||||
.character_by_id(*marked2)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?
|
|
||||||
.alignment();
|
|
||||||
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Arcanist { same },
|
result: ActionResult::Arcanist { same },
|
||||||
change: None,
|
change: None,
|
||||||
unless: Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Gravedigger {
|
ActionPrompt::Gravedigger {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let dig_role = self
|
let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig();
|
||||||
.village
|
|
||||||
.character_by_id(*marked)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?
|
|
||||||
.gravedigger_dig();
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GraveDigger(dig_role),
|
result: ActionResult::GraveDigger(dig_role),
|
||||||
change: None,
|
change: None,
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Hunter {
|
ActionPrompt::Hunter {
|
||||||
|
|
@ -703,7 +842,6 @@ impl Night {
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
target: *marked,
|
target: *marked,
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Militia {
|
ActionPrompt::Militia {
|
||||||
character_id,
|
character_id,
|
||||||
|
|
@ -719,7 +857,6 @@ impl Night {
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Militia { marked: None, .. } => {
|
ActionPrompt::Militia { marked: None, .. } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
|
@ -740,7 +877,6 @@ impl Night {
|
||||||
starves_if_fails: *kill_or_die,
|
starves_if_fails: *kill_or_die,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::MapleWolf { marked: None, .. } => {
|
ActionPrompt::MapleWolf { marked: None, .. } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
|
@ -759,7 +895,6 @@ impl Night {
|
||||||
guarding: false,
|
guarding: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
character_id,
|
character_id,
|
||||||
|
|
@ -779,7 +914,6 @@ impl Night {
|
||||||
guarding: false,
|
guarding: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
|
|
@ -796,7 +930,6 @@ impl Night {
|
||||||
guarding: prev_protect.character_id == *marked,
|
guarding: prev_protect.character_id == *marked,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::WolfPackKill {
|
ActionPrompt::WolfPackKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -815,7 +948,6 @@ impl Night {
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Shapeshifter { character_id } => {
|
ActionPrompt::Shapeshifter { character_id } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
|
@ -823,7 +955,6 @@ impl Night {
|
||||||
change: Some(NightChange::Shapeshift {
|
change: Some(NightChange::Shapeshift {
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
}),
|
}),
|
||||||
unless: None,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::AlphaWolf {
|
ActionPrompt::AlphaWolf {
|
||||||
|
|
@ -840,7 +971,6 @@ impl Night {
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
|
@ -856,10 +986,139 @@ impl Night {
|
||||||
target: *marked,
|
target: *marked,
|
||||||
block_type: RoleBlock::Direwolf,
|
block_type: RoleBlock::Direwolf,
|
||||||
}),
|
}),
|
||||||
unless: Some(Unless::TargetBlocked(*marked)),
|
|
||||||
})),
|
})),
|
||||||
|
ActionPrompt::Adjudicator {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::Adjudicator {
|
||||||
|
killer: self.village.character_by_id(*marked)?.killer(),
|
||||||
|
},
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::PowerSeer {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::PowerSeer {
|
||||||
|
powerful: self.village.character_by_id(*marked)?.powerful(),
|
||||||
|
},
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::Mortician {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::Mortician(
|
||||||
|
self.village
|
||||||
|
.character_by_id(*marked)?
|
||||||
|
.died_to()
|
||||||
|
.ok_or(GameError::InvalidTarget)?
|
||||||
|
.title(),
|
||||||
|
),
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::Beholder {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result)| {
|
||||||
|
prompt.matches_beholding(*marked).then_some(result)
|
||||||
|
}) {
|
||||||
|
Ok(ActionComplete {
|
||||||
|
result: result.clone(),
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
} else {
|
||||||
|
Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionPrompt::MasonsWake { .. } => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::MasonLeaderRecruit {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::Continue,
|
||||||
|
change: Some(NightChange::MasonRecruit {
|
||||||
|
mason_leader: character_id.character_id,
|
||||||
|
recruiting: *marked,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::Empath {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let marked = self.village.character_by_id(*marked)?;
|
||||||
|
let scapegoat = marked.role_title() != RoleTitle::Scapegoat;
|
||||||
|
|
||||||
ActionPrompt::Protector { marked: None, .. }
|
Ok(ActionComplete {
|
||||||
|
result: ActionResult::Empath { scapegoat },
|
||||||
|
change: scapegoat.then(|| NightChange::EmpathFoundScapegoat {
|
||||||
|
empath: character_id.character_id,
|
||||||
|
scapegoat: marked.character_id(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
ActionPrompt::Vindicator {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Protection {
|
||||||
|
target: *marked,
|
||||||
|
protection: Protection::Vindicator {
|
||||||
|
source: character_id.character_id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::PyreMaster {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: NonZeroU8::new(self.night).map(|night| NightChange::Kill {
|
||||||
|
target: *marked,
|
||||||
|
died_to: DiedTo::PyreMaster {
|
||||||
|
killer: character_id.character_id,
|
||||||
|
night,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
|
||||||
|
ActionPrompt::PyreMaster { marked: None, .. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
|
||||||
|
ActionPrompt::Adjudicator { marked: None, .. }
|
||||||
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
||||||
|
| ActionPrompt::Mortician { marked: None, .. }
|
||||||
|
| ActionPrompt::Beholder { marked: None, .. }
|
||||||
|
| ActionPrompt::Empath { marked: None, .. }
|
||||||
|
| ActionPrompt::Vindicator { marked: None, .. }
|
||||||
|
| ActionPrompt::Protector { marked: None, .. }
|
||||||
| ActionPrompt::Arcanist {
|
| ActionPrompt::Arcanist {
|
||||||
marked: (None, None),
|
marked: (None, None),
|
||||||
..
|
..
|
||||||
|
|
@ -923,6 +1182,15 @@ impl Night {
|
||||||
| ActionPrompt::Guardian { character_id, .. }
|
| ActionPrompt::Guardian { character_id, .. }
|
||||||
| ActionPrompt::Shapeshifter { character_id }
|
| ActionPrompt::Shapeshifter { character_id }
|
||||||
| ActionPrompt::AlphaWolf { character_id, .. }
|
| ActionPrompt::AlphaWolf { character_id, .. }
|
||||||
|
| ActionPrompt::Adjudicator { character_id, .. }
|
||||||
|
| ActionPrompt::PowerSeer { character_id, .. }
|
||||||
|
| ActionPrompt::Mortician { character_id, .. }
|
||||||
|
| ActionPrompt::Beholder { character_id, .. }
|
||||||
|
| ActionPrompt::MasonsWake { character_id, .. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { character_id, .. }
|
||||||
|
| ActionPrompt::Empath { character_id, .. }
|
||||||
|
| ActionPrompt::Vindicator { 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::WolfPackKill { .. }
|
| ActionPrompt::WolfPackKill { .. }
|
||||||
|
|
@ -934,7 +1202,7 @@ impl Night {
|
||||||
|
|
||||||
pub fn current_character(&self) -> Option<&Character> {
|
pub fn current_character(&self) -> Option<&Character> {
|
||||||
self.current_character_id()
|
self.current_character_id()
|
||||||
.and_then(|id| self.village.character_by_id(id))
|
.and_then(|id| self.village.character_by_id(id).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn complete(&self) -> bool {
|
pub const fn complete(&self) -> bool {
|
||||||
|
|
@ -944,9 +1212,12 @@ impl Night {
|
||||||
pub fn next(&mut self) -> Result<()> {
|
pub fn next(&mut self) -> Result<()> {
|
||||||
match &self.night_state {
|
match &self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt: _,
|
current_prompt,
|
||||||
current_result: Some(_),
|
current_result: Some(result),
|
||||||
} => {}
|
} => {
|
||||||
|
self.used_actions
|
||||||
|
.push((current_prompt.clone(), result.clone()));
|
||||||
|
}
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt: _,
|
current_prompt: _,
|
||||||
current_result: None,
|
current_result: None,
|
||||||
|
|
@ -954,7 +1225,6 @@ 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() {
|
||||||
self.used_actions.push(prompt.clone());
|
|
||||||
self.night_state = NightState::Active {
|
self.night_state = NightState::Active {
|
||||||
current_prompt: prompt,
|
current_prompt: prompt,
|
||||||
current_result: None,
|
current_result: None,
|
||||||
|
|
@ -977,7 +1247,7 @@ pub enum ServerAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
mod filter {
|
mod filter {
|
||||||
use crate::player::Character;
|
use crate::character::Character;
|
||||||
|
|
||||||
pub fn no_filter(_: &Character) -> bool {
|
pub fn no_filter(_: &Character) -> bool {
|
||||||
true
|
true
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ use super::Result;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{error::GameError, message::Identification, player::Character, role::RoleTitle};
|
use crate::{character::Character, error::GameError, message::Identification, role::RoleTitle};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct GameSettings {
|
pub struct GameSettings {
|
||||||
|
|
@ -212,9 +212,7 @@ impl GameSettings {
|
||||||
SetupRole::Apprentice { to: None } => (mentor_count > 0)
|
SetupRole::Apprentice { to: None } => (mentor_count > 0)
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or(GameError::NoApprenticeMentor),
|
.ok_or(GameError::NoApprenticeMentor),
|
||||||
SetupRole::Apprentice {
|
SetupRole::Apprentice { to: Some(role) } => role
|
||||||
to: Some(role),
|
|
||||||
} => role
|
|
||||||
.is_mentor()
|
.is_mentor()
|
||||||
.then_some(())
|
.then_some(())
|
||||||
.ok_or(GameError::NotAMentor(*role)),
|
.ok_or(GameError::NotAMentor(*role)),
|
||||||
|
|
|
||||||
|
|
@ -9,10 +9,11 @@ use uuid::Uuid;
|
||||||
use werewolves_macros::{All, ChecksAs, Titles};
|
use werewolves_macros::{All, ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::Character,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
message::Identification,
|
message::Identification,
|
||||||
modifier::Modifier,
|
modifier::Modifier,
|
||||||
player::{Character, PlayerId},
|
player::PlayerId,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -110,10 +111,33 @@ pub enum SetupRole {
|
||||||
DireWolf,
|
DireWolf,
|
||||||
#[checks(Category::Wolves)]
|
#[checks(Category::Wolves)]
|
||||||
Shapeshifter,
|
Shapeshifter,
|
||||||
|
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Adjudicator,
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
PowerSeer,
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Mortician,
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Beholder,
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
MasonLeader { recruits_available: NonZeroU8 },
|
||||||
|
#[checks(Category::Intel)]
|
||||||
|
Empath,
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
|
Vindicator,
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
|
Diseased,
|
||||||
|
#[checks(Category::Defensive)]
|
||||||
|
BlackKnight,
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
|
Weightlifter,
|
||||||
|
#[checks(Category::Offensive)]
|
||||||
|
PyreMaster,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SetupRoleTitle {
|
impl SetupRoleTitle {
|
||||||
pub const fn into_role(self) -> Role {
|
pub fn into_role(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
SetupRoleTitle::Villager => Role::Villager,
|
SetupRoleTitle::Villager => Role::Villager,
|
||||||
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
|
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
|
||||||
|
|
@ -141,6 +165,22 @@ impl SetupRoleTitle {
|
||||||
SetupRoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
SetupRoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||||
SetupRoleTitle::DireWolf => Role::DireWolf,
|
SetupRoleTitle::DireWolf => Role::DireWolf,
|
||||||
SetupRoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
SetupRoleTitle::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
||||||
|
SetupRoleTitle::Adjudicator => Role::Adjudicator,
|
||||||
|
SetupRoleTitle::PowerSeer => Role::PowerSeer,
|
||||||
|
SetupRoleTitle::Mortician => Role::Mortician,
|
||||||
|
SetupRoleTitle::Beholder => Role::Beholder,
|
||||||
|
SetupRoleTitle::MasonLeader => Role::MasonLeader {
|
||||||
|
recruits_available: 1,
|
||||||
|
recruits: Box::new([]),
|
||||||
|
},
|
||||||
|
SetupRoleTitle::Empath => Role::Empath { cursed: false },
|
||||||
|
SetupRoleTitle::Vindicator => Role::Vindicator,
|
||||||
|
SetupRoleTitle::Diseased => Role::Diseased,
|
||||||
|
SetupRoleTitle::BlackKnight => Role::BlackKnight { attacked: false },
|
||||||
|
SetupRoleTitle::Weightlifter => Role::Weightlifter,
|
||||||
|
SetupRoleTitle::PyreMaster => Role::PyreMaster {
|
||||||
|
villagers_killed: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -164,6 +204,17 @@ impl Display for SetupRole {
|
||||||
SetupRole::AlphaWolf => "AlphaWolf",
|
SetupRole::AlphaWolf => "AlphaWolf",
|
||||||
SetupRole::DireWolf => "DireWolf",
|
SetupRole::DireWolf => "DireWolf",
|
||||||
SetupRole::Shapeshifter => "Shapeshifter",
|
SetupRole::Shapeshifter => "Shapeshifter",
|
||||||
|
SetupRole::Adjudicator => "Adjudicator",
|
||||||
|
SetupRole::PowerSeer => "PowerSeer",
|
||||||
|
SetupRole::Mortician => "Mortician",
|
||||||
|
SetupRole::Beholder => "Beholder",
|
||||||
|
SetupRole::MasonLeader { .. } => "Mason Leader",
|
||||||
|
SetupRole::Empath => "Empath",
|
||||||
|
SetupRole::Vindicator => "Vindicator",
|
||||||
|
SetupRole::Diseased => "Diseased",
|
||||||
|
SetupRole::BlackKnight => "Black Knight",
|
||||||
|
SetupRole::Weightlifter => "Weightlifter",
|
||||||
|
SetupRole::PyreMaster => "Pyremaster",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -210,6 +261,22 @@ impl SetupRole {
|
||||||
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
SetupRole::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||||
SetupRole::DireWolf => Role::DireWolf,
|
SetupRole::DireWolf => Role::DireWolf,
|
||||||
SetupRole::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
SetupRole::Shapeshifter => Role::Shapeshifter { shifted_into: None },
|
||||||
|
SetupRole::MasonLeader { recruits_available } => Role::MasonLeader {
|
||||||
|
recruits_available: recruits_available.get(),
|
||||||
|
recruits: Box::new([]),
|
||||||
|
},
|
||||||
|
SetupRole::Adjudicator => Role::Adjudicator,
|
||||||
|
SetupRole::PowerSeer => Role::PowerSeer,
|
||||||
|
SetupRole::Mortician => Role::Mortician,
|
||||||
|
SetupRole::Beholder => Role::Beholder,
|
||||||
|
SetupRole::Empath => Role::Empath { cursed: false },
|
||||||
|
SetupRole::Vindicator => Role::Vindicator,
|
||||||
|
SetupRole::Diseased => Role::Diseased,
|
||||||
|
SetupRole::BlackKnight => Role::BlackKnight { attacked: false },
|
||||||
|
SetupRole::Weightlifter => Role::Weightlifter,
|
||||||
|
SetupRole::PyreMaster => Role::PyreMaster {
|
||||||
|
villagers_killed: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -233,6 +300,17 @@ impl From<SetupRole> for RoleTitle {
|
||||||
SetupRole::AlphaWolf => RoleTitle::AlphaWolf,
|
SetupRole::AlphaWolf => RoleTitle::AlphaWolf,
|
||||||
SetupRole::DireWolf => RoleTitle::DireWolf,
|
SetupRole::DireWolf => RoleTitle::DireWolf,
|
||||||
SetupRole::Shapeshifter => RoleTitle::Shapeshifter,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -260,6 +338,19 @@ impl From<RoleTitle> for SetupRole {
|
||||||
RoleTitle::AlphaWolf => SetupRole::AlphaWolf,
|
RoleTitle::AlphaWolf => SetupRole::AlphaWolf,
|
||||||
RoleTitle::DireWolf => SetupRole::DireWolf,
|
RoleTitle::DireWolf => SetupRole::DireWolf,
|
||||||
RoleTitle::Shapeshifter => SetupRole::Shapeshifter,
|
RoleTitle::Shapeshifter => SetupRole::Shapeshifter,
|
||||||
|
RoleTitle::Adjudicator => SetupRole::Adjudicator,
|
||||||
|
RoleTitle::PowerSeer => SetupRole::PowerSeer,
|
||||||
|
RoleTitle::Mortician => SetupRole::Mortician,
|
||||||
|
RoleTitle::Beholder => SetupRole::Beholder,
|
||||||
|
RoleTitle::MasonLeader => SetupRole::MasonLeader {
|
||||||
|
recruits_available: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
RoleTitle::Empath => SetupRole::Empath,
|
||||||
|
RoleTitle::Vindicator => SetupRole::Vindicator,
|
||||||
|
RoleTitle::Diseased => SetupRole::Diseased,
|
||||||
|
RoleTitle::BlackKnight => SetupRole::BlackKnight,
|
||||||
|
RoleTitle::Weightlifter => SetupRole::Weightlifter,
|
||||||
|
RoleTitle::PyreMaster => SetupRole::PyreMaster,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,11 +5,12 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{DateTime, GameOver, GameSettings},
|
game::{DateTime, GameOver, GameSettings},
|
||||||
message::{CharacterIdentity, Identification},
|
message::{CharacterIdentity, Identification},
|
||||||
player::{Character, CharacterId, PlayerId},
|
player::PlayerId,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -43,7 +44,7 @@ impl Village {
|
||||||
{
|
{
|
||||||
let ww = wolves
|
let ww = wolves
|
||||||
.clone()
|
.clone()
|
||||||
.filter(|w| matches!(w.role().title(), RoleTitle::Werewolf))
|
.filter(|w| matches!(w.role_title(), RoleTitle::Werewolf))
|
||||||
.collect::<Box<[_]>>();
|
.collect::<Box<[_]>>();
|
||||||
if !ww.is_empty() {
|
if !ww.is_empty() {
|
||||||
return Some(ww[rand::random_range(0..ww.len())]);
|
return Some(ww[rand::random_range(0..ww.len())]);
|
||||||
|
|
@ -63,12 +64,6 @@ impl Village {
|
||||||
self.date_time
|
self.date_time
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_character_id(&self, character_id: CharacterId) -> Option<&Character> {
|
|
||||||
self.characters
|
|
||||||
.iter()
|
|
||||||
.find(|c| c.character_id() == character_id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn find_by_character_id_mut(
|
pub fn find_by_character_id_mut(
|
||||||
&mut self,
|
&mut self,
|
||||||
character_id: CharacterId,
|
character_id: CharacterId,
|
||||||
|
|
@ -137,18 +132,22 @@ impl Village {
|
||||||
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.role().wolf() && c.alive())
|
.filter(|c| c.is_wolf() && c.alive())
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn killing_wolf_id(&self) -> CharacterId {
|
pub fn killing_wolf_id(&self) -> CharacterId {
|
||||||
let wolves = self.living_wolf_pack_players();
|
let wolves = self.living_wolf_pack_players();
|
||||||
if let Some(ww) = wolves.iter().find(|w| matches!(w.role(), Role::Werewolf)) {
|
if let Some(ww) = wolves
|
||||||
|
.iter()
|
||||||
|
.find(|w| matches!(w.role_title(), RoleTitle::Werewolf))
|
||||||
|
{
|
||||||
ww.character_id()
|
ww.character_id()
|
||||||
} else if let Some(non_ss_wolf) = wolves.iter().find(|w| {
|
} else if let Some(non_ss_wolf) = wolves
|
||||||
w.role().wolf() && !matches!(w.role(), Role::Shapeshifter { shifted_into: _ })
|
.iter()
|
||||||
}) {
|
.find(|w| w.is_wolf() && !matches!(w.role_title(), RoleTitle::Shapeshifter))
|
||||||
|
{
|
||||||
non_ss_wolf.character_id()
|
non_ss_wolf.character_id()
|
||||||
} else {
|
} else {
|
||||||
wolves.into_iter().next().unwrap().character_id()
|
wolves.into_iter().next().unwrap().character_id()
|
||||||
|
|
@ -163,7 +162,7 @@ impl Village {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn target_by_id(&self, character_id: CharacterId) -> Option<CharacterIdentity> {
|
pub fn target_by_id(&self, character_id: CharacterId) -> Result<CharacterIdentity> {
|
||||||
self.character_by_id(character_id).map(Character::identity)
|
self.character_by_id(character_id).map(Character::identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -197,23 +196,28 @@ impl Village {
|
||||||
|
|
||||||
pub fn executed_known_elder(&self) -> bool {
|
pub fn executed_known_elder(&self) -> bool {
|
||||||
self.characters.iter().any(|d| {
|
self.characters.iter().any(|d| {
|
||||||
matches!(
|
d.known_elder()
|
||||||
d.role(),
|
&& d.died_to()
|
||||||
Role::Elder {
|
|
||||||
woken_for_reveal: true,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
) && d
|
|
||||||
.died_to()
|
|
||||||
.map(|d| matches!(d, DiedTo::Execution { .. }))
|
.map(|d| matches!(d, DiedTo::Execution { .. }))
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn executions_on_day(&self, on_day: NonZeroU8) -> Box<[Character]> {
|
||||||
|
self.characters
|
||||||
|
.iter()
|
||||||
|
.filter(|c| match c.died_to() {
|
||||||
|
Some(DiedTo::Execution { day }) => day.get() == on_day.get(),
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.cloned()
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
pub fn living_characters_by_role(&self, role: RoleTitle) -> Box<[Character]> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|c| c.role().title() == role)
|
.filter(|c| c.role_title() == role)
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -222,16 +226,18 @@ impl Village {
|
||||||
self.characters.iter().cloned().collect()
|
self.characters.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Option<&mut Character> {
|
pub fn character_by_id_mut(&mut self, character_id: CharacterId) -> Result<&mut Character> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter_mut()
|
.iter_mut()
|
||||||
.find(|c| c.character_id() == character_id)
|
.find(|c| c.character_id() == character_id)
|
||||||
|
.ok_or(GameError::InvalidTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn character_by_id(&self, character_id: CharacterId) -> Option<&Character> {
|
pub fn character_by_id(&self, character_id: CharacterId) -> Result<&Character> {
|
||||||
self.characters
|
self.characters
|
||||||
.iter()
|
.iter()
|
||||||
.find(|c| c.character_id() == character_id)
|
.find(|c| c.character_id() == character_id)
|
||||||
|
.ok_or(GameError::InvalidTarget)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> {
|
pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> {
|
||||||
|
|
@ -269,8 +275,23 @@ impl RoleTitle {
|
||||||
RoleTitle::Guardian => Role::Guardian {
|
RoleTitle::Guardian => Role::Guardian {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
},
|
},
|
||||||
// fallback to villager
|
|
||||||
RoleTitle::Apprentice => Role::Villager,
|
RoleTitle::Apprentice => Role::Villager,
|
||||||
|
RoleTitle::Adjudicator => Role::Adjudicator,
|
||||||
|
RoleTitle::PowerSeer => Role::PowerSeer,
|
||||||
|
RoleTitle::Mortician => Role::Mortician,
|
||||||
|
RoleTitle::Beholder => Role::Beholder,
|
||||||
|
RoleTitle::MasonLeader => Role::MasonLeader {
|
||||||
|
recruits_available: 1,
|
||||||
|
recruits: Box::new([]),
|
||||||
|
},
|
||||||
|
RoleTitle::Empath => Role::Empath { cursed: false },
|
||||||
|
RoleTitle::Vindicator => Role::Vindicator,
|
||||||
|
RoleTitle::Diseased => Role::Diseased,
|
||||||
|
RoleTitle::BlackKnight => Role::BlackKnight { attacked: false },
|
||||||
|
RoleTitle::Weightlifter => Role::Weightlifter,
|
||||||
|
RoleTitle::PyreMaster => Role::PyreMaster {
|
||||||
|
villagers_killed: 0,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ mod night_order;
|
||||||
mod role;
|
mod role;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameSettings, SetupRole, SetupSlot},
|
game::{Game, GameSettings, SetupRole, SetupSlot},
|
||||||
message::{
|
message::{
|
||||||
|
|
@ -9,14 +10,11 @@ use crate::{
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||||
},
|
},
|
||||||
player::{Character, CharacterId, PlayerId},
|
player::PlayerId,
|
||||||
role::{Alignment, Role, RoleTitle},
|
role::{Alignment, RoleTitle},
|
||||||
};
|
};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use core::{
|
use core::{num::NonZeroU8, ops::Range};
|
||||||
num::NonZeroU8,
|
|
||||||
ops::Range,
|
|
||||||
};
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
@ -40,6 +38,7 @@ impl SettingsExt for GameSettings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
pub trait ActionPromptTitleExt {
|
pub trait ActionPromptTitleExt {
|
||||||
fn wolf_pack_kill(&self);
|
fn wolf_pack_kill(&self);
|
||||||
fn cover_of_darkness(&self);
|
fn cover_of_darkness(&self);
|
||||||
|
|
@ -56,6 +55,8 @@ pub trait ActionPromptTitleExt {
|
||||||
fn shapeshifter(&self);
|
fn shapeshifter(&self);
|
||||||
fn alphawolf(&self);
|
fn alphawolf(&self);
|
||||||
fn direwolf(&self);
|
fn direwolf(&self);
|
||||||
|
fn masons_wake(&self);
|
||||||
|
fn masons_leader_recruit(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPromptTitleExt for ActionPromptTitle {
|
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
|
|
@ -104,6 +105,12 @@ impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
fn wolf_pack_kill(&self) {
|
fn wolf_pack_kill(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::WolfPackKill);
|
assert_eq!(*self, ActionPromptTitle::WolfPackKill);
|
||||||
}
|
}
|
||||||
|
fn masons_wake(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::MasonsWake)
|
||||||
|
}
|
||||||
|
fn masons_leader_recruit(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::MasonLeaderRecruit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait ActionResultExt {
|
pub trait ActionResultExt {
|
||||||
|
|
@ -195,7 +202,9 @@ impl GameExt for Game {
|
||||||
self.village()
|
self.village()
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|c| c.alive() && matches!(c.role(), Role::Villager) && c.player_id() != excl)
|
.find(|c| {
|
||||||
|
c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl
|
||||||
|
})
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,7 +212,9 @@ impl GameExt for Game {
|
||||||
self.village()
|
self.village()
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| matches!(c.role(), Role::Villager).then_some(c.character_id()))
|
.filter_map(|c| {
|
||||||
|
matches!(c.role_title(), RoleTitle::Villager).then_some(c.character_id())
|
||||||
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -233,7 +244,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::ElderReveal { .. }
|
ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::CoverOfDarkness
|
| ActionPrompt::CoverOfDarkness
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
|
@ -243,6 +255,38 @@ impl GameExt for Game {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
| ActionPrompt::Adjudicator {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::PowerSeer {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Mortician {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Beholder {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::MasonLeaderRecruit {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Empath {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Vindicator {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::PyreMaster {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
| ActionPrompt::Protector {
|
| ActionPrompt::Protector {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
|
|
@ -279,8 +323,15 @@ impl GameExt for Game {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => assert_eq!(marked, mark, "marked character"),
|
} => assert_eq!(marked, mark, "marked character"),
|
||||||
|
|
||||||
ActionPrompt::Seer { marked: None, .. }
|
ActionPrompt::Seer { 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::Protector { marked: None, .. }
|
| ActionPrompt::Protector { marked: None, .. }
|
||||||
| ActionPrompt::Gravedigger { marked: None, .. }
|
| ActionPrompt::Gravedigger { marked: None, .. }
|
||||||
| ActionPrompt::Hunter { marked: None, .. }
|
| ActionPrompt::Hunter { marked: None, .. }
|
||||||
|
|
@ -341,9 +392,14 @@ impl GameExt for Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn execute(&mut self) -> ActionPrompt {
|
fn execute(&mut self) -> ActionPrompt {
|
||||||
|
assert_eq!(
|
||||||
self.process(HostGameMessage::Day(HostDayMessage::Execute))
|
self.process(HostGameMessage::Day(HostDayMessage::Execute))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.prompt()
|
.prompt(),
|
||||||
|
ActionPrompt::CoverOfDarkness
|
||||||
|
);
|
||||||
|
self.r#continue().r#continue();
|
||||||
|
self.next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -563,7 +619,7 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
.village()
|
.village()
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find(|v| v.is_village() && !matches!(v.role().title(), RoleTitle::Protector))
|
.find(|v| v.is_village() && !matches!(v.role_title(), RoleTitle::Protector))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.character_id();
|
.character_id();
|
||||||
match game
|
match game
|
||||||
|
|
@ -580,14 +636,7 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
resp => panic!("unexpected server message: {resp:#?}"),
|
resp => panic!("unexpected server message: {resp:#?}"),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
let living_villagers = match game.execute() {
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
let living_villagers = match game
|
|
||||||
.process(HostGameMessage::Night(HostNightMessage::Next))
|
|
||||||
.unwrap()
|
|
||||||
.prompt()
|
|
||||||
{
|
|
||||||
ActionPrompt::WolfPackKill {
|
ActionPrompt::WolfPackKill {
|
||||||
living_villagers,
|
living_villagers,
|
||||||
marked: _,
|
marked: _,
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,11 @@ use core::num::NonZeroU8;
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity,
|
CharacterIdentity,
|
||||||
night::{ActionPrompt, ActionPromptTitle},
|
night::{ActionPrompt, ActionPromptTitle},
|
||||||
},
|
},
|
||||||
player::CharacterId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
fn character_identity() -> CharacterIdentity {
|
fn character_identity() -> CharacterIdentity {
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() {
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let elder = game.character_by_player_id(elder_player_id);
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
||||||
game.mark_and_check(elder.character_id());
|
game.mark_and_check(elder.character_id());
|
||||||
|
|
@ -47,10 +45,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() {
|
||||||
let elder = game.character_by_player_id(elder_player_id);
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
assert_eq!(elder.died_to().cloned(), None);
|
assert_eq!(elder.died_to().cloned(), None);
|
||||||
|
|
||||||
assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness);
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
game.mark_and_check(elder.character_id());
|
game.mark_and_check(elder.character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
|
@ -60,9 +55,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() {
|
||||||
.died_to()
|
.died_to()
|
||||||
.cloned(),
|
.cloned(),
|
||||||
Some(DiedTo::Wolfpack {
|
Some(DiedTo::Wolfpack {
|
||||||
killing_wolf: game
|
killing_wolf: game.character_by_player_id(wolf_player_id).character_id(),
|
||||||
.character_by_player_id(wolf_player_id)
|
|
||||||
.character_id(),
|
|
||||||
night: NonZeroU8::new(2).unwrap(),
|
night: NonZeroU8::new(2).unwrap(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -92,9 +85,7 @@ fn elder_doesnt_die_first_try_night_knows() {
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let elder = game.character_by_player_id(elder_player_id);
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
|
|
||||||
game.mark_and_check(elder.character_id());
|
game.mark_and_check(elder.character_id());
|
||||||
|
|
@ -113,10 +104,7 @@ fn elder_doesnt_die_first_try_night_knows() {
|
||||||
let elder = game.character_by_player_id(elder_player_id);
|
let elder = game.character_by_player_id(elder_player_id);
|
||||||
assert_eq!(elder.died_to().cloned(), None);
|
assert_eq!(elder.died_to().cloned(), None);
|
||||||
|
|
||||||
assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness);
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
game.mark_and_check(elder.character_id());
|
game.mark_and_check(elder.character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
|
@ -167,10 +155,7 @@ fn elder_executed_doesnt_know() {
|
||||||
|
|
||||||
game.mark_for_execution(elder.character_id());
|
game.mark_for_execution(elder.character_id());
|
||||||
|
|
||||||
game.execute().title().cover_of_darkness();
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
game.next().title().wolf_pack_kill();
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
game.character_by_player_id(elder_player_id)
|
game.character_by_player_id(elder_player_id)
|
||||||
|
|
@ -234,10 +219,7 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() {
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
game.execute().title().cover_of_darkness();
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
game.next().title().wolf_pack_kill();
|
|
||||||
game.mark(villagers.next().unwrap());
|
game.mark(villagers.next().unwrap());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
|
@ -269,10 +251,7 @@ fn elder_executed_knows_no_powers_incl_hunter_activation() {
|
||||||
);
|
);
|
||||||
|
|
||||||
game.mark_for_execution(game.character_by_player_id(elder_player_id).character_id());
|
game.mark_for_execution(game.character_by_player_id(elder_player_id).character_id());
|
||||||
game.execute().title().cover_of_darkness();
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
game.next().title().wolf_pack_kill();
|
|
||||||
game.mark(game.character_by_player_id(hunter_player_id).character_id());
|
game.mark(game.character_by_player_id(hunter_player_id).character_id());
|
||||||
game.r#continue().sleep();
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,69 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mason_recruits_decrement() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let mason_leader_player_id = players[0].player_id;
|
||||||
|
let wolf_player_id = players[1].player_id;
|
||||||
|
let sacrificial_wolf_player_id = players[2].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::MasonLeader {
|
||||||
|
recruits_available: NonZeroU8::new(1).unwrap(),
|
||||||
|
},
|
||||||
|
mason_leader_player_id,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, sacrificial_wolf_player_id);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
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_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(mason_leader_player_id)
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
let recruited = game.living_villager_excl(mason_leader_player_id);
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::MasonLeaderRecruit);
|
||||||
|
game.mark(recruited.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::MasonsWake {
|
||||||
|
character_id: game
|
||||||
|
.character_by_player_id(mason_leader_player_id)
|
||||||
|
.identity(),
|
||||||
|
masons: Box::new([game
|
||||||
|
.character_by_player_id(recruited.player_id())
|
||||||
|
.identity()])
|
||||||
|
}
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(recruited.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().masons_wake();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
mod elder;
|
mod elder;
|
||||||
|
mod mason;
|
||||||
mod scapegoat;
|
mod scapegoat;
|
||||||
mod shapeshifter;
|
mod shapeshifter;
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use core::num::NonZero;
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
|
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
|
||||||
game_test::{ActionResultExt, GameExt, gen_players},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
role::{Alignment, RoleTitle},
|
role::{Alignment, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -72,9 +72,7 @@ fn redeemed_scapegoat_role_changes() {
|
||||||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
game.execute().title().wolf_pack_kill();
|
||||||
game.r#continue().r#continue();
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let seer = game
|
let seer = game
|
||||||
.village()
|
.village()
|
||||||
.characters()
|
.characters()
|
||||||
|
|
@ -103,10 +101,8 @@ fn redeemed_scapegoat_role_changes() {
|
||||||
night: NonZero::new(1).unwrap()
|
night: NonZero::new(1).unwrap()
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
|
||||||
game.r#continue().r#continue();
|
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
|
||||||
let wolf_target_2 = game
|
let wolf_target_2 = game
|
||||||
.village()
|
.village()
|
||||||
.characters()
|
.characters()
|
||||||
|
|
@ -153,3 +149,43 @@ fn redeemed_scapegoat_role_changes() {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer);
|
assert_eq!(day_scapegoat.role().title(), RoleTitle::Seer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn redeemed_scapegoat_cannot_redeem_into_wolf() {
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let scapegoat_player_id = players[0].player_id;
|
||||||
|
let wolf_player_id = players[1].player_id;
|
||||||
|
let sacrificial_wolf_player_id = players[2].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(true),
|
||||||
|
},
|
||||||
|
scapegoat_player_id,
|
||||||
|
);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf_player_id);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, sacrificial_wolf_player_id);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
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_expect_day();
|
||||||
|
|
||||||
|
game.mark_for_execution(
|
||||||
|
game.character_by_player_id(sacrificial_wolf_player_id)
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_and_check(
|
||||||
|
game.living_villager_excl(scapegoat_player_id)
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
let day_scapegoat = game.character_by_player_id(scapegoat_player_id);
|
||||||
|
assert_eq!(day_scapegoat.role().title(), RoleTitle::Scapegoat);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,13 +2,13 @@
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log},
|
game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log},
|
||||||
message::{
|
message::{
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||||
},
|
},
|
||||||
player::CharacterId,
|
|
||||||
role::RoleTitle,
|
role::RoleTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -186,9 +186,8 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
|
||||||
let (_, marked, _) = game.mark_for_execution(target);
|
let (_, marked, _) = game.mark_for_execution(target);
|
||||||
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
|
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
|
||||||
assert_eq!(target_list, marked);
|
assert_eq!(target_list, marked);
|
||||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
|
||||||
game.r#continue().r#continue();
|
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
|
||||||
let target = game
|
let target = game
|
||||||
.village()
|
.village()
|
||||||
.characters()
|
.characters()
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
|
|
||||||
|
pub mod character;
|
||||||
pub mod diedto;
|
pub mod diedto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod game;
|
pub mod game;
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use core::num::NonZeroU8;
|
||||||
pub use ident::*;
|
pub use ident::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{game::GameOver, player::CharacterId, role::RoleTitle};
|
use crate::{character::CharacterId, game::GameOver, role::RoleTitle};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum ClientMessage {
|
pub enum ClientMessage {
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,14 @@ use core::num::NonZeroU8;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameOver, GameSettings},
|
game::{GameOver, GameSettings},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity,
|
CharacterIdentity,
|
||||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
},
|
},
|
||||||
player::{CharacterId, PlayerId},
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{CharacterState, PlayerState};
|
use super::{CharacterState, PlayerState};
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,7 @@ use core::{fmt::Display, num::NonZeroU8};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{character::CharacterId, diedto::DiedTo, player::PlayerId, role::RoleTitle};
|
||||||
diedto::DiedTo,
|
|
||||||
player::{CharacterId, PlayerId},
|
|
||||||
role::RoleTitle,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Identification {
|
pub struct Identification {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::{ChecksAs, Titles};
|
use werewolves_macros::{ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
|
diedto::DiedToTitle,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
message::CharacterIdentity,
|
message::CharacterIdentity,
|
||||||
player::CharacterId,
|
|
||||||
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -19,7 +22,11 @@ pub enum ActionType {
|
||||||
Direwolf,
|
Direwolf,
|
||||||
OtherWolf,
|
OtherWolf,
|
||||||
Block,
|
Block,
|
||||||
|
Intel,
|
||||||
Other,
|
Other,
|
||||||
|
MasonRecruit,
|
||||||
|
MasonsWake,
|
||||||
|
Beholder,
|
||||||
RoleChange,
|
RoleChange,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +57,7 @@ pub enum ActionPrompt {
|
||||||
},
|
},
|
||||||
#[checks(ActionType::RoleChange)]
|
#[checks(ActionType::RoleChange)]
|
||||||
ElderReveal { character_id: CharacterIdentity },
|
ElderReveal { character_id: CharacterIdentity },
|
||||||
#[checks(ActionType::Other)]
|
#[checks(ActionType::Intel)]
|
||||||
Seer {
|
Seer {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
|
@ -62,13 +69,13 @@ pub enum ActionPrompt {
|
||||||
targets: Box<[CharacterIdentity]>,
|
targets: Box<[CharacterIdentity]>,
|
||||||
marked: Option<CharacterId>,
|
marked: Option<CharacterId>,
|
||||||
},
|
},
|
||||||
#[checks(ActionType::Other)]
|
#[checks(ActionType::Intel)]
|
||||||
Arcanist {
|
Arcanist {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
marked: (Option<CharacterId>, Option<CharacterId>),
|
marked: (Option<CharacterId>, Option<CharacterId>),
|
||||||
},
|
},
|
||||||
#[checks(ActionType::Other)]
|
#[checks(ActionType::Intel)]
|
||||||
Gravedigger {
|
Gravedigger {
|
||||||
character_id: CharacterIdentity,
|
character_id: CharacterIdentity,
|
||||||
dead_players: Box<[CharacterIdentity]>,
|
dead_players: Box<[CharacterIdentity]>,
|
||||||
|
|
@ -101,6 +108,61 @@ pub enum ActionPrompt {
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
marked: Option<CharacterId>,
|
marked: Option<CharacterId>,
|
||||||
},
|
},
|
||||||
|
#[checks(ActionType::Intel)]
|
||||||
|
Adjudicator {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::Intel)]
|
||||||
|
PowerSeer {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::Intel)]
|
||||||
|
Mortician {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
dead_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::Beholder)]
|
||||||
|
Beholder {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::MasonsWake)]
|
||||||
|
MasonsWake {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
masons: Box<[CharacterIdentity]>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::MasonRecruit)]
|
||||||
|
MasonLeaderRecruit {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
recruits_left: NonZeroU8,
|
||||||
|
potential_recruits: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::Intel)]
|
||||||
|
Empath {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::Protect)]
|
||||||
|
Vindicator {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
#[checks(ActionType::Other)]
|
||||||
|
PyreMaster {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
|
|
||||||
#[checks(ActionType::WolfPackKill)]
|
#[checks(ActionType::WolfPackKill)]
|
||||||
WolfPackKill {
|
WolfPackKill {
|
||||||
living_villagers: Box<[CharacterIdentity]>,
|
living_villagers: Box<[CharacterIdentity]>,
|
||||||
|
|
@ -123,10 +185,41 @@ pub enum ActionPrompt {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPrompt {
|
impl ActionPrompt {
|
||||||
|
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
|
||||||
|
match self {
|
||||||
|
ActionPrompt::Seer { character_id, .. }
|
||||||
|
| ActionPrompt::Arcanist { character_id, .. }
|
||||||
|
| ActionPrompt::Gravedigger { character_id, .. }
|
||||||
|
| ActionPrompt::Adjudicator { character_id, .. }
|
||||||
|
| ActionPrompt::PowerSeer { character_id, .. }
|
||||||
|
| ActionPrompt::Mortician { character_id, .. }
|
||||||
|
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
|
||||||
|
|
||||||
|
ActionPrompt::Beholder { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::Protector { .. }
|
||||||
|
| ActionPrompt::Hunter { .. }
|
||||||
|
| ActionPrompt::Militia { .. }
|
||||||
|
| ActionPrompt::MapleWolf { .. }
|
||||||
|
| ActionPrompt::Guardian { .. }
|
||||||
|
| ActionPrompt::PyreMaster { .. }
|
||||||
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
|
| ActionPrompt::AlphaWolf { .. }
|
||||||
|
| ActionPrompt::DireWolf { .. }
|
||||||
|
| ActionPrompt::Empath { .. }
|
||||||
|
| ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { .. }
|
||||||
|
| ActionPrompt::WolfPackKill { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
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::ElderReveal { .. }
|
ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
| ActionPrompt::Shapeshifter { .. }
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
|
|
@ -195,7 +288,47 @@ impl ActionPrompt {
|
||||||
Ok(prompt)
|
Ok(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionPrompt::Protector {
|
ActionPrompt::Adjudicator {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::PowerSeer {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Mortician {
|
||||||
|
dead_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Beholder {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::MasonLeaderRecruit {
|
||||||
|
potential_recruits: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Empath {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Vindicator {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::PyreMaster {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Protector {
|
||||||
targets, marked, ..
|
targets, marked, ..
|
||||||
}
|
}
|
||||||
| ActionPrompt::Seer {
|
| ActionPrompt::Seer {
|
||||||
|
|
@ -274,17 +407,6 @@ impl PartialOrd for ActionPrompt {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
|
||||||
pub enum ActionResponse {
|
pub enum ActionResponse {
|
||||||
// Seer(CharacterId),
|
|
||||||
// Arcanist(Option<CharacterId>, Option<CharacterId>),
|
|
||||||
// Gravedigger(CharacterId),
|
|
||||||
// Hunter(CharacterId),
|
|
||||||
// Militia(Option<CharacterId>),
|
|
||||||
// MapleWolf(Option<CharacterId>),
|
|
||||||
// Guardian(CharacterId),
|
|
||||||
// WolfPackKillVote(CharacterId),
|
|
||||||
// AlphaWolf(Option<CharacterId>),
|
|
||||||
// Direwolf(CharacterId),
|
|
||||||
// Protector(CharacterId),
|
|
||||||
MarkTarget(CharacterId),
|
MarkTarget(CharacterId),
|
||||||
Shapeshift,
|
Shapeshift,
|
||||||
Continue,
|
Continue,
|
||||||
|
|
@ -294,8 +416,12 @@ pub enum ActionResponse {
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
RoleBlocked,
|
RoleBlocked,
|
||||||
Seer(Alignment),
|
Seer(Alignment),
|
||||||
|
PowerSeer { powerful: bool },
|
||||||
|
Adjudicator { killer: bool },
|
||||||
Arcanist { same: bool },
|
Arcanist { same: bool },
|
||||||
GraveDigger(Option<RoleTitle>),
|
GraveDigger(Option<RoleTitle>),
|
||||||
|
Mortician(DiedToTitle),
|
||||||
|
Empath { scapegoat: bool },
|
||||||
GoBackToSleep,
|
GoBackToSleep,
|
||||||
Continue,
|
Continue,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,10 @@
|
||||||
use core::{fmt::Display, num::NonZeroU8};
|
use core::fmt::Display;
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
character::CharacterId,
|
||||||
error::GameError,
|
role::{Role, RoleTitle},
|
||||||
game::{DateTime, Village},
|
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
|
||||||
modifier::Modifier,
|
|
||||||
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
|
|
@ -30,24 +25,6 @@ impl Display for PlayerId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
|
||||||
pub struct CharacterId(uuid::Uuid);
|
|
||||||
|
|
||||||
impl CharacterId {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
Self(uuid::Uuid::new_v4())
|
|
||||||
}
|
|
||||||
pub const fn from_u128(v: u128) -> Self {
|
|
||||||
Self(uuid::Uuid::from_u128(v))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for CharacterId {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
self.0.fmt(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
id: PlayerId,
|
id: PlayerId,
|
||||||
|
|
@ -67,6 +44,7 @@ impl Player {
|
||||||
pub enum Protection {
|
pub enum Protection {
|
||||||
Guardian { source: CharacterId, guarding: bool },
|
Guardian { source: CharacterId, guarding: bool },
|
||||||
Protector { source: CharacterId },
|
Protector { source: CharacterId },
|
||||||
|
Vindicator { source: CharacterId },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
|
@ -77,361 +55,7 @@ pub enum KillOutcome {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct RoleChange {
|
pub struct RoleChange {
|
||||||
role: Role,
|
pub role: Role,
|
||||||
new_role: RoleTitle,
|
pub new_role: RoleTitle,
|
||||||
changed_on_night: u8,
|
pub changed_on_night: u8,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Character {
|
|
||||||
player_id: PlayerId,
|
|
||||||
identity: CharacterIdentity,
|
|
||||||
role: Role,
|
|
||||||
modifier: Option<Modifier>,
|
|
||||||
died_to: Option<DiedTo>,
|
|
||||||
role_changes: Vec<RoleChange>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Character {
|
|
||||||
pub fn new(
|
|
||||||
Identification {
|
|
||||||
player_id,
|
|
||||||
public:
|
|
||||||
PublicIdentity {
|
|
||||||
name,
|
|
||||||
pronouns,
|
|
||||||
number,
|
|
||||||
},
|
|
||||||
}: Identification,
|
|
||||||
role: Role,
|
|
||||||
) -> Option<Self> {
|
|
||||||
Some(Self {
|
|
||||||
role,
|
|
||||||
identity: CharacterIdentity {
|
|
||||||
character_id: CharacterId::new(),
|
|
||||||
name,
|
|
||||||
pronouns,
|
|
||||||
number: number?,
|
|
||||||
},
|
|
||||||
player_id,
|
|
||||||
modifier: None,
|
|
||||||
died_to: None,
|
|
||||||
role_changes: Vec::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn is_power_role(&self) -> bool {
|
|
||||||
match &self.role {
|
|
||||||
Role::Scapegoat { .. } | Role::Villager => false,
|
|
||||||
|
|
||||||
Role::Seer
|
|
||||||
| Role::Arcanist
|
|
||||||
| Role::Gravedigger
|
|
||||||
| Role::Hunter { .. }
|
|
||||||
| Role::Militia { .. }
|
|
||||||
| Role::MapleWolf { .. }
|
|
||||||
| Role::Guardian { .. }
|
|
||||||
| Role::Protector { .. }
|
|
||||||
| Role::Apprentice(..)
|
|
||||||
| Role::Elder { .. }
|
|
||||||
| Role::Werewolf
|
|
||||||
| Role::AlphaWolf { .. }
|
|
||||||
| Role::DireWolf
|
|
||||||
| Role::Shapeshifter { .. } => true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn identity(&self) -> CharacterIdentity {
|
|
||||||
self.identity.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
self.identity.name.as_str()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn number(&self) -> NonZeroU8 {
|
|
||||||
self.identity.number
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn pronouns(&self) -> Option<&str> {
|
|
||||||
match self.identity.pronouns.as_ref() {
|
|
||||||
Some(p) => Some(p.as_str()),
|
|
||||||
None => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn died_to(&self) -> Option<&DiedTo> {
|
|
||||||
self.died_to.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kill(&mut self, died_to: DiedTo) {
|
|
||||||
match (&mut self.role, died_to.date_time()) {
|
|
||||||
(
|
|
||||||
Role::Elder {
|
|
||||||
lost_protection_night: Some(_),
|
|
||||||
..
|
|
||||||
},
|
|
||||||
_,
|
|
||||||
) => {}
|
|
||||||
(
|
|
||||||
Role::Elder {
|
|
||||||
lost_protection_night,
|
|
||||||
..
|
|
||||||
},
|
|
||||||
DateTime::Night { number: night },
|
|
||||||
) => {
|
|
||||||
*lost_protection_night = lost_protection_night
|
|
||||||
.is_none()
|
|
||||||
.then_some(night)
|
|
||||||
.and_then(NonZeroU8::new);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
match &self.died_to {
|
|
||||||
Some(_) => {}
|
|
||||||
None => self.died_to = Some(died_to),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn alive(&self) -> bool {
|
|
||||||
self.died_to.is_none()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn execute(&mut self, day: NonZeroU8) -> Result<(), GameError> {
|
|
||||||
if self.died_to.is_some() {
|
|
||||||
return Err(GameError::CharacterAlreadyDead);
|
|
||||||
}
|
|
||||||
self.died_to = Some(DiedTo::Execution { day });
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn character_id(&self) -> CharacterId {
|
|
||||||
self.identity.character_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn player_id(&self) -> PlayerId {
|
|
||||||
self.player_id
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn role(&self) -> &Role {
|
|
||||||
&self.role
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn gravedigger_dig(&self) -> Option<RoleTitle> {
|
|
||||||
match &self.role {
|
|
||||||
Role::Shapeshifter {
|
|
||||||
shifted_into: Some(_),
|
|
||||||
} => None,
|
|
||||||
_ => Some(self.role.title()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn alignment(&self) -> Alignment {
|
|
||||||
self.role.alignment()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn role_mut(&mut self) -> &mut Role {
|
|
||||||
&mut self.role
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn elder_reveal(&mut self) {
|
|
||||||
if let Role::Elder {
|
|
||||||
woken_for_reveal, ..
|
|
||||||
} = &mut self.role
|
|
||||||
{
|
|
||||||
*woken_for_reveal = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<(), GameError> {
|
|
||||||
let mut role = new_role.title_to_role_excl_apprentice();
|
|
||||||
core::mem::swap(&mut role, &mut self.role);
|
|
||||||
self.role_changes.push(RoleChange {
|
|
||||||
role,
|
|
||||||
new_role,
|
|
||||||
changed_on_night: match at {
|
|
||||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
|
||||||
DateTime::Night { number } => number,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn is_wolf(&self) -> bool {
|
|
||||||
self.role.wolf()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn is_village(&self) -> bool {
|
|
||||||
!self.is_wolf()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn night_action_prompt(
|
|
||||||
&self,
|
|
||||||
village: &Village,
|
|
||||||
) -> Result<Option<ActionPrompt>, GameError> {
|
|
||||||
if !self.alive() || !self.role.wakes(village) {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
let night = match village.date_time() {
|
|
||||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
|
||||||
DateTime::Night { number } => number,
|
|
||||||
};
|
|
||||||
Ok(Some(match &self.role {
|
|
||||||
Role::Shapeshifter {
|
|
||||||
shifted_into: Some(_),
|
|
||||||
}
|
|
||||||
| Role::AlphaWolf { killed: Some(_) }
|
|
||||||
| Role::Militia { targeted: Some(_) }
|
|
||||||
| Role::Scapegoat { redeemed: false }
|
|
||||||
| Role::Elder {
|
|
||||||
woken_for_reveal: true,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| Role::Villager => return Ok(None),
|
|
||||||
Role::Scapegoat { redeemed: true } => {
|
|
||||||
let mut dead = village.dead_characters();
|
|
||||||
dead.shuffle(&mut rand::rng());
|
|
||||||
if let Some(pr) = dead
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|d| d.is_power_role().then_some(d.role().title()))
|
|
||||||
{
|
|
||||||
ActionPrompt::RoleChange {
|
|
||||||
character_id: self.identity(),
|
|
||||||
new_role: pr,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Role::Seer => ActionPrompt::Seer {
|
|
||||||
character_id: self.identity(),
|
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Arcanist => ActionPrompt::Arcanist {
|
|
||||||
character_id: self.identity(),
|
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: (None, None),
|
|
||||||
},
|
|
||||||
Role::Protector {
|
|
||||||
last_protected: Some(last_protected),
|
|
||||||
} => ActionPrompt::Protector {
|
|
||||||
character_id: self.identity(),
|
|
||||||
targets: village.living_players_excluding(*last_protected),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Protector {
|
|
||||||
last_protected: None,
|
|
||||||
} => ActionPrompt::Protector {
|
|
||||||
character_id: self.identity(),
|
|
||||||
targets: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Apprentice(role) => {
|
|
||||||
let current_night = match village.date_time() {
|
|
||||||
DateTime::Day { number: _ } => return Ok(None),
|
|
||||||
DateTime::Night { number } => number,
|
|
||||||
};
|
|
||||||
return Ok(village
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|c| c.role().title() == *role)
|
|
||||||
.filter_map(|char| char.died_to)
|
|
||||||
.any(|died_to| match died_to.date_time() {
|
|
||||||
DateTime::Day { number } => number.get() + 1 >= current_night,
|
|
||||||
DateTime::Night { number } => number + 1 >= current_night,
|
|
||||||
})
|
|
||||||
.then(|| ActionPrompt::RoleChange {
|
|
||||||
character_id: self.identity(),
|
|
||||||
new_role: *role,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
Role::Elder {
|
|
||||||
knows_on_night,
|
|
||||||
woken_for_reveal: false,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let current_night = match village.date_time() {
|
|
||||||
DateTime::Day { number: _ } => return Ok(None),
|
|
||||||
DateTime::Night { number } => number,
|
|
||||||
};
|
|
||||||
return Ok((current_night >= knows_on_night.get()).then_some({
|
|
||||||
ActionPrompt::ElderReveal {
|
|
||||||
character_id: self.identity(),
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
|
||||||
character_id: self.identity(),
|
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Werewolf => ActionPrompt::WolfPackKill {
|
|
||||||
living_villagers: village.living_players(),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
|
||||||
character_id: self.identity(),
|
|
||||||
living_villagers: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::DireWolf => ActionPrompt::DireWolf {
|
|
||||||
character_id: self.identity(),
|
|
||||||
living_players: village.living_players(),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
|
||||||
character_id: self.identity(),
|
|
||||||
},
|
|
||||||
Role::Gravedigger => {
|
|
||||||
let dead = village.dead_targets();
|
|
||||||
if dead.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
ActionPrompt::Gravedigger {
|
|
||||||
character_id: self.identity(),
|
|
||||||
dead_players: village.dead_targets(),
|
|
||||||
marked: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
|
||||||
character_id: self.identity(),
|
|
||||||
current_target: target.as_ref().and_then(|t| village.target_by_id(*t)),
|
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
|
|
||||||
character_id: self.identity(),
|
|
||||||
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Guardian {
|
|
||||||
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
|
||||||
} => ActionPrompt::Guardian {
|
|
||||||
character_id: self.identity(),
|
|
||||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
|
||||||
living_players: village.living_players_excluding(prev_target.character_id),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Guardian {
|
|
||||||
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
|
||||||
} => ActionPrompt::Guardian {
|
|
||||||
character_id: self.identity(),
|
|
||||||
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
|
||||||
living_players: village.living_players(),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
Role::Guardian {
|
|
||||||
last_protected: None,
|
|
||||||
} => ActionPrompt::Guardian {
|
|
||||||
character_id: self.identity(),
|
|
||||||
previous: None,
|
|
||||||
living_players: village.living_players(),
|
|
||||||
marked: None,
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::{ChecksAs, Titles};
|
use werewolves_macros::{ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
game::{DateTime, Village},
|
game::{DateTime, Village},
|
||||||
message::CharacterIdentity,
|
message::CharacterIdentity,
|
||||||
player::CharacterId,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||||
|
|
@ -28,6 +28,53 @@ pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks("powerful")]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
|
Adjudicator,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
PowerSeer,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
Mortician,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
Beholder,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
MasonLeader {
|
||||||
|
recruits_available: u8,
|
||||||
|
recruits: Box<[CharacterId]>,
|
||||||
|
},
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
Empath { cursed: bool },
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
Vindicator,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
Diseased,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
BlackKnight { attacked: bool },
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
Weightlifter,
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
|
PyreMaster { villagers_killed: u8 },
|
||||||
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks("powerful")]
|
||||||
|
#[checks("is_mentor")]
|
||||||
Gravedigger,
|
Gravedigger,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("killer")]
|
#[checks("killer")]
|
||||||
|
|
@ -101,7 +148,12 @@ impl Role {
|
||||||
|
|
||||||
pub const fn wakes_night_zero(&self) -> bool {
|
pub const fn wakes_night_zero(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Role::DireWolf | Role::Arcanist | Role::Seer => true,
|
Role::PowerSeer
|
||||||
|
| Role::Beholder
|
||||||
|
| Role::Adjudicator
|
||||||
|
| Role::DireWolf
|
||||||
|
| Role::Arcanist
|
||||||
|
| Role::Seer => true,
|
||||||
|
|
||||||
Role::Shapeshifter { .. }
|
Role::Shapeshifter { .. }
|
||||||
| Role::Werewolf
|
| Role::Werewolf
|
||||||
|
|
@ -115,6 +167,14 @@ impl Role {
|
||||||
| Role::Apprentice(_)
|
| Role::Apprentice(_)
|
||||||
| Role::Villager
|
| Role::Villager
|
||||||
| Role::Scapegoat { .. }
|
| Role::Scapegoat { .. }
|
||||||
|
| Role::Mortician
|
||||||
|
| Role::MasonLeader { .. }
|
||||||
|
| Role::Empath { .. }
|
||||||
|
| Role::Vindicator
|
||||||
|
| Role::Diseased
|
||||||
|
| Role::BlackKnight { .. }
|
||||||
|
| Role::Weightlifter
|
||||||
|
| Role::PyreMaster { .. }
|
||||||
| Role::Protector { .. } => false,
|
| Role::Protector { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -132,9 +192,20 @@ impl Role {
|
||||||
| Role::Werewolf
|
| Role::Werewolf
|
||||||
| Role::Scapegoat { redeemed: false }
|
| Role::Scapegoat { redeemed: false }
|
||||||
| Role::Militia { targeted: Some(_) }
|
| Role::Militia { targeted: Some(_) }
|
||||||
|
| Role::Diseased
|
||||||
|
| Role::BlackKnight { .. }
|
||||||
| Role::Villager => false,
|
| Role::Villager => false,
|
||||||
|
|
||||||
Role::Scapegoat { redeemed: true }
|
Role::PowerSeer
|
||||||
|
| Role::Mortician
|
||||||
|
| Role::Beholder
|
||||||
|
| Role::MasonLeader { .. }
|
||||||
|
| Role::Empath { .. }
|
||||||
|
| Role::Vindicator
|
||||||
|
| Role::Weightlifter
|
||||||
|
| Role::PyreMaster { .. }
|
||||||
|
| Role::Adjudicator
|
||||||
|
| Role::Scapegoat { redeemed: true }
|
||||||
| Role::Shapeshifter { .. }
|
| Role::Shapeshifter { .. }
|
||||||
| Role::DireWolf
|
| Role::DireWolf
|
||||||
| Role::AlphaWolf { killed: None }
|
| Role::AlphaWolf { killed: None }
|
||||||
|
|
@ -150,7 +221,7 @@ impl Role {
|
||||||
Role::Apprentice(title) => village
|
Role::Apprentice(title) => village
|
||||||
.characters()
|
.characters()
|
||||||
.iter()
|
.iter()
|
||||||
.any(|c| c.role().title() == *title),
|
.any(|c| c.role_title() == *title),
|
||||||
|
|
||||||
Role::Elder {
|
Role::Elder {
|
||||||
knows_on_night,
|
knows_on_night,
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,14 @@ use crate::{
|
||||||
};
|
};
|
||||||
use tokio::time::Instant;
|
use tokio::time::Instant;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
character::Character,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameOver, Village},
|
game::{Game, GameOver, Village},
|
||||||
message::{
|
message::{
|
||||||
ClientMessage, Identification, ServerMessage,
|
ClientMessage, Identification, ServerMessage,
|
||||||
host::{HostGameMessage, HostMessage, ServerToHostMessage},
|
host::{HostGameMessage, HostMessage, ServerToHostMessage},
|
||||||
},
|
},
|
||||||
player::{Character, PlayerId},
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
@ -70,7 +71,7 @@ impl GameRunner {
|
||||||
if let Err(err) = self.player_sender.send_if_present(
|
if let Err(err) = self.player_sender.send_if_present(
|
||||||
char.player_id(),
|
char.player_id(),
|
||||||
ServerMessage::GameStart {
|
ServerMessage::GameStart {
|
||||||
role: char.role().initial_shown_role(),
|
role: char.initial_shown_role(),
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
log::warn!(
|
log::warn!(
|
||||||
|
|
@ -110,7 +111,7 @@ impl GameRunner {
|
||||||
.send_if_present(
|
.send_if_present(
|
||||||
player_id,
|
player_id,
|
||||||
ServerMessage::GameStart {
|
ServerMessage::GameStart {
|
||||||
role: char.role().initial_shown_role(),
|
role: char.initial_shown_role(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.log_debug();
|
.log_debug();
|
||||||
|
|
@ -171,7 +172,7 @@ impl GameRunner {
|
||||||
{
|
{
|
||||||
sender
|
sender
|
||||||
.send(ServerMessage::GameStart {
|
.send(ServerMessage::GameStart {
|
||||||
role: char.role().initial_shown_role(),
|
role: char.initial_shown_role(),
|
||||||
})
|
})
|
||||||
.log_debug();
|
.log_debug();
|
||||||
} else if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
} else if let Some(sender) = self.joined_players.get_sender(player_id).await {
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ use gloo::net::websocket::{self, futures::WebSocket};
|
||||||
use instant::Instant;
|
use instant::Instant;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameOver, GameSettings},
|
game::{GameOver, GameSettings},
|
||||||
message::{
|
message::{
|
||||||
|
|
@ -19,7 +20,7 @@ use werewolves_proto::{
|
||||||
},
|
},
|
||||||
night::{ActionPrompt, ActionResult},
|
night::{ActionPrompt, ActionResult},
|
||||||
},
|
},
|
||||||
player::{CharacterId, PlayerId},
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
use yew::{html::Scope, prelude::*};
|
use yew::{html::Scope, prelude::*};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
character::CharacterId,
|
||||||
message::{CharacterIdentity, PublicIdentity},
|
message::{CharacterIdentity, PublicIdentity},
|
||||||
player::CharacterId,
|
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,12 @@
|
||||||
use core::ops::Not;
|
use core::ops::Not;
|
||||||
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
character::CharacterId,
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity, PublicIdentity,
|
CharacterIdentity, PublicIdentity,
|
||||||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||||
night::{ActionPrompt, ActionResponse},
|
night::{ActionPrompt, ActionResponse},
|
||||||
},
|
},
|
||||||
player::CharacterId,
|
|
||||||
role::PreviousGuardianAction,
|
role::PreviousGuardianAction,
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
@ -57,6 +57,14 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
)));
|
)));
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let cont = continue_callback.clone().map(|continue_callback| {
|
||||||
|
html! {
|
||||||
|
<Button on_click={continue_callback}>
|
||||||
|
{"continue"}
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
});
|
||||||
let (character_id, targets, marked, role_info) = match &props.prompt {
|
let (character_id, targets, marked, role_info) = match &props.prompt {
|
||||||
ActionPrompt::CoverOfDarkness => {
|
ActionPrompt::CoverOfDarkness => {
|
||||||
return html! {
|
return html! {
|
||||||
|
|
@ -72,13 +80,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ActionPrompt::ElderReveal { character_id } => {
|
ActionPrompt::ElderReveal { character_id } => {
|
||||||
let cont = continue_callback.map(|continue_callback| {
|
|
||||||
html! {
|
|
||||||
<Button on_click={continue_callback}>
|
|
||||||
{"continue"}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return html! {
|
return html! {
|
||||||
<div class="role-change">
|
<div class="role-change">
|
||||||
{identity_html(props, Some(character_id))}
|
{identity_html(props, Some(character_id))}
|
||||||
|
|
@ -91,13 +92,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
character_id,
|
character_id,
|
||||||
new_role,
|
new_role,
|
||||||
} => {
|
} => {
|
||||||
let cont = continue_callback.map(|continue_callback| {
|
|
||||||
html! {
|
|
||||||
<Button on_click={continue_callback}>
|
|
||||||
{"continue"}
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return html! {
|
return html! {
|
||||||
<div class="role-change">
|
<div class="role-change">
|
||||||
{identity_html(props, Some(character_id))}
|
{identity_html(props, Some(character_id))}
|
||||||
|
|
@ -108,6 +102,31 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionPrompt::MasonsWake {
|
||||||
|
character_id,
|
||||||
|
masons,
|
||||||
|
} => {
|
||||||
|
let masons = masons
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| {
|
||||||
|
let ident: PublicIdentity = c.into();
|
||||||
|
html! {
|
||||||
|
<Identity ident={ident}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
return html! {
|
||||||
|
<div class="masons">
|
||||||
|
{identity_html(props, Some(character_id))}
|
||||||
|
<h2>{"these are the masons"}</h2>
|
||||||
|
<div class="mason-list">
|
||||||
|
{masons}
|
||||||
|
</div>
|
||||||
|
{cont}
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
character_id,
|
character_id,
|
||||||
previous,
|
previous,
|
||||||
|
|
@ -147,6 +166,92 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionPrompt::Adjudicator {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"adjudicator"}},
|
||||||
|
),
|
||||||
|
ActionPrompt::Beholder {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"beholder"}},
|
||||||
|
),
|
||||||
|
ActionPrompt::Empath {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"empath"}},
|
||||||
|
),
|
||||||
|
ActionPrompt::MasonLeaderRecruit {
|
||||||
|
character_id,
|
||||||
|
recruits_left,
|
||||||
|
potential_recruits,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
potential_recruits,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<span>{"mason leader recruit"}</span>
|
||||||
|
<span>{"("}{recruits_left.get()}{" remaining)"}</span>
|
||||||
|
</div>
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ActionPrompt::Mortician {
|
||||||
|
character_id,
|
||||||
|
dead_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
dead_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"mortician"}},
|
||||||
|
),
|
||||||
|
ActionPrompt::PowerSeer {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"power seer"}},
|
||||||
|
),
|
||||||
|
ActionPrompt::PyreMaster {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"pyremaster"}},
|
||||||
|
),
|
||||||
|
ActionPrompt::Vindicator {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"vindicator"}},
|
||||||
|
),
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Seer {
|
||||||
character_id,
|
character_id,
|
||||||
living_players,
|
living_players,
|
||||||
|
|
|
||||||
|
|
@ -40,36 +40,64 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
.big_screen
|
.big_screen
|
||||||
.not()
|
.not()
|
||||||
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
|
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
|
||||||
match &props.result {
|
let body = match &props.result {
|
||||||
|
ActionResult::PowerSeer { powerful } => {
|
||||||
|
let inactive = powerful.not().then_some("inactive");
|
||||||
|
let text = if *powerful {
|
||||||
|
"powerful"
|
||||||
|
} else {
|
||||||
|
"not powerful"
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<img src="/img/powerful.svg" class={classes!(inactive)}/>
|
||||||
|
<h3>{text}</h3>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionResult::Adjudicator { killer } => {
|
||||||
|
let inactive = killer.not().then_some("inactive");
|
||||||
|
let text = if *killer { "killer" } else { "not a killer" };
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<img src="/img/killer.svg" class={classes!(inactive)}/>
|
||||||
|
<h3>{text}</h3>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionResult::Mortician(died_to) => html! {
|
||||||
|
<h2>{"cause of death: "}{died_to.to_string().to_case(Case::Title)}</h2>
|
||||||
|
},
|
||||||
|
ActionResult::Empath { scapegoat: true } => html! {
|
||||||
|
<>
|
||||||
|
<h2>{"was the scapegoat!"}</h2>
|
||||||
|
<h3>{"tag! you're it!"}</h3>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
ActionResult::Empath { scapegoat: false } => html! {
|
||||||
|
<h2>{"not the scapegoat"}</h2>
|
||||||
|
},
|
||||||
ActionResult::RoleBlocked => {
|
ActionResult::RoleBlocked => {
|
||||||
html! {
|
html! {
|
||||||
<div class="result">
|
|
||||||
{ident}
|
|
||||||
<h2>{"you were role blocked"}</h2>
|
<h2>{"you were role blocked"}</h2>
|
||||||
{cont}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActionResult::Seer(alignment) => html! {
|
ActionResult::Seer(alignment) => html! {
|
||||||
<div class="result">
|
<>
|
||||||
{ident}
|
|
||||||
<h2>{"the alignment was"}</h2>
|
<h2>{"the alignment was"}</h2>
|
||||||
<p>{match alignment {
|
<p>{match alignment {
|
||||||
Alignment::Village => "village",
|
Alignment::Village => "village",
|
||||||
Alignment::Wolves => "wolfpack",
|
Alignment::Wolves => "wolfpack",
|
||||||
}}</p>
|
}}</p>
|
||||||
{cont}
|
</>
|
||||||
</div>
|
|
||||||
},
|
},
|
||||||
ActionResult::Arcanist { same } => {
|
ActionResult::Arcanist { same } => {
|
||||||
let outcome = if *same { "same" } else { "different" };
|
let outcome = if *same { "same" } else { "different" };
|
||||||
html! {
|
html! {
|
||||||
<div class="result">
|
<>
|
||||||
{ident}
|
|
||||||
<h2>{"the alignments are:"}</h2>
|
<h2>{"the alignments are:"}</h2>
|
||||||
<p>{outcome}</p>
|
<p>{outcome}</p>
|
||||||
{cont}
|
</>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActionResult::GraveDigger(role_title) => {
|
ActionResult::GraveDigger(role_title) => {
|
||||||
|
|
@ -77,12 +105,10 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
.map(|r| r.to_string().to_case(Case::Title))
|
.map(|r| r.to_string().to_case(Case::Title))
|
||||||
.unwrap_or_else(|| String::from("an empty grave"));
|
.unwrap_or_else(|| String::from("an empty grave"));
|
||||||
html! {
|
html! {
|
||||||
<div class="result">
|
<>
|
||||||
{ident}
|
|
||||||
<h2>{"you see:"}</h2>
|
<h2>{"you see:"}</h2>
|
||||||
<p>{dig}</p>
|
<p>{dig}</p>
|
||||||
{cont}
|
</>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActionResult::GoBackToSleep => {
|
ActionResult::GoBackToSleep => {
|
||||||
|
|
@ -94,17 +120,25 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
html! {
|
return html! {
|
||||||
<CoverOfDarkness message={"go to sleep"} next={next}>
|
<CoverOfDarkness message={"go to sleep"} next={next}>
|
||||||
{"continue"}
|
{"continue"}
|
||||||
</CoverOfDarkness>
|
</CoverOfDarkness>
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
ActionResult::Continue => {
|
ActionResult::Continue => {
|
||||||
props.on_complete.emit(HostMessage::GetState);
|
props.on_complete.emit(HostMessage::GetState);
|
||||||
html! {
|
return html! {
|
||||||
<CoverOfDarkness />
|
<CoverOfDarkness />
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="result">
|
||||||
|
{ident}
|
||||||
|
{body}
|
||||||
|
{cont}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,353 +1,353 @@
|
||||||
use core::{fmt::Debug, ops::Not};
|
// use core::{fmt::Debug, ops::Not};
|
||||||
|
|
||||||
use werewolves_proto::{
|
// use werewolves_proto::{
|
||||||
message::{CharacterIdentity, PublicIdentity},
|
// message::{CharacterIdentity, PublicIdentity},
|
||||||
player::CharacterId,
|
// player::CharacterId,
|
||||||
};
|
// };
|
||||||
use yew::prelude::*;
|
// use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Button, Identity};
|
// use crate::components::{Button, Identity};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct TwoTargetProps {
|
// pub struct TwoTargetProps {
|
||||||
pub targets: Box<[CharacterIdentity]>,
|
// pub targets: Box<[CharacterIdentity]>,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub headline: &'static str,
|
// pub headline: &'static str,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
|
// pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[derive(Clone)]
|
// #[derive(Clone)]
|
||||||
enum TwoTargetSelection {
|
// enum TwoTargetSelection {
|
||||||
None,
|
// None,
|
||||||
One(CharacterId),
|
// One(CharacterId),
|
||||||
Two(CharacterId, CharacterId),
|
// Two(CharacterId, CharacterId),
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl TwoTargetSelection {
|
// impl TwoTargetSelection {
|
||||||
fn is_selected(&self, id: &CharacterId) -> bool {
|
// fn is_selected(&self, id: &CharacterId) -> bool {
|
||||||
match self {
|
// match self {
|
||||||
TwoTargetSelection::None => false,
|
// TwoTargetSelection::None => false,
|
||||||
TwoTargetSelection::One(character_id) => id == character_id,
|
// TwoTargetSelection::One(character_id) => id == character_id,
|
||||||
TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2,
|
// TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2,
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub struct TwoTarget(TwoTargetSelection);
|
// pub struct TwoTarget(TwoTargetSelection);
|
||||||
|
|
||||||
impl Component for TwoTarget {
|
// impl Component for TwoTarget {
|
||||||
type Message = CharacterId;
|
// type Message = CharacterId;
|
||||||
|
|
||||||
type Properties = TwoTargetProps;
|
// type Properties = TwoTargetProps;
|
||||||
|
|
||||||
fn create(_: &Context<Self>) -> Self {
|
// fn create(_: &Context<Self>) -> Self {
|
||||||
Self(TwoTargetSelection::None)
|
// Self(TwoTargetSelection::None)
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
// fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let TwoTargetProps {
|
// let TwoTargetProps {
|
||||||
targets,
|
// targets,
|
||||||
headline,
|
// headline,
|
||||||
target_selection,
|
// target_selection,
|
||||||
} = ctx.props();
|
// } = ctx.props();
|
||||||
let mut targets = targets.clone();
|
// let mut targets = targets.clone();
|
||||||
targets.sort_by(|l, r| l.number.cmp(&r.number));
|
// targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||||
|
|
||||||
let target_selection = target_selection.clone();
|
// let target_selection = target_selection.clone();
|
||||||
let scope = ctx.link().clone();
|
// let scope = ctx.link().clone();
|
||||||
let card_select = Callback::from(move |target| {
|
// let card_select = Callback::from(move |target| {
|
||||||
scope.send_message(target);
|
// scope.send_message(target);
|
||||||
});
|
// });
|
||||||
let targets = targets
|
// let targets = targets
|
||||||
.iter()
|
// .iter()
|
||||||
.map(|t| {
|
// .map(|t| {
|
||||||
html! {
|
// html! {
|
||||||
<TargetCard
|
// <TargetCard
|
||||||
target={t.clone()}
|
// target={t.clone()}
|
||||||
selected={self.0.is_selected(&t.character_id)}
|
// selected={self.0.is_selected(&t.character_id)}
|
||||||
on_select={card_select.clone()}
|
// on_select={card_select.clone()}
|
||||||
/>
|
// />
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
.collect::<Html>();
|
// .collect::<Html>();
|
||||||
let headline = headline
|
// let headline = headline
|
||||||
.trim()
|
// .trim()
|
||||||
.is_empty()
|
// .is_empty()
|
||||||
.not()
|
// .not()
|
||||||
.then(|| html!(<h2>{headline}</h2>));
|
// .then(|| html!(<h2>{headline}</h2>));
|
||||||
|
|
||||||
let submit = target_selection.as_ref().map(|target_selection| {
|
// let submit = target_selection.as_ref().map(|target_selection| {
|
||||||
let selected = match &self.0 {
|
// let selected = match &self.0 {
|
||||||
TwoTargetSelection::None | TwoTargetSelection::One(_) => None,
|
// TwoTargetSelection::None | TwoTargetSelection::One(_) => None,
|
||||||
TwoTargetSelection::Two(t1, t2) => Some((*t1, *t2)),
|
// TwoTargetSelection::Two(t1, t2) => Some((*t1, *t2)),
|
||||||
};
|
// };
|
||||||
let target_selection = target_selection.clone();
|
// let target_selection = target_selection.clone();
|
||||||
let disabled = selected.is_none();
|
// let disabled = selected.is_none();
|
||||||
let on_click =
|
// let on_click =
|
||||||
selected.map(|(t1, t2)| move |_| target_selection.emit((t1, t2)));
|
// selected.map(|(t1, t2)| move |_| target_selection.emit((t1, t2)));
|
||||||
html! {
|
// html! {
|
||||||
<div class="button-container sp-ace">
|
// <div class="button-container sp-ace">
|
||||||
<button
|
// <button
|
||||||
disabled={disabled}
|
// disabled={disabled}
|
||||||
onclick={on_click}
|
// onclick={on_click}
|
||||||
>
|
// >
|
||||||
{"submit"}
|
// {"submit"}
|
||||||
</button>
|
// </button>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
html! {
|
// html! {
|
||||||
<div class="column-list">
|
// <div class="column-list">
|
||||||
{headline}
|
// {headline}
|
||||||
<div class="row-list">
|
// <div class="row-list">
|
||||||
{targets}
|
// {targets}
|
||||||
</div>
|
// </div>
|
||||||
{submit}
|
// {submit}
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
// fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match &self.0 {
|
// match &self.0 {
|
||||||
TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
|
// TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
|
||||||
TwoTargetSelection::One(character_id) => {
|
// TwoTargetSelection::One(character_id) => {
|
||||||
if character_id == &msg {
|
// if character_id == &msg {
|
||||||
self.0 = TwoTargetSelection::None
|
// self.0 = TwoTargetSelection::None
|
||||||
} else {
|
// } else {
|
||||||
self.0 = TwoTargetSelection::Two(*character_id, msg)
|
// self.0 = TwoTargetSelection::Two(*character_id, msg)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
TwoTargetSelection::Two(t1, t2) => {
|
// TwoTargetSelection::Two(t1, t2) => {
|
||||||
if &msg == t1 {
|
// if &msg == t1 {
|
||||||
self.0 = TwoTargetSelection::One(*t2);
|
// self.0 = TwoTargetSelection::One(*t2);
|
||||||
} else if &msg == t2 {
|
// } else if &msg == t2 {
|
||||||
self.0 = TwoTargetSelection::One(*t1);
|
// self.0 = TwoTargetSelection::One(*t1);
|
||||||
} else {
|
// } else {
|
||||||
self.0 = TwoTargetSelection::Two(*t1, msg);
|
// self.0 = TwoTargetSelection::Two(*t1, msg);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct OptionalSingleTargetProps {
|
// pub struct OptionalSingleTargetProps {
|
||||||
pub targets: Box<[CharacterIdentity]>,
|
// pub targets: Box<[CharacterIdentity]>,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub headline: &'static str,
|
// pub headline: &'static str,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub target_selection: Option<Callback<Option<CharacterId>>>,
|
// pub target_selection: Option<Callback<Option<CharacterId>>>,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub children: Html,
|
// pub children: Html,
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub struct OptionalSingleTarget(Option<CharacterId>);
|
// pub struct OptionalSingleTarget(Option<CharacterId>);
|
||||||
|
|
||||||
impl Component for OptionalSingleTarget {
|
// impl Component for OptionalSingleTarget {
|
||||||
type Message = CharacterId;
|
// type Message = CharacterId;
|
||||||
|
|
||||||
type Properties = OptionalSingleTargetProps;
|
// type Properties = OptionalSingleTargetProps;
|
||||||
|
|
||||||
fn create(_: &Context<Self>) -> Self {
|
// fn create(_: &Context<Self>) -> Self {
|
||||||
Self(None)
|
// Self(None)
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
// fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let OptionalSingleTargetProps {
|
// let OptionalSingleTargetProps {
|
||||||
targets,
|
// targets,
|
||||||
headline,
|
// headline,
|
||||||
target_selection,
|
// target_selection,
|
||||||
children,
|
// children,
|
||||||
} = ctx.props();
|
// } = ctx.props();
|
||||||
let mut targets = targets.clone();
|
// let mut targets = targets.clone();
|
||||||
targets.sort_by(|l, r| l.number.cmp(&r.number));
|
// targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||||
|
|
||||||
let target_selection = target_selection.clone();
|
// let target_selection = target_selection.clone();
|
||||||
let scope = ctx.link().clone();
|
// let scope = ctx.link().clone();
|
||||||
let card_select = Callback::from(move |target| {
|
// let card_select = Callback::from(move |target| {
|
||||||
scope.send_message(target);
|
// scope.send_message(target);
|
||||||
});
|
// });
|
||||||
let targets = targets
|
// let targets = targets
|
||||||
.iter()
|
// .iter()
|
||||||
.map(|t| {
|
// .map(|t| {
|
||||||
html! {
|
// html! {
|
||||||
<TargetCard
|
// <TargetCard
|
||||||
target={t.clone()}
|
// target={t.clone()}
|
||||||
selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
|
// selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
|
||||||
on_select={card_select.clone()}
|
// on_select={card_select.clone()}
|
||||||
/>
|
// />
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
.collect::<Html>();
|
// .collect::<Html>();
|
||||||
let headline = headline
|
// let headline = headline
|
||||||
.trim()
|
// .trim()
|
||||||
.is_empty()
|
// .is_empty()
|
||||||
.not()
|
// .not()
|
||||||
.then(|| html!(<h2>{headline}</h2>));
|
// .then(|| html!(<h2>{headline}</h2>));
|
||||||
|
|
||||||
let submit = target_selection.as_ref().map(|target_selection| {
|
// let submit = target_selection.as_ref().map(|target_selection| {
|
||||||
let target_selection = target_selection.clone();
|
// let target_selection = target_selection.clone();
|
||||||
let sel = self.0;
|
// let sel = self.0;
|
||||||
let on_click = move |_| target_selection.emit(sel);
|
// let on_click = move |_| target_selection.emit(sel);
|
||||||
html! {
|
// html! {
|
||||||
<div class="button-container sp-ace">
|
// <div class="button-container sp-ace">
|
||||||
<button
|
// <button
|
||||||
onclick={on_click}
|
// onclick={on_click}
|
||||||
>
|
// >
|
||||||
{"submit"}
|
// {"submit"}
|
||||||
</button>
|
// </button>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
html! {
|
// html! {
|
||||||
<div class="column-list">
|
// <div class="column-list">
|
||||||
{headline}
|
// {headline}
|
||||||
{children.clone()}
|
// {children.clone()}
|
||||||
<div class="row-list">
|
// <div class="row-list">
|
||||||
{targets}
|
// {targets}
|
||||||
</div>
|
// </div>
|
||||||
{submit}
|
// {submit}
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
// fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match &self.0 {
|
// match &self.0 {
|
||||||
Some(t) => {
|
// Some(t) => {
|
||||||
if t == &msg {
|
// if t == &msg {
|
||||||
self.0 = None
|
// self.0 = None
|
||||||
} else {
|
// } else {
|
||||||
self.0 = Some(msg);
|
// self.0 = Some(msg);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
None => self.0 = Some(msg),
|
// None => self.0 = Some(msg),
|
||||||
}
|
// }
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct SingleTargetProps {
|
// pub struct SingleTargetProps {
|
||||||
pub targets: Box<[CharacterIdentity]>,
|
// pub targets: Box<[CharacterIdentity]>,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub headline: &'static str,
|
// pub headline: &'static str,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub target_selection: Option<Callback<CharacterId>>,
|
// pub target_selection: Option<Callback<CharacterId>>,
|
||||||
#[prop_or_default]
|
// #[prop_or_default]
|
||||||
pub children: Html,
|
// pub children: Html,
|
||||||
}
|
// }
|
||||||
|
|
||||||
pub struct SingleTarget {
|
// pub struct SingleTarget {
|
||||||
selected: Option<CharacterId>,
|
// selected: Option<CharacterId>,
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl Component for SingleTarget {
|
// impl Component for SingleTarget {
|
||||||
type Message = CharacterId;
|
// type Message = CharacterId;
|
||||||
|
|
||||||
type Properties = SingleTargetProps;
|
// type Properties = SingleTargetProps;
|
||||||
|
|
||||||
fn create(_: &Context<Self>) -> Self {
|
// fn create(_: &Context<Self>) -> Self {
|
||||||
Self { selected: None }
|
// Self { selected: None }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
// fn view(&self, ctx: &Context<Self>) -> Html {
|
||||||
let SingleTargetProps {
|
// let SingleTargetProps {
|
||||||
headline,
|
// headline,
|
||||||
targets,
|
// targets,
|
||||||
target_selection,
|
// target_selection,
|
||||||
children,
|
// children,
|
||||||
} = ctx.props();
|
// } = ctx.props();
|
||||||
let mut targets = targets.clone();
|
// let mut targets = targets.clone();
|
||||||
targets.sort_by(|l, r| l.number.cmp(&r.number));
|
// targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||||
let target_selection = target_selection.clone();
|
// let target_selection = target_selection.clone();
|
||||||
let scope = ctx.link().clone();
|
// let scope = ctx.link().clone();
|
||||||
let card_select = Callback::from(move |target| {
|
// let card_select = Callback::from(move |target| {
|
||||||
scope.send_message(target);
|
// scope.send_message(target);
|
||||||
});
|
// });
|
||||||
let targets = targets
|
// let targets = targets
|
||||||
.iter()
|
// .iter()
|
||||||
.map(|t| {
|
// .map(|t| {
|
||||||
html! {
|
// html! {
|
||||||
<TargetCard
|
// <TargetCard
|
||||||
target={t.clone()}
|
// target={t.clone()}
|
||||||
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
// selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
||||||
on_select={card_select.clone()}
|
// on_select={card_select.clone()}
|
||||||
/>
|
// />
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
.collect::<Html>();
|
// .collect::<Html>();
|
||||||
let headline = headline
|
// let headline = headline
|
||||||
.trim()
|
// .trim()
|
||||||
.is_empty()
|
// .is_empty()
|
||||||
.not()
|
// .not()
|
||||||
.then(|| html!(<h2>{headline}</h2>));
|
// .then(|| html!(<h2>{headline}</h2>));
|
||||||
|
|
||||||
let submit = target_selection.as_ref().map(|target_selection| {
|
// let submit = target_selection.as_ref().map(|target_selection| {
|
||||||
let disabled = self.selected.is_none().then_some("pick a target");
|
// let disabled = self.selected.is_none().then_some("pick a target");
|
||||||
let target_selection = target_selection.clone();
|
// let target_selection = target_selection.clone();
|
||||||
let on_click = self
|
// let on_click = self
|
||||||
.selected
|
// .selected
|
||||||
.map(|t| Callback::from(move |_| target_selection.emit(t)))
|
// .map(|t| Callback::from(move |_| target_selection.emit(t)))
|
||||||
.unwrap_or_default();
|
// .unwrap_or_default();
|
||||||
html! {
|
// html! {
|
||||||
<div class="button-container sp-ace">
|
// <div class="button-container sp-ace">
|
||||||
<Button disabled_reason={disabled} on_click={on_click}>
|
// <Button disabled_reason={disabled} on_click={on_click}>
|
||||||
{"submit"}
|
// {"submit"}
|
||||||
</Button>
|
// </Button>
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
});
|
// });
|
||||||
|
|
||||||
html! {
|
// html! {
|
||||||
<div class="character-picker">
|
// <div class="character-picker">
|
||||||
{headline}
|
// {headline}
|
||||||
{children.clone()}
|
// {children.clone()}
|
||||||
<div class="row-list">
|
// <div class="row-list">
|
||||||
{targets}
|
// {targets}
|
||||||
</div>
|
// </div>
|
||||||
{submit}
|
// {submit}
|
||||||
</div>
|
// </div>
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
// fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match &self.selected {
|
// match &self.selected {
|
||||||
Some(current) => {
|
// Some(current) => {
|
||||||
if current == &msg {
|
// if current == &msg {
|
||||||
self.selected = None;
|
// self.selected = None;
|
||||||
} else {
|
// } else {
|
||||||
self.selected = Some(msg);
|
// self.selected = Some(msg);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
None => self.selected = Some(msg),
|
// None => self.selected = Some(msg),
|
||||||
}
|
// }
|
||||||
true
|
// true
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct TargetCardProps {
|
// pub struct TargetCardProps {
|
||||||
pub target: CharacterIdentity,
|
// pub target: CharacterIdentity,
|
||||||
pub selected: bool,
|
// pub selected: bool,
|
||||||
pub on_select: Callback<CharacterId>,
|
// pub on_select: Callback<CharacterId>,
|
||||||
}
|
// }
|
||||||
|
|
||||||
#[function_component]
|
// #[function_component]
|
||||||
fn TargetCard(props: &TargetCardProps) -> Html {
|
// fn TargetCard(props: &TargetCardProps) -> Html {
|
||||||
let character_id = props.target.character_id;
|
// let character_id = props.target.character_id;
|
||||||
let on_select = props.on_select.clone();
|
// let on_select = props.on_select.clone();
|
||||||
let on_click = Callback::from(move |_| on_select.emit(character_id));
|
// let on_click = Callback::from(move |_| on_select.emit(character_id));
|
||||||
|
|
||||||
let marked = props.selected.then_some("marked");
|
// let marked = props.selected.then_some("marked");
|
||||||
let ident: PublicIdentity = props.target.clone().into();
|
// let ident: PublicIdentity = props.target.clone().into();
|
||||||
html! {
|
// html! {
|
||||||
<Button on_click={on_click} classes={classes!(marked, "character")}>
|
// <Button on_click={on_click} classes={classes!(marked, "character")}>
|
||||||
<Identity ident={ident}/>
|
// <Identity ident={ident}/>
|
||||||
</Button>
|
// </Button>
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
use core::{num::NonZeroU8, ops::Not};
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
character::CharacterId,
|
||||||
message::{CharacterState, PublicIdentity},
|
message::{CharacterState, PublicIdentity},
|
||||||
player::CharacterId,
|
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
use core::ops::Not;
|
use core::ops::Not;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
game::{Category, GameSettings, SetupRole, SetupRoleTitle},
|
game::{Category, GameSettings, SetupRole, SetupRoleTitle},
|
||||||
|
|
@ -133,7 +134,7 @@ pub fn SetupCategory(
|
||||||
<div class={classes!("slot")}>
|
<div class={classes!("slot")}>
|
||||||
<div class={classes!("role", wakes, r.category().class())}>
|
<div class={classes!("role", wakes, r.category().class())}>
|
||||||
{count}
|
{count}
|
||||||
{r.to_string()}
|
{r.to_string().to_case(Case::Title)}
|
||||||
</div>
|
</div>
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
<div class="alignment">
|
<div class="alignment">
|
||||||
|
|
@ -153,7 +154,9 @@ pub fn SetupCategory(
|
||||||
html! {
|
html! {
|
||||||
<div class="category">
|
<div class="category">
|
||||||
{roles_count}
|
{roles_count}
|
||||||
<div class={classes!("title", category.class())}>{category.to_string()}</div>
|
<div class={classes!("title", category.class())}>
|
||||||
|
{category.to_string().to_case(Case::Title)}
|
||||||
|
</div>
|
||||||
<div class={classes!("category-list")}>
|
<div class={classes!("category-list")}>
|
||||||
{all_roles}
|
{all_roles}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,59 @@ fn setup_options_for_slot(
|
||||||
slot_field_open: UseStateHandle<bool>,
|
slot_field_open: UseStateHandle<bool>,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let setup_options_for_role = match &slot.role {
|
let setup_options_for_role = match &slot.role {
|
||||||
|
SetupRole::MasonLeader { recruits_available } => {
|
||||||
|
let next = {
|
||||||
|
let mut s = slot.clone();
|
||||||
|
match &mut s.role {
|
||||||
|
SetupRole::MasonLeader { recruits_available } => {
|
||||||
|
*recruits_available =
|
||||||
|
NonZeroU8::new(recruits_available.get().checked_add(1).unwrap_or(1))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
s
|
||||||
|
};
|
||||||
|
let prev = recruits_available
|
||||||
|
.get()
|
||||||
|
.checked_sub(1)
|
||||||
|
.and_then(NonZeroU8::new)
|
||||||
|
.map(|new_avail| {
|
||||||
|
let mut s = slot.clone();
|
||||||
|
match &mut s.role {
|
||||||
|
SetupRole::MasonLeader { recruits_available } => {
|
||||||
|
*recruits_available = new_avail
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
s
|
||||||
|
});
|
||||||
|
let increment_update = update.clone();
|
||||||
|
let on_increment = Callback::from(move |_| {
|
||||||
|
increment_update.emit(SettingSlotAction::Update(next.clone()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let decrement_update = update.clone();
|
||||||
|
let on_decrement = prev
|
||||||
|
.clone()
|
||||||
|
.map(|prev| {
|
||||||
|
Callback::from(move |_| {
|
||||||
|
decrement_update.emit(SettingSlotAction::Update(prev.clone()))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or_default();
|
||||||
|
let decrement_disabled_reason = prev.is_none().then_some("at minimum");
|
||||||
|
Some(html! {
|
||||||
|
<>
|
||||||
|
<label>{"recruits"}</label>
|
||||||
|
<div class={classes!("increment-decrement")}>
|
||||||
|
<Button on_click={on_decrement} disabled_reason={decrement_disabled_reason}>{"-"}</Button>
|
||||||
|
<label>{recruits_available.get().to_string()}</label>
|
||||||
|
<Button on_click={on_increment}>{"+"}</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
})
|
||||||
|
}
|
||||||
SetupRole::Scapegoat { redeemed } => {
|
SetupRole::Scapegoat { redeemed } => {
|
||||||
let next = {
|
let next = {
|
||||||
let next_redeemed = match redeemed {
|
let next_redeemed = match redeemed {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue