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 syn::{parse::Parse, parse_macro_input};
|
||||
|
||||
use crate::ref_and_mut::RefAndMut;
|
||||
|
||||
mod all;
|
||||
mod checks;
|
||||
pub(crate) mod hashlist;
|
||||
mod ref_and_mut;
|
||||
mod targets;
|
||||
|
||||
struct IncludePath {
|
||||
|
|
@ -551,3 +554,9 @@ pub fn all(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|||
|
||||
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 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 {
|
||||
Execution {
|
||||
day: NonZeroU8,
|
||||
},
|
||||
#[extract(source as killer)]
|
||||
MapleWolf {
|
||||
source: CharacterId,
|
||||
night: NonZeroU8,
|
||||
|
|
@ -19,17 +18,14 @@ pub enum DiedTo {
|
|||
MapleWolfStarved {
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killer as killer)]
|
||||
Militia {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killing_wolf as killer)]
|
||||
Wolfpack {
|
||||
killing_wolf: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killer as killer)]
|
||||
AlphaWolf {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
|
|
@ -38,12 +34,10 @@ pub enum DiedTo {
|
|||
into: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(killer as killer)]
|
||||
Hunter {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
#[extract(source as killer)]
|
||||
GuardianProtecting {
|
||||
source: CharacterId,
|
||||
protecting: CharacterId,
|
||||
|
|
@ -51,13 +45,44 @@ pub enum DiedTo {
|
|||
protecting_from_cause: Box<DiedTo>,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
PyreMaster {
|
||||
killer: CharacterId,
|
||||
night: NonZeroU8,
|
||||
},
|
||||
MasonLeaderRecruitFail {
|
||||
tried_recruiting: CharacterId,
|
||||
night: u8,
|
||||
},
|
||||
}
|
||||
|
||||
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 {
|
||||
match self {
|
||||
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
||||
|
||||
DiedTo::GuardianProtecting {
|
||||
source: _,
|
||||
protecting: _,
|
||||
|
|
@ -78,9 +103,11 @@ impl DiedTo {
|
|||
}
|
||||
| DiedTo::AlphaWolf { killer: _, night }
|
||||
| DiedTo::Shapeshift { into: _, night }
|
||||
| DiedTo::PyreMaster { night, .. }
|
||||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
||||
number: night.get(),
|
||||
},
|
||||
DiedTo::MasonLeaderRecruitFail { night, .. } => DateTime::Night { number: *night },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ use core::{num::NonZeroU8, ops::Not};
|
|||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{Village, night::NightChange},
|
||||
player::{CharacterId, Protection},
|
||||
player::Protection,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
|
|
@ -24,10 +25,7 @@ impl KillOutcome {
|
|||
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
|
||||
match self {
|
||||
KillOutcome::Single(character_id, died_to) => {
|
||||
village
|
||||
.character_by_id_mut(character_id)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(died_to);
|
||||
village.character_by_id_mut(character_id)?.kill(died_to);
|
||||
Ok(())
|
||||
}
|
||||
KillOutcome::Guarding {
|
||||
|
|
@ -39,12 +37,9 @@ impl KillOutcome {
|
|||
} => {
|
||||
// check if guardian exists before we mutably borrow killer, which would
|
||||
// prevent us from borrowing village to check after.
|
||||
village.character_by_id(guardian)?;
|
||||
village
|
||||
.character_by_id(guardian)
|
||||
.ok_or(GameError::InvalidTarget)?;
|
||||
village
|
||||
.character_by_id_mut(original_killer)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.character_by_id_mut(original_killer)?
|
||||
.kill(DiedTo::GuardianProtecting {
|
||||
night,
|
||||
source: guardian,
|
||||
|
|
@ -52,10 +47,7 @@ impl KillOutcome {
|
|||
protecting_from: original_killer,
|
||||
protecting_from_cause: Box::new(original_kill.clone()),
|
||||
});
|
||||
village
|
||||
.character_by_id_mut(guardian)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.kill(original_kill);
|
||||
village.character_by_id_mut(guardian)?.kill(original_kill);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -84,6 +76,7 @@ fn resolve_protection(
|
|||
source: _,
|
||||
guarding: false,
|
||||
}
|
||||
| Protection::Vindicator { .. }
|
||||
| Protection::Protector { source: _ } => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -114,9 +107,7 @@ pub fn resolve_kill(
|
|||
} = died_to
|
||||
&& let Some(ss_source) = changes.shapeshifter()
|
||||
{
|
||||
let killing_wolf = village
|
||||
.character_by_id(*killing_wolf)
|
||||
.ok_or(GameError::InvalidTarget)?;
|
||||
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
||||
|
||||
match changes.protected_take(target) {
|
||||
Some(protection) => {
|
||||
|
|
@ -151,7 +142,7 @@ pub fn resolve_kill(
|
|||
source,
|
||||
guarding: true,
|
||||
} => Ok(Some(KillOutcome::Guarding {
|
||||
original_killer: *died_to
|
||||
original_killer: died_to
|
||||
.killer()
|
||||
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
||||
original_target: *target,
|
||||
|
|
@ -160,10 +151,10 @@ pub fn resolve_kill(
|
|||
night: NonZeroU8::new(night).unwrap(),
|
||||
})),
|
||||
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 crate::{
|
||||
character::CharacterId,
|
||||
error::GameError,
|
||||
game::night::{Night, ServerAction},
|
||||
message::{
|
||||
CharacterState, Identification,
|
||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
},
|
||||
player::CharacterId,
|
||||
};
|
||||
|
||||
pub use {
|
||||
|
|
@ -98,7 +98,7 @@ impl Game {
|
|||
.map(|c| CharacterState {
|
||||
player_id: c.player_id(),
|
||||
identity: c.identity(),
|
||||
role: c.role().title(),
|
||||
role: c.role_title(),
|
||||
died_to: c.died_to().cloned(),
|
||||
})
|
||||
.collect(),
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ use werewolves_macros::Extract;
|
|||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
character::{Character, CharacterId},
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{
|
||||
|
|
@ -13,8 +14,8 @@ use crate::{
|
|||
kill::{self, ChangesLookup},
|
||||
},
|
||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
player::{Character, CharacterId, Protection},
|
||||
role::{PreviousGuardianAction, Role, RoleBlock, RoleTitle},
|
||||
player::Protection,
|
||||
role::{PreviousGuardianAction, RoleBlock, RoleTitle},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Extract)]
|
||||
|
|
@ -43,6 +44,14 @@ pub enum NightChange {
|
|||
ElderReveal {
|
||||
elder: CharacterId,
|
||||
},
|
||||
MasonRecruit {
|
||||
mason_leader: CharacterId,
|
||||
recruiting: CharacterId,
|
||||
},
|
||||
EmpathFoundScapegoat {
|
||||
empath: CharacterId,
|
||||
scapegoat: CharacterId,
|
||||
},
|
||||
}
|
||||
|
||||
enum BlockResolvedOutcome {
|
||||
|
|
@ -58,7 +67,134 @@ enum ResponseOutcome {
|
|||
struct ActionComplete {
|
||||
pub result: ActionResult,
|
||||
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 {
|
||||
|
|
@ -66,7 +202,6 @@ impl Default for ActionComplete {
|
|||
Self {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +234,7 @@ pub struct Night {
|
|||
village: Village,
|
||||
night: u8,
|
||||
action_queue: VecDeque<ActionPrompt>,
|
||||
used_actions: Vec<ActionPrompt>,
|
||||
used_actions: Vec<(ActionPrompt, ActionResult)>,
|
||||
changes: Vec<NightChange>,
|
||||
night_state: NightState,
|
||||
}
|
||||
|
|
@ -122,7 +257,7 @@ impl Night {
|
|||
.characters()
|
||||
.into_iter()
|
||||
.filter(filter)
|
||||
.map(|c| c.night_action_prompt(&village))
|
||||
.map(|c| c.night_action_prompts(&village))
|
||||
.collect::<Result<Box<[_]>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
|
|
@ -143,7 +278,7 @@ impl Night {
|
|||
wolves: village
|
||||
.living_wolf_pack_players()
|
||||
.into_iter()
|
||||
.map(|w| (w.identity(), w.role().title()))
|
||||
.map(|w| (w.identity(), w.role_title()))
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
|
|
@ -177,10 +312,7 @@ impl Night {
|
|||
.dead_characters()
|
||||
.into_iter()
|
||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||
.filter_map(|(c, d)| match c.role() {
|
||||
Role::Hunter { target } => (*target).map(|t| (c, t, d)),
|
||||
_ => None,
|
||||
})
|
||||
.filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d)))
|
||||
.filter_map(|(c, t, d)| match d.date_time() {
|
||||
DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)),
|
||||
DateTime::Night { number: _ } => None,
|
||||
|
|
@ -199,7 +331,8 @@ impl Night {
|
|||
}
|
||||
|
||||
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:?}");
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
|
|
@ -246,20 +379,15 @@ impl Night {
|
|||
let mut changes = ChangesLookup::new(&self.changes);
|
||||
for change in self.changes.iter() {
|
||||
match change {
|
||||
NightChange::ElderReveal { elder } => new_village
|
||||
.character_by_id_mut(*elder)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.elder_reveal(),
|
||||
NightChange::ElderReveal { elder } => {
|
||||
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
||||
}
|
||||
NightChange::RoleChange(character_id, role_title) => new_village
|
||||
.character_by_id_mut(*character_id)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.character_by_id_mut(*character_id)?
|
||||
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
||||
NightChange::HunterTarget { source, target } => {
|
||||
if let Role::Hunter { target: t } =
|
||||
new_village.character_by_id_mut(*source).unwrap().role_mut()
|
||||
{
|
||||
t.replace(*target);
|
||||
}
|
||||
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
||||
hunter_character.hunter_mut()?.replace(*target);
|
||||
if changes.killed(source).is_some()
|
||||
&& changes.protected(source).is_none()
|
||||
&& changes.protected(target).is_none()
|
||||
|
|
@ -289,12 +417,7 @@ impl Night {
|
|||
&& changes.protected(target).is_none()
|
||||
{
|
||||
let ss = new_village.character_by_id_mut(*source).unwrap();
|
||||
match ss.role_mut() {
|
||||
Role::Shapeshifter { shifted_into } => {
|
||||
*shifted_into = Some(*target)
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
ss.shapeshifter_mut().unwrap().replace(*target);
|
||||
ss.kill(DiedTo::Shapeshift {
|
||||
into: *target,
|
||||
night: NonZeroU8::new(self.night).unwrap(),
|
||||
|
|
@ -311,6 +434,30 @@ impl Night {
|
|||
target: _,
|
||||
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() {
|
||||
|
|
@ -319,6 +466,34 @@ impl Night {
|
|||
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<()> {
|
||||
if let Some(kill_target) = self.changes.iter().find_map(|c| match c {
|
||||
NightChange::Kill {
|
||||
|
|
@ -362,16 +537,11 @@ impl Night {
|
|||
},
|
||||
});
|
||||
}
|
||||
self.changes.push(NightChange::Shapeshift {
|
||||
source: *source,
|
||||
});
|
||||
self.changes
|
||||
.push(NightChange::Shapeshift { source: *source });
|
||||
self.action_queue.push_front(ActionPrompt::RoleChange {
|
||||
new_role: RoleTitle::Werewolf,
|
||||
character_id: self
|
||||
.village
|
||||
.character_by_id(kill_target)
|
||||
.ok_or(GameError::NoMatchingCharacterFound)?
|
||||
.identity(),
|
||||
character_id: self.village.character_by_id(kill_target)?.identity(),
|
||||
});
|
||||
}
|
||||
// Remove any further shapeshift prompts from the queue
|
||||
|
|
@ -399,7 +569,7 @@ impl Night {
|
|||
}
|
||||
NightState::Complete => Err(GameError::NightOver),
|
||||
},
|
||||
BlockResolvedOutcome::ActionComplete(result, Some(change)) => {
|
||||
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
||||
match &mut self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
|
|
@ -419,6 +589,13 @@ impl Night {
|
|||
.unwrap_or(ActionResult::GoBackToSleep),
|
||||
));
|
||||
}
|
||||
if let NightChange::MasonRecruit {
|
||||
mason_leader,
|
||||
recruiting,
|
||||
} = &change
|
||||
{
|
||||
result = self.apply_mason_recruit(*mason_leader, *recruiting)?;
|
||||
}
|
||||
self.changes.push(change);
|
||||
Ok(ServerAction::Result(result))
|
||||
}
|
||||
|
|
@ -461,7 +638,6 @@ impl Night {
|
|||
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Continue,
|
||||
change: None,
|
||||
unless: None,
|
||||
}));
|
||||
}
|
||||
|
||||
|
|
@ -475,27 +651,23 @@ impl Night {
|
|||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Shapeshift { source }),
|
||||
unless,
|
||||
}),
|
||||
true,
|
||||
_,
|
||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Continue,
|
||||
change: Some(NightChange::Shapeshift { source }),
|
||||
unless,
|
||||
})),
|
||||
(
|
||||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change,
|
||||
unless,
|
||||
}),
|
||||
true,
|
||||
true,
|
||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Continue,
|
||||
change,
|
||||
unless,
|
||||
})),
|
||||
(outcome, _, _) => Ok(outcome),
|
||||
}
|
||||
|
|
@ -507,11 +679,9 @@ impl Night {
|
|||
) -> Result<BlockResolvedOutcome> {
|
||||
match self.received_response_consecutive_wolves_dont_sleep(resp)? {
|
||||
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
||||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result,
|
||||
change,
|
||||
unless: Some(Unless::TargetBlocked(unless_blocked)),
|
||||
}) => {
|
||||
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
|
||||
match self.current_prompt().ok_or(GameError::NightOver)?.unless() {
|
||||
Some(Unless::TargetBlocked(unless_blocked)) => {
|
||||
if self.changes.iter().any(|c| match c {
|
||||
NightChange::RoleBlock {
|
||||
source: _,
|
||||
|
|
@ -528,11 +698,7 @@ impl Night {
|
|||
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
||||
}
|
||||
}
|
||||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result,
|
||||
change,
|
||||
unless: Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)),
|
||||
}) => {
|
||||
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
|
||||
if self.changes.iter().any(|c| match c {
|
||||
NightChange::RoleBlock {
|
||||
source: _,
|
||||
|
|
@ -549,12 +715,9 @@ impl Night {
|
|||
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
||||
}
|
||||
}
|
||||
|
||||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result,
|
||||
change,
|
||||
unless: None,
|
||||
}) => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
|
||||
None => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -586,7 +749,6 @@ impl Night {
|
|||
change: Some(NightChange::Shapeshift {
|
||||
source: source.character_id,
|
||||
}),
|
||||
unless: None,
|
||||
})),
|
||||
_ => Err(GameError::InvalidMessageForGameState),
|
||||
};
|
||||
|
|
@ -603,7 +765,6 @@ impl Night {
|
|||
character_id.character_id,
|
||||
*new_role,
|
||||
)),
|
||||
unless: None,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
@ -616,7 +777,6 @@ impl Night {
|
|||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
unless: None,
|
||||
}))
|
||||
}
|
||||
ActionPrompt::ElderReveal { character_id } => {
|
||||
|
|
@ -625,22 +785,16 @@ impl Night {
|
|||
change: Some(NightChange::ElderReveal {
|
||||
elder: character_id.character_id,
|
||||
}),
|
||||
unless: None,
|
||||
}))
|
||||
}
|
||||
ActionPrompt::Seer {
|
||||
marked: Some(marked),
|
||||
..
|
||||
} => {
|
||||
let alignment = self
|
||||
.village
|
||||
.character_by_id(*marked)
|
||||
.ok_or(GameError::InvalidTarget)?
|
||||
.alignment();
|
||||
let alignment = self.village.character_by_id(*marked)?.alignment();
|
||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Seer(alignment),
|
||||
change: None,
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
}))
|
||||
}
|
||||
ActionPrompt::Protector {
|
||||
|
|
@ -655,42 +809,27 @@ impl Night {
|
|||
source: character_id.character_id,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::Arcanist {
|
||||
marked: (Some(marked1), Some(marked2)),
|
||||
..
|
||||
} => {
|
||||
let same = self
|
||||
.village
|
||||
.character_by_id(*marked1)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?
|
||||
.alignment()
|
||||
== self
|
||||
.village
|
||||
.character_by_id(*marked2)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?
|
||||
.alignment();
|
||||
let same = self.village.character_by_id(*marked1)?.alignment()
|
||||
== self.village.character_by_id(*marked2)?.alignment();
|
||||
|
||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Arcanist { same },
|
||||
change: None,
|
||||
unless: Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
||||
}))
|
||||
}
|
||||
ActionPrompt::Gravedigger {
|
||||
marked: Some(marked),
|
||||
..
|
||||
} => {
|
||||
let dig_role = self
|
||||
.village
|
||||
.character_by_id(*marked)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?
|
||||
.gravedigger_dig();
|
||||
let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig();
|
||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::GraveDigger(dig_role),
|
||||
change: None,
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
}))
|
||||
}
|
||||
ActionPrompt::Hunter {
|
||||
|
|
@ -703,7 +842,6 @@ impl Night {
|
|||
source: character_id.character_id,
|
||||
target: *marked,
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::Militia {
|
||||
character_id,
|
||||
|
|
@ -719,7 +857,6 @@ impl Night {
|
|||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::Militia { marked: None, .. } => {
|
||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||
|
|
@ -740,7 +877,6 @@ impl Night {
|
|||
starves_if_fails: *kill_or_die,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::MapleWolf { marked: None, .. } => {
|
||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||
|
|
@ -759,7 +895,6 @@ impl Night {
|
|||
guarding: false,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::Guardian {
|
||||
character_id,
|
||||
|
|
@ -779,7 +914,6 @@ impl Night {
|
|||
guarding: false,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
}))
|
||||
}
|
||||
ActionPrompt::Guardian {
|
||||
|
|
@ -796,7 +930,6 @@ impl Night {
|
|||
guarding: prev_protect.character_id == *marked,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::WolfPackKill {
|
||||
marked: Some(marked),
|
||||
|
|
@ -815,7 +948,6 @@ impl Night {
|
|||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::Shapeshifter { character_id } => {
|
||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
|
|
@ -823,7 +955,6 @@ impl Night {
|
|||
change: Some(NightChange::Shapeshift {
|
||||
source: character_id.character_id,
|
||||
}),
|
||||
unless: None,
|
||||
}))
|
||||
}
|
||||
ActionPrompt::AlphaWolf {
|
||||
|
|
@ -840,7 +971,6 @@ impl Night {
|
|||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
},
|
||||
}),
|
||||
unless: Some(Unless::TargetBlocked(*marked)),
|
||||
})),
|
||||
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||
|
|
@ -856,10 +986,139 @@ impl Night {
|
|||
target: *marked,
|
||||
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 {
|
||||
marked: (None, None),
|
||||
..
|
||||
|
|
@ -923,6 +1182,15 @@ impl Night {
|
|||
| ActionPrompt::Guardian { character_id, .. }
|
||||
| ActionPrompt::Shapeshifter { 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::WolvesIntro { wolves: _ }
|
||||
| ActionPrompt::WolfPackKill { .. }
|
||||
|
|
@ -934,7 +1202,7 @@ impl Night {
|
|||
|
||||
pub fn current_character(&self) -> Option<&Character> {
|
||||
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 {
|
||||
|
|
@ -944,9 +1212,12 @@ impl Night {
|
|||
pub fn next(&mut self) -> Result<()> {
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_result: Some(_),
|
||||
} => {}
|
||||
current_prompt,
|
||||
current_result: Some(result),
|
||||
} => {
|
||||
self.used_actions
|
||||
.push((current_prompt.clone(), result.clone()));
|
||||
}
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_result: None,
|
||||
|
|
@ -954,7 +1225,6 @@ impl Night {
|
|||
NightState::Complete => return Err(GameError::NightOver),
|
||||
}
|
||||
if let Some(prompt) = self.action_queue.pop_front() {
|
||||
self.used_actions.push(prompt.clone());
|
||||
self.night_state = NightState::Active {
|
||||
current_prompt: prompt,
|
||||
current_result: None,
|
||||
|
|
@ -977,7 +1247,7 @@ pub enum ServerAction {
|
|||
}
|
||||
|
||||
mod filter {
|
||||
use crate::player::Character;
|
||||
use crate::character::Character;
|
||||
|
||||
pub fn no_filter(_: &Character) -> bool {
|
||||
true
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ use super::Result;
|
|||
|
||||
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)]
|
||||
pub struct GameSettings {
|
||||
|
|
@ -212,9 +212,7 @@ impl GameSettings {
|
|||
SetupRole::Apprentice { to: None } => (mentor_count > 0)
|
||||
.then_some(())
|
||||
.ok_or(GameError::NoApprenticeMentor),
|
||||
SetupRole::Apprentice {
|
||||
to: Some(role),
|
||||
} => role
|
||||
SetupRole::Apprentice { to: Some(role) } => role
|
||||
.is_mentor()
|
||||
.then_some(())
|
||||
.ok_or(GameError::NotAMentor(*role)),
|
||||
|
|
|
|||
|
|
@ -9,10 +9,11 @@ use uuid::Uuid;
|
|||
use werewolves_macros::{All, ChecksAs, Titles};
|
||||
|
||||
use crate::{
|
||||
character::Character,
|
||||
error::GameError,
|
||||
message::Identification,
|
||||
modifier::Modifier,
|
||||
player::{Character, PlayerId},
|
||||
player::PlayerId,
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
||||
|
|
@ -110,10 +111,33 @@ pub enum SetupRole {
|
|||
DireWolf,
|
||||
#[checks(Category::Wolves)]
|
||||
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 {
|
||||
pub const fn into_role(self) -> Role {
|
||||
pub fn into_role(self) -> Role {
|
||||
match self {
|
||||
SetupRoleTitle::Villager => Role::Villager,
|
||||
SetupRoleTitle::Scapegoat => Role::Scapegoat { redeemed: false },
|
||||
|
|
@ -141,6 +165,22 @@ impl SetupRoleTitle {
|
|||
SetupRoleTitle::AlphaWolf => Role::AlphaWolf { killed: None },
|
||||
SetupRoleTitle::DireWolf => Role::DireWolf,
|
||||
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::DireWolf => "DireWolf",
|
||||
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::DireWolf => Role::DireWolf,
|
||||
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::DireWolf => RoleTitle::DireWolf,
|
||||
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::DireWolf => SetupRole::DireWolf,
|
||||
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 crate::{
|
||||
character::{Character, CharacterId},
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{DateTime, GameOver, GameSettings},
|
||||
message::{CharacterIdentity, Identification},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
player::PlayerId,
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
||||
|
|
@ -43,7 +44,7 @@ impl Village {
|
|||
{
|
||||
let ww = wolves
|
||||
.clone()
|
||||
.filter(|w| matches!(w.role().title(), RoleTitle::Werewolf))
|
||||
.filter(|w| matches!(w.role_title(), RoleTitle::Werewolf))
|
||||
.collect::<Box<[_]>>();
|
||||
if !ww.is_empty() {
|
||||
return Some(ww[rand::random_range(0..ww.len())]);
|
||||
|
|
@ -63,12 +64,6 @@ impl Village {
|
|||
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(
|
||||
&mut self,
|
||||
character_id: CharacterId,
|
||||
|
|
@ -137,18 +132,22 @@ impl Village {
|
|||
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.role().wolf() && c.alive())
|
||||
.filter(|c| c.is_wolf() && c.alive())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn killing_wolf_id(&self) -> CharacterId {
|
||||
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()
|
||||
} else if let Some(non_ss_wolf) = wolves.iter().find(|w| {
|
||||
w.role().wolf() && !matches!(w.role(), Role::Shapeshifter { shifted_into: _ })
|
||||
}) {
|
||||
} else if let Some(non_ss_wolf) = wolves
|
||||
.iter()
|
||||
.find(|w| w.is_wolf() && !matches!(w.role_title(), RoleTitle::Shapeshifter))
|
||||
{
|
||||
non_ss_wolf.character_id()
|
||||
} else {
|
||||
wolves.into_iter().next().unwrap().character_id()
|
||||
|
|
@ -163,7 +162,7 @@ impl Village {
|
|||
.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)
|
||||
}
|
||||
|
||||
|
|
@ -197,23 +196,28 @@ impl Village {
|
|||
|
||||
pub fn executed_known_elder(&self) -> bool {
|
||||
self.characters.iter().any(|d| {
|
||||
matches!(
|
||||
d.role(),
|
||||
Role::Elder {
|
||||
woken_for_reveal: true,
|
||||
..
|
||||
}
|
||||
) && d
|
||||
.died_to()
|
||||
d.known_elder()
|
||||
&& d.died_to()
|
||||
.map(|d| matches!(d, DiedTo::Execution { .. }))
|
||||
.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]> {
|
||||
self.characters
|
||||
.iter()
|
||||
.filter(|c| c.role().title() == role)
|
||||
.filter(|c| c.role_title() == role)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
|
@ -222,16 +226,18 @@ impl Village {
|
|||
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
|
||||
.iter_mut()
|
||||
.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
|
||||
.iter()
|
||||
.find(|c| c.character_id() == character_id)
|
||||
.ok_or(GameError::InvalidTarget)
|
||||
}
|
||||
|
||||
pub fn character_by_player_id(&self, player_id: PlayerId) -> Option<&Character> {
|
||||
|
|
@ -269,8 +275,23 @@ impl RoleTitle {
|
|||
RoleTitle::Guardian => Role::Guardian {
|
||||
last_protected: None,
|
||||
},
|
||||
// fallback to 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;
|
||||
|
||||
use crate::{
|
||||
character::{Character, CharacterId},
|
||||
error::GameError,
|
||||
game::{Game, GameSettings, SetupRole, SetupSlot},
|
||||
message::{
|
||||
|
|
@ -9,14 +10,11 @@ use crate::{
|
|||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{Character, CharacterId, PlayerId},
|
||||
role::{Alignment, Role, RoleTitle},
|
||||
player::PlayerId,
|
||||
role::{Alignment, RoleTitle},
|
||||
};
|
||||
use colored::Colorize;
|
||||
use core::{
|
||||
num::NonZeroU8,
|
||||
ops::Range,
|
||||
};
|
||||
use core::{num::NonZeroU8, ops::Range};
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
use std::io::Write;
|
||||
|
|
@ -40,6 +38,7 @@ impl SettingsExt for GameSettings {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait ActionPromptTitleExt {
|
||||
fn wolf_pack_kill(&self);
|
||||
fn cover_of_darkness(&self);
|
||||
|
|
@ -56,6 +55,8 @@ pub trait ActionPromptTitleExt {
|
|||
fn shapeshifter(&self);
|
||||
fn alphawolf(&self);
|
||||
fn direwolf(&self);
|
||||
fn masons_wake(&self);
|
||||
fn masons_leader_recruit(&self);
|
||||
}
|
||||
|
||||
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||
|
|
@ -104,6 +105,12 @@ impl ActionPromptTitleExt for ActionPromptTitle {
|
|||
fn wolf_pack_kill(&self) {
|
||||
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 {
|
||||
|
|
@ -195,7 +202,9 @@ impl GameExt for Game {
|
|||
self.village()
|
||||
.characters()
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
@ -203,7 +212,9 @@ impl GameExt for Game {
|
|||
self.village()
|
||||
.characters()
|
||||
.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()
|
||||
}
|
||||
|
||||
|
|
@ -233,7 +244,8 @@ impl GameExt for Game {
|
|||
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||
let prompt = self.mark(mark);
|
||||
match prompt {
|
||||
ActionPrompt::ElderReveal { .. }
|
||||
ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::CoverOfDarkness
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
|
|
@ -243,6 +255,38 @@ impl GameExt for Game {
|
|||
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 {
|
||||
marked: Some(marked),
|
||||
..
|
||||
|
|
@ -279,8 +323,15 @@ impl GameExt for Game {
|
|||
marked: Some(marked),
|
||||
..
|
||||
} => assert_eq!(marked, mark, "marked character"),
|
||||
|
||||
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::Gravedigger { marked: None, .. }
|
||||
| ActionPrompt::Hunter { marked: None, .. }
|
||||
|
|
@ -341,9 +392,14 @@ impl GameExt for Game {
|
|||
}
|
||||
|
||||
fn execute(&mut self) -> ActionPrompt {
|
||||
assert_eq!(
|
||||
self.process(HostGameMessage::Day(HostDayMessage::Execute))
|
||||
.unwrap()
|
||||
.prompt()
|
||||
.prompt(),
|
||||
ActionPrompt::CoverOfDarkness
|
||||
);
|
||||
self.r#continue().r#continue();
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -563,7 +619,7 @@ fn wolfpack_kill_all_targets_valid() {
|
|||
.village()
|
||||
.characters()
|
||||
.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()
|
||||
.character_id();
|
||||
match game
|
||||
|
|
@ -580,14 +636,7 @@ fn wolfpack_kill_all_targets_valid() {
|
|||
resp => panic!("unexpected server message: {resp:#?}"),
|
||||
}
|
||||
|
||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||
game.r#continue().r#continue();
|
||||
|
||||
let living_villagers = match game
|
||||
.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||
.unwrap()
|
||||
.prompt()
|
||||
{
|
||||
let living_villagers = match game.execute() {
|
||||
ActionPrompt::WolfPackKill {
|
||||
living_villagers,
|
||||
marked: _,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ use core::num::NonZeroU8;
|
|||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
night::{ActionPrompt, ActionPromptTitle},
|
||||
},
|
||||
player::CharacterId,
|
||||
};
|
||||
|
||||
fn character_identity() -> CharacterIdentity {
|
||||
|
|
|
|||
|
|
@ -34,9 +34,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() {
|
|||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||
game.r#continue().r#continue();
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||
game.execute().title().wolf_pack_kill();
|
||||
let elder = game.character_by_player_id(elder_player_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);
|
||||
assert_eq!(elder.died_to().cloned(), None);
|
||||
|
||||
assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness);
|
||||
game.r#continue().r#continue();
|
||||
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark_and_check(elder.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
|
|
@ -60,9 +55,7 @@ fn elder_doesnt_die_first_try_night_doesnt_know() {
|
|||
.died_to()
|
||||
.cloned(),
|
||||
Some(DiedTo::Wolfpack {
|
||||
killing_wolf: game
|
||||
.character_by_player_id(wolf_player_id)
|
||||
.character_id(),
|
||||
killing_wolf: game.character_by_player_id(wolf_player_id).character_id(),
|
||||
night: NonZeroU8::new(2).unwrap(),
|
||||
})
|
||||
);
|
||||
|
|
@ -92,9 +85,7 @@ fn elder_doesnt_die_first_try_night_knows() {
|
|||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||
game.r#continue().r#continue();
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||
game.execute().title().wolf_pack_kill();
|
||||
let elder = game.character_by_player_id(elder_player_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);
|
||||
assert_eq!(elder.died_to().cloned(), None);
|
||||
|
||||
assert_eq!(game.execute(), ActionPrompt::CoverOfDarkness);
|
||||
game.r#continue().r#continue();
|
||||
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark_and_check(elder.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
|
|
@ -167,10 +155,7 @@ fn elder_executed_doesnt_know() {
|
|||
|
||||
game.mark_for_execution(elder.character_id());
|
||||
|
||||
game.execute().title().cover_of_darkness();
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.execute().title().wolf_pack_kill();
|
||||
|
||||
assert_eq!(
|
||||
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.execute().title().cover_of_darkness();
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark(villagers.next().unwrap());
|
||||
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.execute().title().cover_of_darkness();
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark(game.character_by_player_id(hunter_player_id).character_id());
|
||||
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 mason;
|
||||
mod scapegoat;
|
||||
mod shapeshifter;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ use core::num::NonZero;
|
|||
use crate::{
|
||||
diedto::DiedTo,
|
||||
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},
|
||||
role::{Alignment, RoleTitle},
|
||||
};
|
||||
|
|
@ -72,9 +72,7 @@ fn redeemed_scapegoat_role_changes() {
|
|||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(game.execute().title(), ActionPromptTitle::CoverOfDarkness);
|
||||
game.r#continue().r#continue();
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::WolfPackKill);
|
||||
game.execute().title().wolf_pack_kill();
|
||||
let seer = game
|
||||
.village()
|
||||
.characters()
|
||||
|
|
@ -103,10 +101,8 @@ fn redeemed_scapegoat_role_changes() {
|
|||
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
|
||||
.village()
|
||||
.characters()
|
||||
|
|
@ -153,3 +149,43 @@ fn redeemed_scapegoat_role_changes() {
|
|||
.unwrap();
|
||||
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 crate::{
|
||||
character::CharacterId,
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{ActionResultExt, GameExt, ServerToHostMessageExt, gen_players, init_log},
|
||||
message::{
|
||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult},
|
||||
},
|
||||
player::CharacterId,
|
||||
role::RoleTitle,
|
||||
};
|
||||
|
||||
|
|
@ -186,9 +186,8 @@ fn only_1_shapeshift_prompt_if_first_shifts() {
|
|||
let (_, marked, _) = game.mark_for_execution(target);
|
||||
let (marked, target_list): (&[CharacterId], &[CharacterId]) = (&marked, &[target]);
|
||||
assert_eq!(target_list, marked);
|
||||
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 target = game
|
||||
.village()
|
||||
.characters()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
#![allow(clippy::new_without_default)]
|
||||
|
||||
pub mod character;
|
||||
pub mod diedto;
|
||||
pub mod error;
|
||||
pub mod game;
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ use core::num::NonZeroU8;
|
|||
pub use ident::*;
|
||||
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)]
|
||||
pub enum ClientMessage {
|
||||
|
|
|
|||
|
|
@ -3,13 +3,14 @@ use core::num::NonZeroU8;
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
error::GameError,
|
||||
game::{GameOver, GameSettings},
|
||||
message::{
|
||||
CharacterIdentity,
|
||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||
},
|
||||
player::{CharacterId, PlayerId},
|
||||
player::PlayerId,
|
||||
};
|
||||
|
||||
use super::{CharacterState, PlayerState};
|
||||
|
|
|
|||
|
|
@ -2,11 +2,7 @@ use core::{fmt::Display, num::NonZeroU8};
|
|||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
player::{CharacterId, PlayerId},
|
||||
role::RoleTitle,
|
||||
};
|
||||
use crate::{character::CharacterId, diedto::DiedTo, player::PlayerId, role::RoleTitle};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Identification {
|
||||
|
|
|
|||
|
|
@ -1,10 +1,13 @@
|
|||
use core::num::NonZeroU8;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::{ChecksAs, Titles};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
diedto::DiedToTitle,
|
||||
error::GameError,
|
||||
message::CharacterIdentity,
|
||||
player::CharacterId,
|
||||
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
||||
};
|
||||
|
||||
|
|
@ -19,7 +22,11 @@ pub enum ActionType {
|
|||
Direwolf,
|
||||
OtherWolf,
|
||||
Block,
|
||||
Intel,
|
||||
Other,
|
||||
MasonRecruit,
|
||||
MasonsWake,
|
||||
Beholder,
|
||||
RoleChange,
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +57,7 @@ pub enum ActionPrompt {
|
|||
},
|
||||
#[checks(ActionType::RoleChange)]
|
||||
ElderReveal { character_id: CharacterIdentity },
|
||||
#[checks(ActionType::Other)]
|
||||
#[checks(ActionType::Intel)]
|
||||
Seer {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
|
|
@ -62,13 +69,13 @@ pub enum ActionPrompt {
|
|||
targets: Box<[CharacterIdentity]>,
|
||||
marked: Option<CharacterId>,
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
#[checks(ActionType::Intel)]
|
||||
Arcanist {
|
||||
character_id: CharacterIdentity,
|
||||
living_players: Box<[CharacterIdentity]>,
|
||||
marked: (Option<CharacterId>, Option<CharacterId>),
|
||||
},
|
||||
#[checks(ActionType::Other)]
|
||||
#[checks(ActionType::Intel)]
|
||||
Gravedigger {
|
||||
character_id: CharacterIdentity,
|
||||
dead_players: Box<[CharacterIdentity]>,
|
||||
|
|
@ -101,6 +108,61 @@ pub enum ActionPrompt {
|
|||
living_players: Box<[CharacterIdentity]>,
|
||||
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)]
|
||||
WolfPackKill {
|
||||
living_villagers: Box<[CharacterIdentity]>,
|
||||
|
|
@ -123,10 +185,41 @@ pub enum 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> {
|
||||
let mut prompt = self.clone();
|
||||
match &mut prompt {
|
||||
ActionPrompt::ElderReveal { .. }
|
||||
ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
|
|
@ -195,7 +288,47 @@ impl ActionPrompt {
|
|||
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, ..
|
||||
}
|
||||
| ActionPrompt::Seer {
|
||||
|
|
@ -274,17 +407,6 @@ impl PartialOrd for ActionPrompt {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize)]
|
||||
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),
|
||||
Shapeshift,
|
||||
Continue,
|
||||
|
|
@ -294,8 +416,12 @@ pub enum ActionResponse {
|
|||
pub enum ActionResult {
|
||||
RoleBlocked,
|
||||
Seer(Alignment),
|
||||
PowerSeer { powerful: bool },
|
||||
Adjudicator { killer: bool },
|
||||
Arcanist { same: bool },
|
||||
GraveDigger(Option<RoleTitle>),
|
||||
Mortician(DiedToTitle),
|
||||
Empath { scapegoat: bool },
|
||||
GoBackToSleep,
|
||||
Continue,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
use core::{fmt::Display, num::NonZeroU8};
|
||||
use core::fmt::Display;
|
||||
|
||||
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,
|
||||
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
||||
character::CharacterId,
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
||||
#[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)]
|
||||
pub struct Player {
|
||||
id: PlayerId,
|
||||
|
|
@ -67,6 +44,7 @@ impl Player {
|
|||
pub enum Protection {
|
||||
Guardian { source: CharacterId, guarding: bool },
|
||||
Protector { source: CharacterId },
|
||||
Vindicator { source: CharacterId },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -77,361 +55,7 @@ pub enum KillOutcome {
|
|||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RoleChange {
|
||||
role: Role,
|
||||
new_role: RoleTitle,
|
||||
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,
|
||||
},
|
||||
}))
|
||||
}
|
||||
pub role: Role,
|
||||
pub new_role: RoleTitle,
|
||||
pub changed_on_night: u8,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,9 +4,9 @@ use serde::{Deserialize, Serialize};
|
|||
use werewolves_macros::{ChecksAs, Titles};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
game::{DateTime, Village},
|
||||
message::CharacterIdentity,
|
||||
player::CharacterId,
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||
|
|
@ -28,6 +28,53 @@ pub enum Role {
|
|||
#[checks(Alignment::Village)]
|
||||
#[checks("powerful")]
|
||||
#[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,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks("killer")]
|
||||
|
|
@ -101,7 +148,12 @@ impl Role {
|
|||
|
||||
pub const fn wakes_night_zero(&self) -> bool {
|
||||
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::Werewolf
|
||||
|
|
@ -115,6 +167,14 @@ impl Role {
|
|||
| Role::Apprentice(_)
|
||||
| Role::Villager
|
||||
| Role::Scapegoat { .. }
|
||||
| Role::Mortician
|
||||
| Role::MasonLeader { .. }
|
||||
| Role::Empath { .. }
|
||||
| Role::Vindicator
|
||||
| Role::Diseased
|
||||
| Role::BlackKnight { .. }
|
||||
| Role::Weightlifter
|
||||
| Role::PyreMaster { .. }
|
||||
| Role::Protector { .. } => false,
|
||||
}
|
||||
}
|
||||
|
|
@ -132,9 +192,20 @@ impl Role {
|
|||
| Role::Werewolf
|
||||
| Role::Scapegoat { redeemed: false }
|
||||
| Role::Militia { targeted: Some(_) }
|
||||
| Role::Diseased
|
||||
| Role::BlackKnight { .. }
|
||||
| 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::DireWolf
|
||||
| Role::AlphaWolf { killed: None }
|
||||
|
|
@ -150,7 +221,7 @@ impl Role {
|
|||
Role::Apprentice(title) => village
|
||||
.characters()
|
||||
.iter()
|
||||
.any(|c| c.role().title() == *title),
|
||||
.any(|c| c.role_title() == *title),
|
||||
|
||||
Role::Elder {
|
||||
knows_on_night,
|
||||
|
|
|
|||
|
|
@ -10,13 +10,14 @@ use crate::{
|
|||
};
|
||||
use tokio::time::Instant;
|
||||
use werewolves_proto::{
|
||||
character::Character,
|
||||
error::GameError,
|
||||
game::{Game, GameOver, Village},
|
||||
message::{
|
||||
ClientMessage, Identification, ServerMessage,
|
||||
host::{HostGameMessage, HostMessage, ServerToHostMessage},
|
||||
},
|
||||
player::{Character, PlayerId},
|
||||
player::PlayerId,
|
||||
};
|
||||
|
||||
type Result<T> = core::result::Result<T, GameError>;
|
||||
|
|
@ -70,7 +71,7 @@ impl GameRunner {
|
|||
if let Err(err) = self.player_sender.send_if_present(
|
||||
char.player_id(),
|
||||
ServerMessage::GameStart {
|
||||
role: char.role().initial_shown_role(),
|
||||
role: char.initial_shown_role(),
|
||||
},
|
||||
) {
|
||||
log::warn!(
|
||||
|
|
@ -110,7 +111,7 @@ impl GameRunner {
|
|||
.send_if_present(
|
||||
player_id,
|
||||
ServerMessage::GameStart {
|
||||
role: char.role().initial_shown_role(),
|
||||
role: char.initial_shown_role(),
|
||||
},
|
||||
)
|
||||
.log_debug();
|
||||
|
|
@ -171,7 +172,7 @@ impl GameRunner {
|
|||
{
|
||||
sender
|
||||
.send(ServerMessage::GameStart {
|
||||
role: char.role().initial_shown_role(),
|
||||
role: char.initial_shown_role(),
|
||||
})
|
||||
.log_debug();
|
||||
} 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 serde::Serialize;
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
error::GameError,
|
||||
game::{GameOver, GameSettings},
|
||||
message::{
|
||||
|
|
@ -19,7 +20,7 @@ use werewolves_proto::{
|
|||
},
|
||||
night::{ActionPrompt, ActionResult},
|
||||
},
|
||||
player::{CharacterId, PlayerId},
|
||||
player::PlayerId,
|
||||
};
|
||||
use yew::{html::Scope, prelude::*};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
message::{CharacterIdentity, PublicIdentity},
|
||||
player::CharacterId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
use core::ops::Not;
|
||||
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
message::{
|
||||
CharacterIdentity, PublicIdentity,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
player::CharacterId,
|
||||
role::PreviousGuardianAction,
|
||||
};
|
||||
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 {
|
||||
ActionPrompt::CoverOfDarkness => {
|
||||
return html! {
|
||||
|
|
@ -72,13 +80,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
};
|
||||
}
|
||||
ActionPrompt::ElderReveal { character_id } => {
|
||||
let cont = continue_callback.map(|continue_callback| {
|
||||
html! {
|
||||
<Button on_click={continue_callback}>
|
||||
{"continue"}
|
||||
</Button>
|
||||
}
|
||||
});
|
||||
return html! {
|
||||
<div class="role-change">
|
||||
{identity_html(props, Some(character_id))}
|
||||
|
|
@ -91,13 +92,6 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
character_id,
|
||||
new_role,
|
||||
} => {
|
||||
let cont = continue_callback.map(|continue_callback| {
|
||||
html! {
|
||||
<Button on_click={continue_callback}>
|
||||
{"continue"}
|
||||
</Button>
|
||||
}
|
||||
});
|
||||
return html! {
|
||||
<div class="role-change">
|
||||
{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 {
|
||||
character_id,
|
||||
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 {
|
||||
character_id,
|
||||
living_players,
|
||||
|
|
|
|||
|
|
@ -40,36 +40,64 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
|||
.big_screen
|
||||
.not()
|
||||
.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 => {
|
||||
html! {
|
||||
<div class="result">
|
||||
{ident}
|
||||
<h2>{"you were role blocked"}</h2>
|
||||
{cont}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
ActionResult::Seer(alignment) => html! {
|
||||
<div class="result">
|
||||
{ident}
|
||||
<>
|
||||
<h2>{"the alignment was"}</h2>
|
||||
<p>{match alignment {
|
||||
Alignment::Village => "village",
|
||||
Alignment::Wolves => "wolfpack",
|
||||
}}</p>
|
||||
{cont}
|
||||
</div>
|
||||
</>
|
||||
},
|
||||
ActionResult::Arcanist { same } => {
|
||||
let outcome = if *same { "same" } else { "different" };
|
||||
html! {
|
||||
<div class="result">
|
||||
{ident}
|
||||
<>
|
||||
<h2>{"the alignments are:"}</h2>
|
||||
<p>{outcome}</p>
|
||||
{cont}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
ActionResult::GraveDigger(role_title) => {
|
||||
|
|
@ -77,12 +105,10 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
|||
.map(|r| r.to_string().to_case(Case::Title))
|
||||
.unwrap_or_else(|| String::from("an empty grave"));
|
||||
html! {
|
||||
<div class="result">
|
||||
{ident}
|
||||
<>
|
||||
<h2>{"you see:"}</h2>
|
||||
<p>{dig}</p>
|
||||
{cont}
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
}
|
||||
ActionResult::GoBackToSleep => {
|
||||
|
|
@ -94,17 +120,25 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
|||
)))
|
||||
}
|
||||
});
|
||||
html! {
|
||||
return html! {
|
||||
<CoverOfDarkness message={"go to sleep"} next={next}>
|
||||
{"continue"}
|
||||
</CoverOfDarkness>
|
||||
}
|
||||
};
|
||||
}
|
||||
ActionResult::Continue => {
|
||||
props.on_complete.emit(HostMessage::GetState);
|
||||
html! {
|
||||
return html! {
|
||||
<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::{
|
||||
message::{CharacterIdentity, PublicIdentity},
|
||||
player::CharacterId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
// use werewolves_proto::{
|
||||
// message::{CharacterIdentity, PublicIdentity},
|
||||
// player::CharacterId,
|
||||
// };
|
||||
// use yew::prelude::*;
|
||||
|
||||
use crate::components::{Button, Identity};
|
||||
// use crate::components::{Button, Identity};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TwoTargetProps {
|
||||
pub targets: Box<[CharacterIdentity]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
|
||||
}
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// pub struct TwoTargetProps {
|
||||
// pub targets: Box<[CharacterIdentity]>,
|
||||
// #[prop_or_default]
|
||||
// pub headline: &'static str,
|
||||
// #[prop_or_default]
|
||||
// pub target_selection: Option<Callback<(CharacterId, CharacterId)>>,
|
||||
// }
|
||||
|
||||
#[derive(Clone)]
|
||||
enum TwoTargetSelection {
|
||||
None,
|
||||
One(CharacterId),
|
||||
Two(CharacterId, CharacterId),
|
||||
}
|
||||
// #[derive(Clone)]
|
||||
// enum TwoTargetSelection {
|
||||
// None,
|
||||
// One(CharacterId),
|
||||
// Two(CharacterId, CharacterId),
|
||||
// }
|
||||
|
||||
impl TwoTargetSelection {
|
||||
fn is_selected(&self, id: &CharacterId) -> bool {
|
||||
match self {
|
||||
TwoTargetSelection::None => false,
|
||||
TwoTargetSelection::One(character_id) => id == character_id,
|
||||
TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2,
|
||||
}
|
||||
}
|
||||
}
|
||||
// impl TwoTargetSelection {
|
||||
// fn is_selected(&self, id: &CharacterId) -> bool {
|
||||
// match self {
|
||||
// TwoTargetSelection::None => false,
|
||||
// TwoTargetSelection::One(character_id) => id == character_id,
|
||||
// TwoTargetSelection::Two(cid1, cid2) => id == cid1 || id == cid2,
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
pub struct TwoTarget(TwoTargetSelection);
|
||||
// pub struct TwoTarget(TwoTargetSelection);
|
||||
|
||||
impl Component for TwoTarget {
|
||||
type Message = CharacterId;
|
||||
// impl Component for TwoTarget {
|
||||
// type Message = CharacterId;
|
||||
|
||||
type Properties = TwoTargetProps;
|
||||
// type Properties = TwoTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self(TwoTargetSelection::None)
|
||||
}
|
||||
// fn create(_: &Context<Self>) -> Self {
|
||||
// Self(TwoTargetSelection::None)
|
||||
// }
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let TwoTargetProps {
|
||||
targets,
|
||||
headline,
|
||||
target_selection,
|
||||
} = ctx.props();
|
||||
let mut targets = targets.clone();
|
||||
targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||
// fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
// let TwoTargetProps {
|
||||
// targets,
|
||||
// headline,
|
||||
// target_selection,
|
||||
// } = ctx.props();
|
||||
// let mut targets = targets.clone();
|
||||
// targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets
|
||||
.iter()
|
||||
.map(|t| {
|
||||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.0.is_selected(&t.character_id)}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = headline
|
||||
.trim()
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| html!(<h2>{headline}</h2>));
|
||||
// let target_selection = target_selection.clone();
|
||||
// let scope = ctx.link().clone();
|
||||
// let card_select = Callback::from(move |target| {
|
||||
// scope.send_message(target);
|
||||
// });
|
||||
// let targets = targets
|
||||
// .iter()
|
||||
// .map(|t| {
|
||||
// html! {
|
||||
// <TargetCard
|
||||
// target={t.clone()}
|
||||
// selected={self.0.is_selected(&t.character_id)}
|
||||
// on_select={card_select.clone()}
|
||||
// />
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// let headline = headline
|
||||
// .trim()
|
||||
// .is_empty()
|
||||
// .not()
|
||||
// .then(|| html!(<h2>{headline}</h2>));
|
||||
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let selected = match &self.0 {
|
||||
TwoTargetSelection::None | TwoTargetSelection::One(_) => None,
|
||||
TwoTargetSelection::Two(t1, t2) => Some((*t1, *t2)),
|
||||
};
|
||||
let target_selection = target_selection.clone();
|
||||
let disabled = selected.is_none();
|
||||
let on_click =
|
||||
selected.map(|(t1, t2)| move |_| target_selection.emit((t1, t2)));
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<button
|
||||
disabled={disabled}
|
||||
onclick={on_click}
|
||||
>
|
||||
{"submit"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
// let submit = target_selection.as_ref().map(|target_selection| {
|
||||
// let selected = match &self.0 {
|
||||
// TwoTargetSelection::None | TwoTargetSelection::One(_) => None,
|
||||
// TwoTargetSelection::Two(t1, t2) => Some((*t1, *t2)),
|
||||
// };
|
||||
// let target_selection = target_selection.clone();
|
||||
// let disabled = selected.is_none();
|
||||
// let on_click =
|
||||
// selected.map(|(t1, t2)| move |_| target_selection.emit((t1, t2)));
|
||||
// html! {
|
||||
// <div class="button-container sp-ace">
|
||||
// <button
|
||||
// disabled={disabled}
|
||||
// onclick={on_click}
|
||||
// >
|
||||
// {"submit"}
|
||||
// </button>
|
||||
// </div>
|
||||
// }
|
||||
// });
|
||||
|
||||
html! {
|
||||
<div class="column-list">
|
||||
{headline}
|
||||
<div class="row-list">
|
||||
{targets}
|
||||
</div>
|
||||
{submit}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
// html! {
|
||||
// <div class="column-list">
|
||||
// {headline}
|
||||
// <div class="row-list">
|
||||
// {targets}
|
||||
// </div>
|
||||
// {submit}
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match &self.0 {
|
||||
TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
|
||||
TwoTargetSelection::One(character_id) => {
|
||||
if character_id == &msg {
|
||||
self.0 = TwoTargetSelection::None
|
||||
} else {
|
||||
self.0 = TwoTargetSelection::Two(*character_id, msg)
|
||||
}
|
||||
}
|
||||
TwoTargetSelection::Two(t1, t2) => {
|
||||
if &msg == t1 {
|
||||
self.0 = TwoTargetSelection::One(*t2);
|
||||
} else if &msg == t2 {
|
||||
self.0 = TwoTargetSelection::One(*t1);
|
||||
} else {
|
||||
self.0 = TwoTargetSelection::Two(*t1, msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
// fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
// match &self.0 {
|
||||
// TwoTargetSelection::None => self.0 = TwoTargetSelection::One(msg),
|
||||
// TwoTargetSelection::One(character_id) => {
|
||||
// if character_id == &msg {
|
||||
// self.0 = TwoTargetSelection::None
|
||||
// } else {
|
||||
// self.0 = TwoTargetSelection::Two(*character_id, msg)
|
||||
// }
|
||||
// }
|
||||
// TwoTargetSelection::Two(t1, t2) => {
|
||||
// if &msg == t1 {
|
||||
// self.0 = TwoTargetSelection::One(*t2);
|
||||
// } else if &msg == t2 {
|
||||
// self.0 = TwoTargetSelection::One(*t1);
|
||||
// } else {
|
||||
// self.0 = TwoTargetSelection::Two(*t1, msg);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
// true
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct OptionalSingleTargetProps {
|
||||
pub targets: Box<[CharacterIdentity]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub target_selection: Option<Callback<Option<CharacterId>>>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// pub struct OptionalSingleTargetProps {
|
||||
// pub targets: Box<[CharacterIdentity]>,
|
||||
// #[prop_or_default]
|
||||
// pub headline: &'static str,
|
||||
// #[prop_or_default]
|
||||
// pub target_selection: Option<Callback<Option<CharacterId>>>,
|
||||
// #[prop_or_default]
|
||||
// pub children: Html,
|
||||
// }
|
||||
|
||||
pub struct OptionalSingleTarget(Option<CharacterId>);
|
||||
// pub struct OptionalSingleTarget(Option<CharacterId>);
|
||||
|
||||
impl Component for OptionalSingleTarget {
|
||||
type Message = CharacterId;
|
||||
// impl Component for OptionalSingleTarget {
|
||||
// type Message = CharacterId;
|
||||
|
||||
type Properties = OptionalSingleTargetProps;
|
||||
// type Properties = OptionalSingleTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self(None)
|
||||
}
|
||||
// fn create(_: &Context<Self>) -> Self {
|
||||
// Self(None)
|
||||
// }
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let OptionalSingleTargetProps {
|
||||
targets,
|
||||
headline,
|
||||
target_selection,
|
||||
children,
|
||||
} = ctx.props();
|
||||
let mut targets = targets.clone();
|
||||
targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||
// fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
// let OptionalSingleTargetProps {
|
||||
// targets,
|
||||
// headline,
|
||||
// target_selection,
|
||||
// children,
|
||||
// } = ctx.props();
|
||||
// let mut targets = targets.clone();
|
||||
// targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets
|
||||
.iter()
|
||||
.map(|t| {
|
||||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = headline
|
||||
.trim()
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| html!(<h2>{headline}</h2>));
|
||||
// let target_selection = target_selection.clone();
|
||||
// let scope = ctx.link().clone();
|
||||
// let card_select = Callback::from(move |target| {
|
||||
// scope.send_message(target);
|
||||
// });
|
||||
// let targets = targets
|
||||
// .iter()
|
||||
// .map(|t| {
|
||||
// html! {
|
||||
// <TargetCard
|
||||
// target={t.clone()}
|
||||
// selected={self.0.as_ref().map(|c| c == &t.character_id).unwrap_or_default()}
|
||||
// on_select={card_select.clone()}
|
||||
// />
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// let headline = headline
|
||||
// .trim()
|
||||
// .is_empty()
|
||||
// .not()
|
||||
// .then(|| html!(<h2>{headline}</h2>));
|
||||
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let target_selection = target_selection.clone();
|
||||
let sel = self.0;
|
||||
let on_click = move |_| target_selection.emit(sel);
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<button
|
||||
onclick={on_click}
|
||||
>
|
||||
{"submit"}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
// let submit = target_selection.as_ref().map(|target_selection| {
|
||||
// let target_selection = target_selection.clone();
|
||||
// let sel = self.0;
|
||||
// let on_click = move |_| target_selection.emit(sel);
|
||||
// html! {
|
||||
// <div class="button-container sp-ace">
|
||||
// <button
|
||||
// onclick={on_click}
|
||||
// >
|
||||
// {"submit"}
|
||||
// </button>
|
||||
// </div>
|
||||
// }
|
||||
// });
|
||||
|
||||
html! {
|
||||
<div class="column-list">
|
||||
{headline}
|
||||
{children.clone()}
|
||||
<div class="row-list">
|
||||
{targets}
|
||||
</div>
|
||||
{submit}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
// html! {
|
||||
// <div class="column-list">
|
||||
// {headline}
|
||||
// {children.clone()}
|
||||
// <div class="row-list">
|
||||
// {targets}
|
||||
// </div>
|
||||
// {submit}
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match &self.0 {
|
||||
Some(t) => {
|
||||
if t == &msg {
|
||||
self.0 = None
|
||||
} else {
|
||||
self.0 = Some(msg);
|
||||
}
|
||||
}
|
||||
None => self.0 = Some(msg),
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
// fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
// match &self.0 {
|
||||
// Some(t) => {
|
||||
// if t == &msg {
|
||||
// self.0 = None
|
||||
// } else {
|
||||
// self.0 = Some(msg);
|
||||
// }
|
||||
// }
|
||||
// None => self.0 = Some(msg),
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct SingleTargetProps {
|
||||
pub targets: Box<[CharacterIdentity]>,
|
||||
#[prop_or_default]
|
||||
pub headline: &'static str,
|
||||
#[prop_or_default]
|
||||
pub target_selection: Option<Callback<CharacterId>>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
}
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// pub struct SingleTargetProps {
|
||||
// pub targets: Box<[CharacterIdentity]>,
|
||||
// #[prop_or_default]
|
||||
// pub headline: &'static str,
|
||||
// #[prop_or_default]
|
||||
// pub target_selection: Option<Callback<CharacterId>>,
|
||||
// #[prop_or_default]
|
||||
// pub children: Html,
|
||||
// }
|
||||
|
||||
pub struct SingleTarget {
|
||||
selected: Option<CharacterId>,
|
||||
}
|
||||
// pub struct SingleTarget {
|
||||
// selected: Option<CharacterId>,
|
||||
// }
|
||||
|
||||
impl Component for SingleTarget {
|
||||
type Message = CharacterId;
|
||||
// impl Component for SingleTarget {
|
||||
// type Message = CharacterId;
|
||||
|
||||
type Properties = SingleTargetProps;
|
||||
// type Properties = SingleTargetProps;
|
||||
|
||||
fn create(_: &Context<Self>) -> Self {
|
||||
Self { selected: None }
|
||||
}
|
||||
// fn create(_: &Context<Self>) -> Self {
|
||||
// Self { selected: None }
|
||||
// }
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let SingleTargetProps {
|
||||
headline,
|
||||
targets,
|
||||
target_selection,
|
||||
children,
|
||||
} = ctx.props();
|
||||
let mut targets = targets.clone();
|
||||
targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||
let target_selection = target_selection.clone();
|
||||
let scope = ctx.link().clone();
|
||||
let card_select = Callback::from(move |target| {
|
||||
scope.send_message(target);
|
||||
});
|
||||
let targets = targets
|
||||
.iter()
|
||||
.map(|t| {
|
||||
html! {
|
||||
<TargetCard
|
||||
target={t.clone()}
|
||||
selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
||||
on_select={card_select.clone()}
|
||||
/>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let headline = headline
|
||||
.trim()
|
||||
.is_empty()
|
||||
.not()
|
||||
.then(|| html!(<h2>{headline}</h2>));
|
||||
// fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
// let SingleTargetProps {
|
||||
// headline,
|
||||
// targets,
|
||||
// target_selection,
|
||||
// children,
|
||||
// } = ctx.props();
|
||||
// let mut targets = targets.clone();
|
||||
// targets.sort_by(|l, r| l.number.cmp(&r.number));
|
||||
// let target_selection = target_selection.clone();
|
||||
// let scope = ctx.link().clone();
|
||||
// let card_select = Callback::from(move |target| {
|
||||
// scope.send_message(target);
|
||||
// });
|
||||
// let targets = targets
|
||||
// .iter()
|
||||
// .map(|t| {
|
||||
// html! {
|
||||
// <TargetCard
|
||||
// target={t.clone()}
|
||||
// selected={self.selected.as_ref().map(|sel| sel == &t.character_id).unwrap_or_default()}
|
||||
// on_select={card_select.clone()}
|
||||
// />
|
||||
// }
|
||||
// })
|
||||
// .collect::<Html>();
|
||||
// let headline = headline
|
||||
// .trim()
|
||||
// .is_empty()
|
||||
// .not()
|
||||
// .then(|| html!(<h2>{headline}</h2>));
|
||||
|
||||
let submit = target_selection.as_ref().map(|target_selection| {
|
||||
let disabled = self.selected.is_none().then_some("pick a target");
|
||||
let target_selection = target_selection.clone();
|
||||
let on_click = self
|
||||
.selected
|
||||
.map(|t| Callback::from(move |_| target_selection.emit(t)))
|
||||
.unwrap_or_default();
|
||||
html! {
|
||||
<div class="button-container sp-ace">
|
||||
<Button disabled_reason={disabled} on_click={on_click}>
|
||||
{"submit"}
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
// let submit = target_selection.as_ref().map(|target_selection| {
|
||||
// let disabled = self.selected.is_none().then_some("pick a target");
|
||||
// let target_selection = target_selection.clone();
|
||||
// let on_click = self
|
||||
// .selected
|
||||
// .map(|t| Callback::from(move |_| target_selection.emit(t)))
|
||||
// .unwrap_or_default();
|
||||
// html! {
|
||||
// <div class="button-container sp-ace">
|
||||
// <Button disabled_reason={disabled} on_click={on_click}>
|
||||
// {"submit"}
|
||||
// </Button>
|
||||
// </div>
|
||||
// }
|
||||
// });
|
||||
|
||||
html! {
|
||||
<div class="character-picker">
|
||||
{headline}
|
||||
{children.clone()}
|
||||
<div class="row-list">
|
||||
{targets}
|
||||
</div>
|
||||
{submit}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
// html! {
|
||||
// <div class="character-picker">
|
||||
// {headline}
|
||||
// {children.clone()}
|
||||
// <div class="row-list">
|
||||
// {targets}
|
||||
// </div>
|
||||
// {submit}
|
||||
// </div>
|
||||
// }
|
||||
// }
|
||||
|
||||
fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
match &self.selected {
|
||||
Some(current) => {
|
||||
if current == &msg {
|
||||
self.selected = None;
|
||||
} else {
|
||||
self.selected = Some(msg);
|
||||
}
|
||||
}
|
||||
None => self.selected = Some(msg),
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
// fn update(&mut self, _: &Context<Self>, msg: Self::Message) -> bool {
|
||||
// match &self.selected {
|
||||
// Some(current) => {
|
||||
// if current == &msg {
|
||||
// self.selected = None;
|
||||
// } else {
|
||||
// self.selected = Some(msg);
|
||||
// }
|
||||
// }
|
||||
// None => self.selected = Some(msg),
|
||||
// }
|
||||
// true
|
||||
// }
|
||||
// }
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct TargetCardProps {
|
||||
pub target: CharacterIdentity,
|
||||
pub selected: bool,
|
||||
pub on_select: Callback<CharacterId>,
|
||||
}
|
||||
// #[derive(Debug, Clone, PartialEq, Properties)]
|
||||
// pub struct TargetCardProps {
|
||||
// pub target: CharacterIdentity,
|
||||
// pub selected: bool,
|
||||
// pub on_select: Callback<CharacterId>,
|
||||
// }
|
||||
|
||||
#[function_component]
|
||||
fn TargetCard(props: &TargetCardProps) -> Html {
|
||||
let character_id = props.target.character_id;
|
||||
let on_select = props.on_select.clone();
|
||||
let on_click = Callback::from(move |_| on_select.emit(character_id));
|
||||
// #[function_component]
|
||||
// fn TargetCard(props: &TargetCardProps) -> Html {
|
||||
// let character_id = props.target.character_id;
|
||||
// let on_select = props.on_select.clone();
|
||||
// let on_click = Callback::from(move |_| on_select.emit(character_id));
|
||||
|
||||
let marked = props.selected.then_some("marked");
|
||||
let ident: PublicIdentity = props.target.clone().into();
|
||||
html! {
|
||||
<Button on_click={on_click} classes={classes!(marked, "character")}>
|
||||
<Identity ident={ident}/>
|
||||
</Button>
|
||||
}
|
||||
}
|
||||
// let marked = props.selected.then_some("marked");
|
||||
// let ident: PublicIdentity = props.target.clone().into();
|
||||
// html! {
|
||||
// <Button on_click={on_click} classes={classes!(marked, "character")}>
|
||||
// <Identity ident={ident}/>
|
||||
// </Button>
|
||||
// }
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -1,8 +1,8 @@
|
|||
use core::{num::NonZeroU8, ops::Not};
|
||||
|
||||
use werewolves_proto::{
|
||||
character::CharacterId,
|
||||
message::{CharacterState, PublicIdentity},
|
||||
player::CharacterId,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use core::ops::Not;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use rand::Rng;
|
||||
use werewolves_proto::{
|
||||
game::{Category, GameSettings, SetupRole, SetupRoleTitle},
|
||||
|
|
@ -133,7 +134,7 @@ pub fn SetupCategory(
|
|||
<div class={classes!("slot")}>
|
||||
<div class={classes!("role", wakes, r.category().class())}>
|
||||
{count}
|
||||
{r.to_string()}
|
||||
{r.to_string().to_case(Case::Title)}
|
||||
</div>
|
||||
<div class="attributes">
|
||||
<div class="alignment">
|
||||
|
|
@ -153,7 +154,9 @@ pub fn SetupCategory(
|
|||
html! {
|
||||
<div class="category">
|
||||
{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")}>
|
||||
{all_roles}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -378,6 +378,59 @@ fn setup_options_for_slot(
|
|||
slot_field_open: UseStateHandle<bool>,
|
||||
) -> Html {
|
||||
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 } => {
|
||||
let next = {
|
||||
let next_redeemed = match redeemed {
|
||||
|
|
|
|||
Loading…
Reference in New Issue