added end-game story screen with icons
bugfixes and tests to guardians and hunters ron serialization for game story saves
|
|
@ -79,7 +79,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
|
checksum = "8a18ed336352031311f4e0b4dd2ff392d4fbb370777c9d18d7fc9d7359f73871"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum-core",
|
"axum-core",
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"form_urlencoded",
|
"form_urlencoded",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -165,6 +165,12 @@ dependencies = [
|
||||||
"windows-link",
|
"windows-link",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "base64"
|
||||||
|
version = "0.21.7"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "base64"
|
name = "base64"
|
||||||
version = "0.22.1"
|
version = "0.22.1"
|
||||||
|
|
@ -185,6 +191,9 @@ name = "bitflags"
|
||||||
version = "2.9.4"
|
version = "2.9.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block-buffer"
|
name = "block-buffer"
|
||||||
|
|
@ -1054,7 +1063,7 @@ version = "0.4.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
checksum = "b3314d5adb5d94bcdf56771f2e50dbbc80bb4bdf88967526706205ac9eff24eb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64 0.22.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
"headers-core",
|
"headers-core",
|
||||||
"http 1.3.1",
|
"http 1.3.1",
|
||||||
|
|
@ -1783,6 +1792,18 @@ version = "0.8.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ron"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.21.7",
|
||||||
|
"bitflags",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "route-recognizer"
|
name = "route-recognizer"
|
||||||
version = "0.3.1"
|
version = "0.3.1"
|
||||||
|
|
@ -2459,6 +2480,7 @@ dependencies = [
|
||||||
"mime-sniffer",
|
"mime-sniffer",
|
||||||
"pretty_env_logger",
|
"pretty_env_logger",
|
||||||
"rand",
|
"rand",
|
||||||
|
"ron",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
|
|
|
||||||
|
|
@ -6,11 +6,14 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{DateTime, Village, night::NightChange},
|
game::{GameTime, Village},
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
modifier::Modifier,
|
modifier::Modifier,
|
||||||
player::{PlayerId, RoleChange},
|
player::{PlayerId, RoleChange},
|
||||||
role::{Alignment, MAPLE_WOLF_ABSTAIN_LIMIT, PreviousGuardianAction, Role, RoleTitle},
|
role::{
|
||||||
|
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
|
||||||
|
PreviousGuardianAction, Role, RoleTitle,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
@ -33,7 +36,7 @@ impl Display for CharacterId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Character {
|
pub struct Character {
|
||||||
player_id: PlayerId,
|
player_id: PlayerId,
|
||||||
identity: CharacterIdentity,
|
identity: CharacterIdentity,
|
||||||
|
|
@ -103,7 +106,7 @@ impl Character {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
match (&mut self.role, died_to.date_time()) {
|
match (&mut self.role, died_to.date_time()) {
|
||||||
(Role::BlackKnight { attacked }, DateTime::Night { .. }) => {
|
(Role::BlackKnight { attacked }, GameTime::Night { .. }) => {
|
||||||
attacked.replace(died_to);
|
attacked.replace(died_to);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -119,7 +122,7 @@ impl Character {
|
||||||
lost_protection_night,
|
lost_protection_night,
|
||||||
..
|
..
|
||||||
},
|
},
|
||||||
DateTime::Night { number: night },
|
GameTime::Night { number: night },
|
||||||
) => {
|
) => {
|
||||||
*lost_protection_night = lost_protection_night
|
*lost_protection_night = lost_protection_night
|
||||||
.is_none()
|
.is_none()
|
||||||
|
|
@ -184,15 +187,15 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn role_change(&mut self, new_role: RoleTitle, at: DateTime) -> Result<()> {
|
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> {
|
||||||
let mut role = new_role.title_to_role_excl_apprentice();
|
let mut role = new_role.title_to_role_excl_apprentice();
|
||||||
core::mem::swap(&mut role, &mut self.role);
|
core::mem::swap(&mut role, &mut self.role);
|
||||||
self.role_changes.push(RoleChange {
|
self.role_changes.push(RoleChange {
|
||||||
role,
|
role,
|
||||||
new_role,
|
new_role,
|
||||||
changed_on_night: match at {
|
changed_on_night: match at {
|
||||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
DateTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -276,9 +279,9 @@ impl Character {
|
||||||
if !self.alive() || !self.role.wakes(village) {
|
if !self.alive() || !self.role.wakes(village) {
|
||||||
return Ok(Box::new([]));
|
return Ok(Box::new([]));
|
||||||
}
|
}
|
||||||
let night = match village.date_time() {
|
let night = match village.time() {
|
||||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
DateTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
Ok(Box::new([match &self.role {
|
Ok(Box::new([match &self.role {
|
||||||
Role::Empath { cursed: true }
|
Role::Empath { cursed: true }
|
||||||
|
|
@ -341,9 +344,9 @@ impl Character {
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
},
|
||||||
Role::Apprentice(role) => {
|
Role::Apprentice(role) => {
|
||||||
let current_night = match village.date_time() {
|
let current_night = match village.time() {
|
||||||
DateTime::Day { number: _ } => return Ok(Box::new([])),
|
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
||||||
DateTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
return Ok(village
|
return Ok(village
|
||||||
.characters()
|
.characters()
|
||||||
|
|
@ -351,8 +354,8 @@ impl Character {
|
||||||
.filter(|c| c.role_title() == *role)
|
.filter(|c| c.role_title() == *role)
|
||||||
.filter_map(|char| char.died_to)
|
.filter_map(|char| char.died_to)
|
||||||
.any(|died_to| match died_to.date_time() {
|
.any(|died_to| match died_to.date_time() {
|
||||||
DateTime::Day { number } => number.get() + 1 >= current_night,
|
GameTime::Day { number } => number.get() + 1 >= current_night,
|
||||||
DateTime::Night { number } => number + 1 >= current_night,
|
GameTime::Night { number } => number + 1 >= current_night,
|
||||||
})
|
})
|
||||||
.then(|| ActionPrompt::RoleChange {
|
.then(|| ActionPrompt::RoleChange {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
|
|
@ -366,9 +369,9 @@ impl Character {
|
||||||
woken_for_reveal: false,
|
woken_for_reveal: false,
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let current_night = match village.date_time() {
|
let current_night = match village.time() {
|
||||||
DateTime::Day { number: _ } => return Ok(Box::new([])),
|
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
||||||
DateTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
return Ok((current_night >= knows_on_night.get())
|
return Ok((current_night >= knows_on_night.get())
|
||||||
.then_some({
|
.then_some({
|
||||||
|
|
@ -486,14 +489,14 @@ impl Character {
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
},
|
||||||
Role::Vindicator => {
|
Role::Vindicator => {
|
||||||
let last_day = match village.date_time() {
|
let last_day = match village.time() {
|
||||||
DateTime::Day { .. } => {
|
GameTime::Day { .. } => {
|
||||||
log::error!(
|
log::error!(
|
||||||
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
||||||
);
|
);
|
||||||
return Ok(Box::new([]));
|
return Ok(Box::new([]));
|
||||||
}
|
}
|
||||||
DateTime::Night { number } => {
|
GameTime::Night { number } => {
|
||||||
if number == 0 {
|
if number == 0 {
|
||||||
return Ok(Box::new([]));
|
return Ok(Box::new([]));
|
||||||
}
|
}
|
||||||
|
|
@ -530,16 +533,20 @@ impl Character {
|
||||||
&self.role
|
&self.role
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn killer(&self) -> bool {
|
pub const fn killing_wolf_order(&self) -> Option<KillingWolfOrder> {
|
||||||
|
self.role.killing_wolf_order()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn killer(&self) -> Killer {
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
return true;
|
return Killer::Killer;
|
||||||
}
|
}
|
||||||
self.role.killer()
|
self.role.killer()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn powerful(&self) -> bool {
|
pub const fn powerful(&self) -> Powerful {
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
return true;
|
return Powerful::Powerful;
|
||||||
}
|
}
|
||||||
self.role.powerful()
|
self.role.powerful()
|
||||||
}
|
}
|
||||||
|
|
@ -689,6 +696,28 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn guardian<'a>(&'a self) -> Result<Guardian<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &self.role {
|
||||||
|
Role::Guardian { last_protected } => Ok(Guardian(last_protected)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Guardian,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn guardian_mut<'a>(&'a mut self) -> Result<GuardianMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Guardian { last_protected } => Ok(GuardianMut(last_protected)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Guardian,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn initial_shown_role(&self) -> RoleTitle {
|
pub const fn initial_shown_role(&self) -> RoleTitle {
|
||||||
self.role.initial_shown_role()
|
self.role.initial_shown_role()
|
||||||
}
|
}
|
||||||
|
|
@ -728,6 +757,7 @@ decl_ref_and_mut!(
|
||||||
Scapegoat, ScapegoatMut: bool;
|
Scapegoat, ScapegoatMut: bool;
|
||||||
Empath, EmpathMut: bool;
|
Empath, EmpathMut: bool;
|
||||||
BlackKnight, BlackKnightMut: Option<DiedTo>;
|
BlackKnight, BlackKnightMut: Option<DiedTo>;
|
||||||
|
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
||||||
);
|
);
|
||||||
|
|
||||||
pub struct BlackKnightKill<'a> {
|
pub struct BlackKnightKill<'a> {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use core::{fmt::Debug, num::NonZeroU8};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::Titles;
|
use werewolves_macros::Titles;
|
||||||
|
|
||||||
use crate::{character::CharacterId, game::DateTime};
|
use crate::{character::CharacterId, game::GameTime};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Titles)]
|
||||||
pub enum DiedTo {
|
pub enum DiedTo {
|
||||||
|
|
@ -113,9 +113,9 @@ impl DiedTo {
|
||||||
| DiedTo::LoneWolf { killer, .. } => Some(*killer),
|
| DiedTo::LoneWolf { killer, .. } => Some(*killer),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn date_time(&self) -> DateTime {
|
pub const fn date_time(&self) -> GameTime {
|
||||||
match self {
|
match self {
|
||||||
DiedTo::Execution { day } => DateTime::Day { number: *day },
|
DiedTo::Execution { day } => GameTime::Day { number: *day },
|
||||||
DiedTo::GuardianProtecting {
|
DiedTo::GuardianProtecting {
|
||||||
source: _,
|
source: _,
|
||||||
protecting: _,
|
protecting: _,
|
||||||
|
|
@ -138,11 +138,11 @@ impl DiedTo {
|
||||||
| DiedTo::Shapeshift { into: _, night }
|
| DiedTo::Shapeshift { into: _, night }
|
||||||
| DiedTo::PyreMasterLynchMob { night, .. }
|
| DiedTo::PyreMasterLynchMob { night, .. }
|
||||||
| DiedTo::PyreMaster { night, .. }
|
| DiedTo::PyreMaster { night, .. }
|
||||||
| DiedTo::Hunter { killer: _, night } => DateTime::Night {
|
| DiedTo::Hunter { killer: _, night } => GameTime::Night {
|
||||||
number: night.get(),
|
number: night.get(),
|
||||||
},
|
},
|
||||||
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => {
|
DiedTo::LoneWolf { night, .. } | DiedTo::MasonLeaderRecruitFail { night, .. } => {
|
||||||
DateTime::Night { number: *night }
|
GameTime::Night { number: *night }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::{message::PublicIdentity, player::PlayerId, role::RoleTitle};
|
use crate::{game::GameTime, message::PublicIdentity, player::PlayerId, role::RoleTitle};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
|
||||||
pub enum GameError {
|
pub enum GameError {
|
||||||
|
|
@ -75,4 +75,10 @@ pub enum GameError {
|
||||||
AssignedPlayerMissing(PlayerId),
|
AssignedPlayerMissing(PlayerId),
|
||||||
#[error(" {0} assigned to {1} roles")]
|
#[error(" {0} assigned to {1} roles")]
|
||||||
AssignedMultipleTimes(PublicIdentity, usize),
|
AssignedMultipleTimes(PublicIdentity, usize),
|
||||||
|
#[error("change set for {0} is already set")]
|
||||||
|
ChangesAlreadySet(GameTime),
|
||||||
|
#[error("missing {0} in game story")]
|
||||||
|
MissingTime(GameTime),
|
||||||
|
#[error("no previous during day")]
|
||||||
|
NoPreviousDuringDay,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Village, night::NightChange},
|
game::{Village, night::changes::ChangesLookup},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -157,93 +157,3 @@ pub fn resolve_kill(
|
||||||
| Protection::Protector { .. } => Ok(None),
|
| Protection::Protector { .. } => Ok(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
|
||||||
|
|
||||||
impl<'a> ChangesLookup<'a> {
|
|
||||||
pub fn new(changes: &'a [NightChange]) -> Self {
|
|
||||||
Self(changes, Vec::new())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
|
||||||
self.1
|
|
||||||
.contains(&idx)
|
|
||||||
.not()
|
|
||||||
.then(|| match c {
|
|
||||||
NightChange::Kill { target: t, died_to } => (*t == target).then_some(died_to),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn protected_take(&mut self, target: CharacterId) -> Option<Protection> {
|
|
||||||
if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| {
|
|
||||||
self.1
|
|
||||||
.contains(&idx)
|
|
||||||
.not()
|
|
||||||
.then(|| match c {
|
|
||||||
NightChange::Protection {
|
|
||||||
target: t,
|
|
||||||
protection,
|
|
||||||
} => (*t == target).then_some((idx, protection)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
}) {
|
|
||||||
self.1.push(idx);
|
|
||||||
Some(c.clone())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
|
||||||
self.1
|
|
||||||
.contains(&idx)
|
|
||||||
.not()
|
|
||||||
.then(|| match c {
|
|
||||||
NightChange::Protection {
|
|
||||||
target: t,
|
|
||||||
protection,
|
|
||||||
} => (t == target).then_some(protection),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shapeshifter(&self) -> Option<&'a CharacterId> {
|
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
|
||||||
self.1
|
|
||||||
.contains(&idx)
|
|
||||||
.not()
|
|
||||||
.then_some(match c {
|
|
||||||
NightChange::Shapeshift { source, .. } => Some(source),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
|
|
||||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
|
||||||
self.1
|
|
||||||
.contains(&idx)
|
|
||||||
.not()
|
|
||||||
.then_some(match c {
|
|
||||||
NightChange::Kill {
|
|
||||||
target,
|
|
||||||
died_to:
|
|
||||||
DiedTo::Wolfpack {
|
|
||||||
night: _,
|
|
||||||
killing_wolf: _,
|
|
||||||
},
|
|
||||||
} => Some(target),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.flatten()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
mod kill;
|
mod kill;
|
||||||
pub(crate) mod night;
|
pub mod night;
|
||||||
mod settings;
|
mod settings;
|
||||||
|
pub mod story;
|
||||||
mod village;
|
mod village;
|
||||||
|
|
||||||
use core::{
|
use core::{
|
||||||
fmt::Debug,
|
fmt::{Debug, Display},
|
||||||
num::NonZeroU8,
|
num::NonZeroU8,
|
||||||
ops::{Deref, Range, RangeBounds},
|
ops::{Deref, Range, RangeBounds},
|
||||||
};
|
};
|
||||||
|
|
@ -15,7 +16,10 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{Night, ServerAction},
|
game::{
|
||||||
|
night::{Night, ServerAction},
|
||||||
|
story::{DayDetail, GameActions, GameStory, NightDetails},
|
||||||
|
},
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification,
|
CharacterState, Identification,
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
|
|
@ -31,18 +35,17 @@ type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Game {
|
pub struct Game {
|
||||||
previous: Vec<GameState>,
|
history: GameStory,
|
||||||
next: Vec<GameState>,
|
|
||||||
state: GameState,
|
state: GameState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
pub fn new(players: &[Identification], settings: GameSettings) -> Result<Self> {
|
||||||
|
let village = Village::new(players, settings)?;
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
next: Vec::new(),
|
history: GameStory::new(village.clone()),
|
||||||
previous: Vec::new(),
|
|
||||||
state: GameState::Night {
|
state: GameState::Night {
|
||||||
night: Night::new(Village::new(players, settings)?)?,
|
night: Night::new(village)?,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -82,11 +85,28 @@ impl Game {
|
||||||
self.process(HostGameMessage::GetState)
|
self.process(HostGameMessage::GetState)
|
||||||
}
|
}
|
||||||
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
||||||
|
let time = village.time();
|
||||||
if let Some(outcome) = village.execute(marked)? {
|
if let Some(outcome) = village.execute(marked)? {
|
||||||
|
log::warn!("adding to history for {}", village.time());
|
||||||
|
self.history.add(
|
||||||
|
village.time(),
|
||||||
|
GameActions::DayDetails(
|
||||||
|
marked.iter().map(|c| DayDetail::Execute(*c)).collect(),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
return Ok(ServerToHostMessage::GameOver(outcome));
|
return Ok(ServerToHostMessage::GameOver(outcome));
|
||||||
}
|
}
|
||||||
let night = Night::new(village.clone())?;
|
let night = Night::new(village.clone())?;
|
||||||
self.previous.push(self.state.clone());
|
log::warn!("adding to history for {time}");
|
||||||
|
self.history.add(
|
||||||
|
time,
|
||||||
|
GameActions::DayDetails(
|
||||||
|
marked
|
||||||
|
.iter()
|
||||||
|
.map(|mark| DayDetail::Execute(*mark))
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
|
)?;
|
||||||
self.state = GameState::Night { night };
|
self.state = GameState::Night { night };
|
||||||
self.process(HostGameMessage::GetState)
|
self.process(HostGameMessage::GetState)
|
||||||
}
|
}
|
||||||
|
|
@ -106,9 +126,9 @@ impl Game {
|
||||||
died_to: c.died_to().cloned(),
|
died_to: c.died_to().cloned(),
|
||||||
})
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
day: match village.date_time() {
|
day: match village.time() {
|
||||||
DateTime::Day { number } => number,
|
GameTime::Day { number } => number,
|
||||||
DateTime::Night { number: _ } => unreachable!(),
|
GameTime::Night { number: _ } => unreachable!(),
|
||||||
},
|
},
|
||||||
settings: village.settings(),
|
settings: village.settings(),
|
||||||
})
|
})
|
||||||
|
|
@ -126,8 +146,16 @@ impl Game {
|
||||||
match night.next() {
|
match night.next() {
|
||||||
Ok(_) => self.process(HostGameMessage::GetState),
|
Ok(_) => self.process(HostGameMessage::GetState),
|
||||||
Err(GameError::NightOver) => {
|
Err(GameError::NightOver) => {
|
||||||
let village = night.collect_completed()?;
|
let changes = night.collect_changes()?;
|
||||||
self.previous.push(self.state.clone());
|
let village = night.village().with_night_changes(&changes)?;
|
||||||
|
log::warn!("adding to history for {}", night.village().time());
|
||||||
|
self.history.add(
|
||||||
|
night.village().time(),
|
||||||
|
GameActions::NightDetails(NightDetails::new(
|
||||||
|
&night.used_actions(),
|
||||||
|
changes,
|
||||||
|
)),
|
||||||
|
)?;
|
||||||
self.state = GameState::Day {
|
self.state = GameState::Day {
|
||||||
village,
|
village,
|
||||||
marked: Vec::new(),
|
marked: Vec::new(),
|
||||||
|
|
@ -171,19 +199,8 @@ impl Game {
|
||||||
},
|
},
|
||||||
HostGameMessage::Night(_),
|
HostGameMessage::Night(_),
|
||||||
) => Err(GameError::InvalidMessageForGameState),
|
) => Err(GameError::InvalidMessageForGameState),
|
||||||
(
|
(GameState::Day { .. }, HostGameMessage::PreviousState) => {
|
||||||
GameState::Day {
|
Err(GameError::NoPreviousDuringDay)
|
||||||
village: _,
|
|
||||||
marked: _,
|
|
||||||
},
|
|
||||||
HostGameMessage::PreviousState,
|
|
||||||
) => {
|
|
||||||
let mut prev = self.previous.pop().ok_or(GameError::NoPreviousState)?;
|
|
||||||
log::info!("previous state loaded: {prev:?}");
|
|
||||||
core::mem::swap(&mut prev, &mut self.state);
|
|
||||||
self.next.push(prev);
|
|
||||||
|
|
||||||
self.process(HostGameMessage::GetState)
|
|
||||||
}
|
}
|
||||||
(GameState::Night { night }, HostGameMessage::PreviousState) => {
|
(GameState::Night { night }, HostGameMessage::PreviousState) => {
|
||||||
night.previous_state()?;
|
night.previous_state()?;
|
||||||
|
|
@ -192,6 +209,10 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn story(&self) -> GameStory {
|
||||||
|
self.history.clone()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn game_over(&self) -> Option<GameOver> {
|
pub fn game_over(&self) -> Option<GameOver> {
|
||||||
self.state.game_over()
|
self.state.game_over()
|
||||||
}
|
}
|
||||||
|
|
@ -204,10 +225,6 @@ impl Game {
|
||||||
pub fn game_state_mut(&mut self) -> &mut GameState {
|
pub fn game_state_mut(&mut self) -> &mut GameState {
|
||||||
&mut self.state
|
&mut self.state
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn previous_game_states(&self) -> &[GameState] {
|
|
||||||
&self.previous
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::large_enum_variant)]
|
#[allow(clippy::large_enum_variant)]
|
||||||
|
|
@ -296,37 +313,58 @@ pub enum Maybe {
|
||||||
Maybe,
|
Maybe,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)]
|
||||||
pub enum DateTime {
|
pub enum GameTime {
|
||||||
Day { number: NonZeroU8 },
|
Day { number: NonZeroU8 },
|
||||||
Night { number: u8 },
|
Night { number: u8 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for DateTime {
|
impl Display for GameTime {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
GameTime::Day { number } => write!(f, "Day {number}"),
|
||||||
|
GameTime::Night { number } => write!(f, "Night {number}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for GameTime {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
DateTime::Day {
|
GameTime::Day {
|
||||||
number: NonZeroU8::new(1).unwrap(),
|
number: NonZeroU8::new(1).unwrap(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DateTime {
|
impl GameTime {
|
||||||
pub const fn is_day(&self) -> bool {
|
pub const fn is_day(&self) -> bool {
|
||||||
matches!(self, DateTime::Day { number: _ })
|
matches!(self, GameTime::Day { number: _ })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn is_night(&self) -> bool {
|
pub const fn is_night(&self) -> bool {
|
||||||
matches!(self, DateTime::Night { number: _ })
|
matches!(self, GameTime::Night { number: _ })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn next(self) -> Self {
|
pub const fn next(self) -> Self {
|
||||||
match self {
|
match self {
|
||||||
DateTime::Day { number } => DateTime::Night {
|
GameTime::Day { number } => GameTime::Night {
|
||||||
number: number.get(),
|
number: number.get(),
|
||||||
},
|
},
|
||||||
DateTime::Night { number } => DateTime::Day {
|
GameTime::Night { number } => GameTime::Day {
|
||||||
number: NonZeroU8::new(number + 1).unwrap(),
|
number: NonZeroU8::new(number + 1).unwrap(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn previous(self) -> Option<Self> {
|
||||||
|
match self {
|
||||||
|
GameTime::Day { number } => Some(GameTime::Night {
|
||||||
|
number: number.get() - 1,
|
||||||
|
}),
|
||||||
|
GameTime::Night { number } => match NonZeroU8::new(number) {
|
||||||
|
Some(number) => Some(GameTime::Day { number }),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
pub mod changes;
|
||||||
|
mod process;
|
||||||
|
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
|
@ -10,54 +13,16 @@ use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
DateTime, Village,
|
GameTime, Village,
|
||||||
kill::{self, ChangesLookup},
|
kill::{self},
|
||||||
},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
message::{
|
story::NightChoice,
|
||||||
CharacterIdentity,
|
|
||||||
night::{ActionPrompt, ActionResponse, ActionResult, Visits},
|
|
||||||
},
|
},
|
||||||
|
message::night::{ActionPrompt, ActionResponse, ActionResult, Visits},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle},
|
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Extract)]
|
|
||||||
pub enum NightChange {
|
|
||||||
RoleChange(CharacterId, RoleTitle),
|
|
||||||
HunterTarget {
|
|
||||||
source: CharacterId,
|
|
||||||
target: CharacterId,
|
|
||||||
},
|
|
||||||
Kill {
|
|
||||||
target: CharacterId,
|
|
||||||
died_to: DiedTo,
|
|
||||||
},
|
|
||||||
RoleBlock {
|
|
||||||
source: CharacterId,
|
|
||||||
target: CharacterId,
|
|
||||||
block_type: RoleBlock,
|
|
||||||
},
|
|
||||||
Shapeshift {
|
|
||||||
source: CharacterId,
|
|
||||||
into: CharacterId,
|
|
||||||
},
|
|
||||||
Protection {
|
|
||||||
target: CharacterId,
|
|
||||||
protection: Protection,
|
|
||||||
},
|
|
||||||
ElderReveal {
|
|
||||||
elder: CharacterId,
|
|
||||||
},
|
|
||||||
MasonRecruit {
|
|
||||||
mason_leader: CharacterId,
|
|
||||||
recruiting: CharacterId,
|
|
||||||
},
|
|
||||||
EmpathFoundScapegoat {
|
|
||||||
empath: CharacterId,
|
|
||||||
scapegoat: CharacterId,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
enum BlockResolvedOutcome {
|
enum BlockResolvedOutcome {
|
||||||
PromptUpdate(ActionPrompt),
|
PromptUpdate(ActionPrompt),
|
||||||
ActionComplete(ActionResult, Option<NightChange>),
|
ActionComplete(ActionResult, Option<NightChange>),
|
||||||
|
|
@ -252,9 +217,9 @@ pub struct Night {
|
||||||
|
|
||||||
impl Night {
|
impl Night {
|
||||||
pub fn new(village: Village) -> Result<Self> {
|
pub fn new(village: Village) -> Result<Self> {
|
||||||
let night = match village.date_time() {
|
let night = match village.time() {
|
||||||
DateTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
DateTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter = if village.executed_known_elder() {
|
let filter = if village.executed_known_elder() {
|
||||||
|
|
@ -308,21 +273,21 @@ impl Night {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// changes that require no input (such as hunter firing)
|
/// changes that require no input (such as hunter firing)
|
||||||
fn automatic_changes(village: &Village, night: u8) -> Vec<NightChange> {
|
fn automatic_changes(&self) -> Vec<NightChange> {
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
let night = match NonZeroU8::new(night) {
|
let night = match NonZeroU8::new(self.night) {
|
||||||
Some(night) => night,
|
Some(night) => night,
|
||||||
None => return changes,
|
None => return changes,
|
||||||
};
|
};
|
||||||
if !village.executed_known_elder() {
|
if !self.village.executed_known_elder() {
|
||||||
village
|
self.village
|
||||||
.dead_characters()
|
.dead_characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||||
.filter_map(|(c, d)| c.hunter().ok().and_then(|h| *h).map(|t| (c, t, d)))
|
.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() {
|
.filter_map(|(c, t, d)| match d.date_time() {
|
||||||
DateTime::Day { number } => (number.get() == night.get()).then_some((c, t)),
|
GameTime::Day { number } => (number.get() == night.get()).then_some((c, t)),
|
||||||
DateTime::Night { number: _ } => None,
|
GameTime::Night { number: _ } => None,
|
||||||
})
|
})
|
||||||
.map(|(c, target)| NightChange::Kill {
|
.map(|(c, target)| NightChange::Kill {
|
||||||
target,
|
target,
|
||||||
|
|
@ -387,143 +352,13 @@ impl Night {
|
||||||
self.action_queue.iter().cloned().collect()
|
self.action_queue.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn collect_completed(&self) -> Result<Village> {
|
pub fn collect_changes(&self) -> Result<Box<[NightChange]>> {
|
||||||
if !matches!(self.night_state, NightState::Complete) {
|
if !matches!(self.night_state, NightState::Complete) {
|
||||||
return Err(GameError::NotEndOfNight);
|
return Err(GameError::NotEndOfNight);
|
||||||
}
|
}
|
||||||
let mut new_village = self.village.clone();
|
let mut all_changes = self.automatic_changes();
|
||||||
let mut all_changes = Self::automatic_changes(&self.village, self.night);
|
|
||||||
all_changes.append(&mut self.changes_from_actions().into_vec());
|
all_changes.append(&mut self.changes_from_actions().into_vec());
|
||||||
let mut changes = ChangesLookup::new(&all_changes);
|
Ok(all_changes.into_boxed_slice())
|
||||||
for change in all_changes.iter() {
|
|
||||||
match change {
|
|
||||||
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)?
|
|
||||||
.role_change(*role_title, DateTime::Night { number: self.night })?,
|
|
||||||
NightChange::HunterTarget { source, target } => {
|
|
||||||
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
|
||||||
hunter_character.hunter_mut()?.replace(*target);
|
|
||||||
if changes.killed(*source).is_some()
|
|
||||||
&& changes.protected(source).is_none()
|
|
||||||
&& changes.protected(target).is_none()
|
|
||||||
{
|
|
||||||
new_village
|
|
||||||
.character_by_id_mut(*target)
|
|
||||||
.unwrap()
|
|
||||||
.kill(DiedTo::Hunter {
|
|
||||||
killer: *source,
|
|
||||||
night: NonZeroU8::new(self.night).unwrap(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NightChange::Kill { target, died_to } => {
|
|
||||||
if let Some(kill) = kill::resolve_kill(
|
|
||||||
&mut changes,
|
|
||||||
*target,
|
|
||||||
died_to,
|
|
||||||
self.night,
|
|
||||||
&self.village,
|
|
||||||
)? {
|
|
||||||
kill.apply_to_village(&mut new_village)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NightChange::Shapeshift { source, into } => {
|
|
||||||
if let Some(target) = changes.wolf_pack_kill_target()
|
|
||||||
&& changes.protected(target).is_none()
|
|
||||||
{
|
|
||||||
if *target != *into {
|
|
||||||
log::error!("shapeshift into({into}) != target({target})");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let ss = new_village.character_by_id_mut(*source).unwrap();
|
|
||||||
ss.shapeshifter_mut().unwrap().replace(*target);
|
|
||||||
ss.kill(DiedTo::Shapeshift {
|
|
||||||
into: *target,
|
|
||||||
night: NonZeroU8::new(self.night).unwrap(),
|
|
||||||
});
|
|
||||||
// role change pushed in [apply_shapeshift]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
NightChange::RoleBlock {
|
|
||||||
source: _,
|
|
||||||
target: _,
|
|
||||||
block_type: _,
|
|
||||||
}
|
|
||||||
| NightChange::Protection {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// black knights death
|
|
||||||
for knight in new_village
|
|
||||||
.characters_mut()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|k| k.alive())
|
|
||||||
.filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some())
|
|
||||||
.filter(|k| changes.killed(k.character_id()).is_none())
|
|
||||||
{
|
|
||||||
knight.black_knight_kill()?.kill();
|
|
||||||
}
|
|
||||||
|
|
||||||
// pyre masters death
|
|
||||||
let village_dead = new_village
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|c| c.is_village())
|
|
||||||
.filter_map(|c| c.died_to().cloned())
|
|
||||||
.filter_map(|c| c.killer().map(|k| (k, c)))
|
|
||||||
.collect::<Box<[_]>>();
|
|
||||||
for pyremaster in new_village
|
|
||||||
.living_characters_by_role_mut(RoleTitle::PyreMaster)
|
|
||||||
.into_iter()
|
|
||||||
.filter(|p| {
|
|
||||||
village_dead
|
|
||||||
.iter()
|
|
||||||
.filter(|(k, _)| *k == p.character_id())
|
|
||||||
.count()
|
|
||||||
>= PYREMASTER_VILLAGER_KILLS_TO_DIE.get() as usize
|
|
||||||
})
|
|
||||||
{
|
|
||||||
if let Some(night) = NonZeroU8::new(self.night) {
|
|
||||||
pyremaster.kill(DiedTo::PyreMasterLynchMob {
|
|
||||||
night,
|
|
||||||
source: pyremaster.character_id(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if new_village.is_game_over().is_none() {
|
|
||||||
new_village.to_day()?;
|
|
||||||
}
|
|
||||||
Ok(new_village)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_mason_recruit(
|
fn apply_mason_recruit(
|
||||||
|
|
@ -730,11 +565,7 @@ impl Night {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
match (
|
match (self.process(resp)?, current_wolfy, next_wolfy) {
|
||||||
self.received_response_inner(resp)?,
|
|
||||||
current_wolfy,
|
|
||||||
next_wolfy,
|
|
||||||
) {
|
|
||||||
(ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
(ResponseOutcome::PromptUpdate(p), _, _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
||||||
(
|
(
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
|
@ -815,483 +646,6 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn received_response_inner(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
|
|
||||||
let current_prompt = match &self.night_state {
|
|
||||||
NightState::Active {
|
|
||||||
current_prompt: _,
|
|
||||||
current_result: Some(_),
|
|
||||||
..
|
|
||||||
} => return Err(GameError::NightNeedsNext),
|
|
||||||
NightState::Active {
|
|
||||||
current_prompt,
|
|
||||||
current_result: None,
|
|
||||||
..
|
|
||||||
} => current_prompt,
|
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
|
||||||
};
|
|
||||||
|
|
||||||
match resp {
|
|
||||||
ActionResponse::MarkTarget(mark) => {
|
|
||||||
return Ok(ResponseOutcome::PromptUpdate(
|
|
||||||
current_prompt.with_mark(mark)?,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
ActionResponse::Shapeshift => {
|
|
||||||
return match current_prompt {
|
|
||||||
ActionPrompt::Shapeshifter {
|
|
||||||
character_id: source,
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Shapeshift {
|
|
||||||
source: source.character_id,
|
|
||||||
into: self
|
|
||||||
.changes_from_actions()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|c| match c {
|
|
||||||
NightChange::Kill {
|
|
||||||
target,
|
|
||||||
died_to: DiedTo::Wolfpack { .. },
|
|
||||||
} => Some(target),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.ok_or(GameError::InvalidTarget)?,
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
_ => Err(GameError::InvalidMessageForGameState),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
ActionResponse::Continue => {
|
|
||||||
if let ActionPrompt::Insomniac { character_id } = current_prompt {
|
|
||||||
return Ok(ActionComplete {
|
|
||||||
result: ActionResult::Insomniac(
|
|
||||||
self.get_visits_for(character_id.character_id),
|
|
||||||
),
|
|
||||||
change: None,
|
|
||||||
}
|
|
||||||
.into());
|
|
||||||
}
|
|
||||||
if let ActionPrompt::RoleChange {
|
|
||||||
character_id,
|
|
||||||
new_role,
|
|
||||||
} = current_prompt
|
|
||||||
{
|
|
||||||
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::RoleChange(
|
|
||||||
character_id.character_id,
|
|
||||||
*new_role,
|
|
||||||
)),
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match current_prompt {
|
|
||||||
ActionPrompt::LoneWolfKill {
|
|
||||||
character_id,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Kill {
|
|
||||||
target: *marked,
|
|
||||||
died_to: DiedTo::LoneWolf {
|
|
||||||
killer: character_id.character_id,
|
|
||||||
night: self.night,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
.into()),
|
|
||||||
ActionPrompt::RoleChange { .. }
|
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
|
||||||
| ActionPrompt::CoverOfDarkness => {
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::ElderReveal { character_id } => {
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::ElderReveal {
|
|
||||||
elder: character_id.character_id,
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::Seer {
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let alignment = self.village.character_by_id(*marked)?.alignment();
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::Seer(alignment),
|
|
||||||
change: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::Protector {
|
|
||||||
marked: Some(marked),
|
|
||||||
character_id,
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Protection {
|
|
||||||
target: *marked,
|
|
||||||
protection: Protection::Protector {
|
|
||||||
source: character_id.character_id,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::Arcanist {
|
|
||||||
marked: (Some(marked1), Some(marked2)),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
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,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::Gravedigger {
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig();
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GraveDigger(dig_role),
|
|
||||||
change: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::Hunter {
|
|
||||||
character_id,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::HunterTarget {
|
|
||||||
source: character_id.character_id,
|
|
||||||
target: *marked,
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::Militia {
|
|
||||||
character_id,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Kill {
|
|
||||||
target: *marked,
|
|
||||||
died_to: DiedTo::Militia {
|
|
||||||
killer: character_id.character_id,
|
|
||||||
night: NonZeroU8::new(self.night)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::Militia { marked: None, .. } => {
|
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
|
||||||
}
|
|
||||||
ActionPrompt::MapleWolf {
|
|
||||||
character_id,
|
|
||||||
kill_or_die,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Kill {
|
|
||||||
target: *marked,
|
|
||||||
died_to: DiedTo::MapleWolf {
|
|
||||||
source: character_id.character_id,
|
|
||||||
night: NonZeroU8::new(self.night)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
|
||||||
starves_if_fails: *kill_or_die,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::MapleWolf { marked: None, .. } => {
|
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
|
||||||
}
|
|
||||||
ActionPrompt::Guardian {
|
|
||||||
character_id,
|
|
||||||
previous: None,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Protection {
|
|
||||||
target: *marked,
|
|
||||||
protection: Protection::Guardian {
|
|
||||||
source: character_id.character_id,
|
|
||||||
guarding: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::Guardian {
|
|
||||||
character_id,
|
|
||||||
previous: Some(PreviousGuardianAction::Guard(prev_target)),
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
if prev_target.character_id == *marked {
|
|
||||||
return Err(GameError::InvalidTarget);
|
|
||||||
}
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Protection {
|
|
||||||
target: *marked,
|
|
||||||
protection: Protection::Guardian {
|
|
||||||
source: character_id.character_id,
|
|
||||||
guarding: false,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::Guardian {
|
|
||||||
character_id,
|
|
||||||
previous: Some(PreviousGuardianAction::Protect(prev_protect)),
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Protection {
|
|
||||||
target: *marked,
|
|
||||||
protection: Protection::Guardian {
|
|
||||||
source: character_id.character_id,
|
|
||||||
guarding: prev_protect.character_id == *marked,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::WolfPackKill {
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Kill {
|
|
||||||
target: *marked,
|
|
||||||
died_to: DiedTo::Wolfpack {
|
|
||||||
killing_wolf: self
|
|
||||||
.village
|
|
||||||
.killing_wolf()
|
|
||||||
.ok_or(GameError::NoWolves)?
|
|
||||||
.character_id(),
|
|
||||||
night: NonZeroU8::new(self.night)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::Shapeshifter { character_id } => {
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: match &resp {
|
|
||||||
ActionResponse::Continue => None,
|
|
||||||
ActionResponse::Shapeshift => Some(NightChange::Shapeshift {
|
|
||||||
source: character_id.character_id,
|
|
||||||
into: self
|
|
||||||
.changes_from_actions()
|
|
||||||
.into_iter()
|
|
||||||
.find_map(|c| match c {
|
|
||||||
NightChange::Kill {
|
|
||||||
target,
|
|
||||||
died_to: DiedTo::Wolfpack { .. },
|
|
||||||
} => Some(target),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.ok_or(GameError::InvalidTarget)?,
|
|
||||||
}),
|
|
||||||
_ => return Err(GameError::InvalidMessageForGameState),
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
ActionPrompt::AlphaWolf {
|
|
||||||
character_id,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::Kill {
|
|
||||||
target: *marked,
|
|
||||||
died_to: DiedTo::AlphaWolf {
|
|
||||||
killer: character_id.character_id,
|
|
||||||
night: NonZeroU8::new(self.night)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
|
||||||
}
|
|
||||||
ActionPrompt::DireWolf {
|
|
||||||
character_id,
|
|
||||||
marked: Some(marked),
|
|
||||||
..
|
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: Some(NightChange::RoleBlock {
|
|
||||||
source: character_id.character_id,
|
|
||||||
target: *marked,
|
|
||||||
block_type: RoleBlock::Direwolf,
|
|
||||||
}),
|
|
||||||
})),
|
|
||||||
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;
|
|
||||||
|
|
||||||
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::Insomniac { .. } => Ok(ActionComplete {
|
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: None,
|
|
||||||
}
|
|
||||||
.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),
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| ActionPrompt::Arcanist {
|
|
||||||
marked: (None, Some(_)),
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| ActionPrompt::Arcanist {
|
|
||||||
marked: (Some(_), None),
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
|
||||||
| ActionPrompt::Gravedigger { marked: None, .. }
|
|
||||||
| ActionPrompt::Hunter { marked: None, .. }
|
|
||||||
| ActionPrompt::Guardian { marked: None, .. }
|
|
||||||
| ActionPrompt::WolfPackKill { marked: None, .. }
|
|
||||||
| ActionPrompt::DireWolf { marked: None, .. }
|
|
||||||
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn village(&self) -> &Village {
|
pub const fn village(&self) -> &Village {
|
||||||
&self.village
|
&self.village
|
||||||
}
|
}
|
||||||
|
|
@ -1368,6 +722,13 @@ impl Night {
|
||||||
matches!(self.night_state, NightState::Complete)
|
matches!(self.night_state, NightState::Complete)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn used_actions(&self) -> Box<[(ActionPrompt, ActionResult)]> {
|
||||||
|
self.used_actions
|
||||||
|
.iter()
|
||||||
|
.map(|(p, r, _)| (p.clone(), r.clone()))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn next(&mut self) -> Result<()> {
|
pub fn next(&mut self) -> Result<()> {
|
||||||
match &self.night_state {
|
match &self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
|
|
@ -1413,8 +774,7 @@ impl Night {
|
||||||
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
||||||
self.used_actions
|
self.used_actions
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(_, _, act)| act.into_iter())
|
.flat_map(|(_, _, act)| act.iter())
|
||||||
.flatten()
|
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
@ -1561,9 +921,8 @@ impl Night {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn next_page(&mut self) {
|
pub fn next_page(&mut self) {
|
||||||
match &mut self.night_state {
|
if let NightState::Active { current_page, .. } = &mut self.night_state {
|
||||||
NightState::Active { current_page, .. } => *current_page += 1,
|
*current_page += 1
|
||||||
_ => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,138 @@
|
||||||
|
use core::ops::Not;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::Extract;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
|
diedto::DiedTo,
|
||||||
|
player::Protection,
|
||||||
|
role::{RoleBlock, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, PartialEq, Deserialize, Extract)]
|
||||||
|
pub enum NightChange {
|
||||||
|
RoleChange(CharacterId, RoleTitle),
|
||||||
|
HunterTarget {
|
||||||
|
source: CharacterId,
|
||||||
|
target: CharacterId,
|
||||||
|
},
|
||||||
|
Kill {
|
||||||
|
target: CharacterId,
|
||||||
|
died_to: DiedTo,
|
||||||
|
},
|
||||||
|
RoleBlock {
|
||||||
|
source: CharacterId,
|
||||||
|
target: CharacterId,
|
||||||
|
block_type: RoleBlock,
|
||||||
|
},
|
||||||
|
Shapeshift {
|
||||||
|
source: CharacterId,
|
||||||
|
into: CharacterId,
|
||||||
|
},
|
||||||
|
Protection {
|
||||||
|
target: CharacterId,
|
||||||
|
protection: Protection,
|
||||||
|
},
|
||||||
|
ElderReveal {
|
||||||
|
elder: CharacterId,
|
||||||
|
},
|
||||||
|
MasonRecruit {
|
||||||
|
mason_leader: CharacterId,
|
||||||
|
recruiting: CharacterId,
|
||||||
|
},
|
||||||
|
EmpathFoundScapegoat {
|
||||||
|
empath: CharacterId,
|
||||||
|
scapegoat: CharacterId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
||||||
|
|
||||||
|
impl<'a> ChangesLookup<'a> {
|
||||||
|
pub fn new(changes: &'a [NightChange]) -> Self {
|
||||||
|
Self(changes, Vec::new())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
||||||
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
|
self.1
|
||||||
|
.contains(&idx)
|
||||||
|
.not()
|
||||||
|
.then(|| match c {
|
||||||
|
NightChange::Kill { target: t, died_to } => (*t == target).then_some(died_to),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn protected_take(&mut self, target: CharacterId) -> Option<Protection> {
|
||||||
|
if let Some((idx, c)) = self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
|
self.1
|
||||||
|
.contains(&idx)
|
||||||
|
.not()
|
||||||
|
.then(|| match c {
|
||||||
|
NightChange::Protection {
|
||||||
|
target: t,
|
||||||
|
protection,
|
||||||
|
} => (*t == target).then_some((idx, protection)),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
}) {
|
||||||
|
self.1.push(idx);
|
||||||
|
Some(c.clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
||||||
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
|
self.1
|
||||||
|
.contains(&idx)
|
||||||
|
.not()
|
||||||
|
.then(|| match c {
|
||||||
|
NightChange::Protection {
|
||||||
|
target: t,
|
||||||
|
protection,
|
||||||
|
} => (t == target).then_some(protection),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn shapeshifter(&self) -> Option<&'a CharacterId> {
|
||||||
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
|
self.1
|
||||||
|
.contains(&idx)
|
||||||
|
.not()
|
||||||
|
.then_some(match c {
|
||||||
|
NightChange::Shapeshift { source, .. } => Some(source),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wolf_pack_kill_target(&self) -> Option<&'a CharacterId> {
|
||||||
|
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||||
|
self.1
|
||||||
|
.contains(&idx)
|
||||||
|
.not()
|
||||||
|
.then_some(match c {
|
||||||
|
NightChange::Kill {
|
||||||
|
target,
|
||||||
|
died_to:
|
||||||
|
DiedTo::Wolfpack {
|
||||||
|
night: _,
|
||||||
|
killing_wolf: _,
|
||||||
|
},
|
||||||
|
} => Some(target),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,491 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::night::{ActionComplete, Night, NightState, ResponseOutcome, changes::NightChange},
|
||||||
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
|
player::Protection,
|
||||||
|
role::{AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
|
impl Night {
|
||||||
|
pub(super) fn process(&self, resp: ActionResponse) -> Result<ResponseOutcome> {
|
||||||
|
let current_prompt = match &self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt: _,
|
||||||
|
current_result: Some(_),
|
||||||
|
..
|
||||||
|
} => return Err(GameError::NightNeedsNext),
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: None,
|
||||||
|
..
|
||||||
|
} => current_prompt,
|
||||||
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
|
};
|
||||||
|
|
||||||
|
match resp {
|
||||||
|
ActionResponse::MarkTarget(mark) => {
|
||||||
|
return Ok(ResponseOutcome::PromptUpdate(
|
||||||
|
current_prompt.with_mark(mark)?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
ActionResponse::Shapeshift => {
|
||||||
|
return match current_prompt {
|
||||||
|
ActionPrompt::Shapeshifter {
|
||||||
|
character_id: source,
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Shapeshift {
|
||||||
|
source: source.character_id,
|
||||||
|
into: self
|
||||||
|
.changes_from_actions()
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|c| match c {
|
||||||
|
NightChange::Kill {
|
||||||
|
target,
|
||||||
|
died_to: DiedTo::Wolfpack { .. },
|
||||||
|
} => Some(target),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.ok_or(GameError::InvalidTarget)?,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
_ => Err(GameError::InvalidMessageForGameState),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ActionResponse::Continue => {
|
||||||
|
if let ActionPrompt::Insomniac { character_id } = current_prompt {
|
||||||
|
return Ok(ActionComplete {
|
||||||
|
result: ActionResult::Insomniac(
|
||||||
|
self.get_visits_for(character_id.character_id),
|
||||||
|
),
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
if let ActionPrompt::RoleChange {
|
||||||
|
character_id,
|
||||||
|
new_role,
|
||||||
|
} = current_prompt
|
||||||
|
{
|
||||||
|
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::RoleChange(
|
||||||
|
character_id.character_id,
|
||||||
|
*new_role,
|
||||||
|
)),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
match current_prompt {
|
||||||
|
ActionPrompt::LoneWolfKill {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Kill {
|
||||||
|
target: *marked,
|
||||||
|
died_to: DiedTo::LoneWolf {
|
||||||
|
killer: character_id.character_id,
|
||||||
|
night: self.night,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
|
ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::ElderReveal { character_id } => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::ElderReveal {
|
||||||
|
elder: character_id.character_id,
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::Seer {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let alignment = self.village.character_by_id(*marked)?.alignment();
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::Seer(alignment),
|
||||||
|
change: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::Protector {
|
||||||
|
marked: Some(marked),
|
||||||
|
character_id,
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Protection {
|
||||||
|
target: *marked,
|
||||||
|
protection: Protection::Protector {
|
||||||
|
source: character_id.character_id,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::Arcanist {
|
||||||
|
marked: (Some(marked1), Some(marked2)),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let same = self.village.character_by_id(*marked1)?.alignment()
|
||||||
|
== self.village.character_by_id(*marked2)?.alignment();
|
||||||
|
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
||||||
|
change: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::Gravedigger {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig();
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GraveDigger(dig_role),
|
||||||
|
change: None,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::Hunter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::HunterTarget {
|
||||||
|
source: character_id.character_id,
|
||||||
|
target: *marked,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::Militia {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Kill {
|
||||||
|
target: *marked,
|
||||||
|
died_to: DiedTo::Militia {
|
||||||
|
killer: character_id.character_id,
|
||||||
|
night: NonZeroU8::new(self.night)
|
||||||
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::Militia { marked: None, .. } => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
}
|
||||||
|
ActionPrompt::MapleWolf {
|
||||||
|
character_id,
|
||||||
|
kill_or_die,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Kill {
|
||||||
|
target: *marked,
|
||||||
|
died_to: DiedTo::MapleWolf {
|
||||||
|
source: character_id.character_id,
|
||||||
|
night: NonZeroU8::new(self.night)
|
||||||
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
|
starves_if_fails: *kill_or_die,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::MapleWolf { marked: None, .. } => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
}
|
||||||
|
ActionPrompt::Guardian {
|
||||||
|
character_id,
|
||||||
|
previous: None,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Protection {
|
||||||
|
target: *marked,
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: character_id.character_id,
|
||||||
|
guarding: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::Guardian {
|
||||||
|
character_id,
|
||||||
|
previous: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
if prev_target.character_id == *marked {
|
||||||
|
return Err(GameError::InvalidTarget);
|
||||||
|
}
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Protection {
|
||||||
|
target: *marked,
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: character_id.character_id,
|
||||||
|
guarding: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::Guardian {
|
||||||
|
character_id,
|
||||||
|
previous: Some(PreviousGuardianAction::Protect(prev_protect)),
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Protection {
|
||||||
|
target: *marked,
|
||||||
|
protection: Protection::Guardian {
|
||||||
|
source: character_id.character_id,
|
||||||
|
guarding: prev_protect.character_id == *marked,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::WolfPackKill {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Kill {
|
||||||
|
target: *marked,
|
||||||
|
died_to: DiedTo::Wolfpack {
|
||||||
|
killing_wolf: self
|
||||||
|
.village
|
||||||
|
.killing_wolf()
|
||||||
|
.ok_or(GameError::NoWolves)?
|
||||||
|
.character_id(),
|
||||||
|
night: NonZeroU8::new(self.night)
|
||||||
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::Shapeshifter { character_id } => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: match &resp {
|
||||||
|
ActionResponse::Continue => None,
|
||||||
|
ActionResponse::Shapeshift => Some(NightChange::Shapeshift {
|
||||||
|
source: character_id.character_id,
|
||||||
|
into: self
|
||||||
|
.changes_from_actions()
|
||||||
|
.into_iter()
|
||||||
|
.find_map(|c| match c {
|
||||||
|
NightChange::Kill {
|
||||||
|
target,
|
||||||
|
died_to: DiedTo::Wolfpack { .. },
|
||||||
|
} => Some(target),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.ok_or(GameError::InvalidTarget)?,
|
||||||
|
}),
|
||||||
|
_ => return Err(GameError::InvalidMessageForGameState),
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
ActionPrompt::AlphaWolf {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::Kill {
|
||||||
|
target: *marked,
|
||||||
|
died_to: DiedTo::AlphaWolf {
|
||||||
|
killer: character_id.character_id,
|
||||||
|
night: NonZeroU8::new(self.night)
|
||||||
|
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
||||||
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
}
|
||||||
|
ActionPrompt::DireWolf {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::RoleBlock {
|
||||||
|
source: character_id.character_id,
|
||||||
|
target: *marked,
|
||||||
|
block_type: RoleBlock::Direwolf,
|
||||||
|
}),
|
||||||
|
})),
|
||||||
|
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;
|
||||||
|
|
||||||
|
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::Insomniac { .. } => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.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),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Arcanist {
|
||||||
|
marked: (None, Some(_)),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Arcanist {
|
||||||
|
marked: (Some(_), None),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||||
|
| ActionPrompt::Gravedigger { marked: None, .. }
|
||||||
|
| ActionPrompt::Hunter { marked: None, .. }
|
||||||
|
| ActionPrompt::Guardian { marked: None, .. }
|
||||||
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||||
|
| ActionPrompt::DireWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,458 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
character::CharacterId,
|
||||||
|
diedto::DiedToTitle,
|
||||||
|
error::GameError,
|
||||||
|
game::{GameTime, Village, night::changes::NightChange},
|
||||||
|
message::night::{ActionPrompt, ActionResult},
|
||||||
|
role::{Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum GameActions {
|
||||||
|
DayDetails(Box<[DayDetail]>),
|
||||||
|
NightDetails(NightDetails),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum DayDetail {
|
||||||
|
Execute(CharacterId),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct NightDetails {
|
||||||
|
pub choices: Box<[NightChoice]>,
|
||||||
|
pub changes: Box<[NightChange]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NightDetails {
|
||||||
|
pub fn new(choices: &[(ActionPrompt, ActionResult)], changes: Box<[NightChange]>) -> Self {
|
||||||
|
Self {
|
||||||
|
changes,
|
||||||
|
choices: choices
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.filter_map(|(prompt, result)| NightChoice::new(prompt, result))
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct NightChoice {
|
||||||
|
pub prompt: StoryActionPrompt,
|
||||||
|
pub result: Option<StoryActionResult>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NightChoice {
|
||||||
|
pub fn new(prompt: ActionPrompt, result: ActionResult) -> Option<Self> {
|
||||||
|
Some(Self {
|
||||||
|
prompt: StoryActionPrompt::new(prompt)?,
|
||||||
|
result: StoryActionResult::new(result),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum StoryActionResult {
|
||||||
|
RoleBlocked,
|
||||||
|
Seer(Alignment),
|
||||||
|
PowerSeer { powerful: Powerful },
|
||||||
|
Adjudicator { killer: Killer },
|
||||||
|
Arcanist(AlignmentEq),
|
||||||
|
GraveDigger(Option<RoleTitle>),
|
||||||
|
Mortician(DiedToTitle),
|
||||||
|
Insomniac { visits: Box<[CharacterId]> },
|
||||||
|
Empath { scapegoat: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoryActionResult {
|
||||||
|
pub fn new(result: ActionResult) -> Option<Self> {
|
||||||
|
Some(match result {
|
||||||
|
ActionResult::RoleBlocked => Self::RoleBlocked,
|
||||||
|
ActionResult::Seer(alignment) => Self::Seer(alignment),
|
||||||
|
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
|
||||||
|
ActionResult::Adjudicator { killer } => Self::Adjudicator { killer },
|
||||||
|
ActionResult::Arcanist(same) => Self::Arcanist(same),
|
||||||
|
ActionResult::GraveDigger(role_title) => Self::GraveDigger(role_title),
|
||||||
|
ActionResult::Mortician(died_to) => Self::Mortician(died_to),
|
||||||
|
ActionResult::Insomniac(visits) => Self::Insomniac {
|
||||||
|
visits: visits.iter().map(|c| c.character_id).collect(),
|
||||||
|
},
|
||||||
|
ActionResult::Empath { scapegoat } => Self::Empath { scapegoat },
|
||||||
|
|
||||||
|
ActionResult::GoBackToSleep | ActionResult::Continue => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum StoryActionPrompt {
|
||||||
|
Seer {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Protector {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Arcanist {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: (CharacterId, CharacterId),
|
||||||
|
},
|
||||||
|
Gravedigger {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Hunter {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Militia {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
MapleWolf {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Guardian {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
guarding: bool,
|
||||||
|
},
|
||||||
|
Adjudicator {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
PowerSeer {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Mortician {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Beholder {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
MasonsWake {
|
||||||
|
leader: CharacterId,
|
||||||
|
masons: Box<[CharacterId]>,
|
||||||
|
},
|
||||||
|
MasonLeaderRecruit {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Empath {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Vindicator {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
PyreMaster {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
WolfPackKill {
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Shapeshifter {
|
||||||
|
character_id: CharacterId,
|
||||||
|
},
|
||||||
|
AlphaWolf {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
DireWolf {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
LoneWolfKill {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
|
Insomniac {
|
||||||
|
character_id: CharacterId,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoryActionPrompt {
|
||||||
|
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||||
|
Some(match prompt {
|
||||||
|
ActionPrompt::Seer {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Seer {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Arcanist {
|
||||||
|
character_id,
|
||||||
|
marked: (Some(marked1), Some(marked2)),
|
||||||
|
..
|
||||||
|
} => Self::Arcanist {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: (marked1, marked2),
|
||||||
|
},
|
||||||
|
ActionPrompt::Protector {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Protector {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Gravedigger {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Gravedigger {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Hunter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Hunter {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Militia {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Militia {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::MapleWolf {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::MapleWolf {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Guardian {
|
||||||
|
character_id,
|
||||||
|
previous,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Guardian {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
guarding: previous
|
||||||
|
.map(|prev| match prev {
|
||||||
|
PreviousGuardianAction::Protect(id) => id.character_id == marked,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
},
|
||||||
|
ActionPrompt::Adjudicator {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Adjudicator {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::PowerSeer {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::PowerSeer {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Mortician {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Mortician {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Beholder {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Beholder {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::MasonsWake { leader, masons } => Self::MasonsWake {
|
||||||
|
leader,
|
||||||
|
masons: masons.into_iter().map(|c| c.character_id).collect(),
|
||||||
|
},
|
||||||
|
ActionPrompt::MasonLeaderRecruit {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::MasonLeaderRecruit {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Empath {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Empath {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Vindicator {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Vindicator {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::PyreMaster {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::PyreMaster {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::WolfPackKill {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::WolfPackKill { chosen: marked },
|
||||||
|
ActionPrompt::Shapeshifter { character_id } => Self::Shapeshifter {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
},
|
||||||
|
ActionPrompt::AlphaWolf {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::AlphaWolf {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::DireWolf {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::DireWolf {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::LoneWolfKill {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::LoneWolfKill {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
|
ActionPrompt::Insomniac { character_id } => Self::Insomniac {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
},
|
||||||
|
|
||||||
|
ActionPrompt::Protector { .. }
|
||||||
|
| ActionPrompt::Gravedigger { .. }
|
||||||
|
| ActionPrompt::Hunter { .. }
|
||||||
|
| ActionPrompt::Militia { .. }
|
||||||
|
| ActionPrompt::MapleWolf { .. }
|
||||||
|
| ActionPrompt::Guardian { .. }
|
||||||
|
| ActionPrompt::Adjudicator { .. }
|
||||||
|
| ActionPrompt::PowerSeer { .. }
|
||||||
|
| ActionPrompt::Mortician { .. }
|
||||||
|
| ActionPrompt::Beholder { .. }
|
||||||
|
| ActionPrompt::MasonLeaderRecruit { .. }
|
||||||
|
| ActionPrompt::Empath { .. }
|
||||||
|
| ActionPrompt::Vindicator { .. }
|
||||||
|
| ActionPrompt::PyreMaster { .. }
|
||||||
|
| ActionPrompt::WolfPackKill { .. }
|
||||||
|
| ActionPrompt::AlphaWolf { .. }
|
||||||
|
| ActionPrompt::DireWolf { .. }
|
||||||
|
| ActionPrompt::LoneWolfKill { .. }
|
||||||
|
| ActionPrompt::Seer { .. }
|
||||||
|
| ActionPrompt::Arcanist { .. }
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct GameStory {
|
||||||
|
pub starting_village: Village,
|
||||||
|
pub changes: HashMap<GameTime, GameActions>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameStory {
|
||||||
|
pub fn new(starting_village: Village) -> Self {
|
||||||
|
Self {
|
||||||
|
starting_village,
|
||||||
|
changes: HashMap::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, time: GameTime, changes: GameActions) -> Result<()> {
|
||||||
|
if self.changes.contains_key(&time) {
|
||||||
|
return Err(GameError::ChangesAlreadySet(time));
|
||||||
|
}
|
||||||
|
self.changes.insert(time, changes);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn village_at(&self, at_time: GameTime) -> Result<Option<Village>> {
|
||||||
|
let mut village = self.starting_village.clone();
|
||||||
|
for (time, actions) in self.iter() {
|
||||||
|
village = match actions {
|
||||||
|
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
||||||
|
GameActions::NightDetails(night_details) => {
|
||||||
|
village.with_night_changes(&night_details.changes)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if time == at_time {
|
||||||
|
return Ok(Some(village));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn iter<'a>(&'a self) -> StoryIterator<'a> {
|
||||||
|
StoryIterator {
|
||||||
|
story: self,
|
||||||
|
time: self.starting_village.time(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct StoryIterator<'a> {
|
||||||
|
story: &'a GameStory,
|
||||||
|
time: GameTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Iterator for StoryIterator<'a> {
|
||||||
|
type Item = (GameTime, &'a GameActions);
|
||||||
|
|
||||||
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
|
match self.story.changes.get(&self.time) {
|
||||||
|
Some(changes) => {
|
||||||
|
let changes_time = self.time;
|
||||||
|
self.time = self.time.next();
|
||||||
|
Some((changes_time, changes))
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
|
mod apply;
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
use std::{rc::Rc, sync::Arc};
|
|
||||||
|
|
||||||
use rand::Rng;
|
use rand::Rng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
@ -9,16 +9,16 @@ use crate::{
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{DateTime, GameOver, GameSettings},
|
game::{GameOver, GameSettings, GameTime},
|
||||||
message::{CharacterIdentity, Identification, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, night::ActionPrompt},
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Village {
|
pub struct Village {
|
||||||
characters: Box<[Character]>,
|
characters: Box<[Character]>,
|
||||||
date_time: DateTime,
|
time: GameTime,
|
||||||
settings: GameSettings,
|
settings: GameSettings,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,7 +37,7 @@ impl Village {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
settings,
|
settings,
|
||||||
characters,
|
characters,
|
||||||
date_time: DateTime::Night { number: 0 },
|
time: GameTime::Night { number: 0 },
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,30 +46,18 @@ impl Village {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn killing_wolf(&self) -> Option<&Character> {
|
pub fn killing_wolf(&self) -> Option<&Character> {
|
||||||
let wolves = self.characters.iter().filter(|c| c.is_wolf());
|
let mut wolves = self
|
||||||
|
.characters
|
||||||
{
|
.iter()
|
||||||
let ww = wolves
|
.filter(|c| c.is_wolf())
|
||||||
.clone()
|
.collect::<Box<[_]>>();
|
||||||
.filter(|w| matches!(w.role_title(), RoleTitle::Werewolf))
|
wolves.sort_by_key(|w| w.killing_wolf_order());
|
||||||
.collect::<Box<[_]>>();
|
wolves.first().copied()
|
||||||
if !ww.is_empty() {
|
|
||||||
return Some(ww[rand::random_range(0..ww.len())]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
{
|
|
||||||
let wolves = wolves.collect::<Box<[_]>>();
|
|
||||||
if wolves.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
Some(wolves[rand::random_range(0..wolves.len())])
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
pub fn wolf_pack_kill(&self) -> Option<ActionPrompt> {
|
pub fn wolf_pack_kill(&self) -> Option<ActionPrompt> {
|
||||||
let night = match self.date_time {
|
let night = match self.time {
|
||||||
DateTime::Day { .. } => return None,
|
GameTime::Day { .. } => return None,
|
||||||
DateTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
let no_kill_due_to_disease = self
|
let no_kill_due_to_disease = self
|
||||||
.characters
|
.characters
|
||||||
|
|
@ -88,8 +76,8 @@ impl Village {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn date_time(&self) -> DateTime {
|
pub const fn time(&self) -> GameTime {
|
||||||
self.date_time
|
self.time
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_by_character_id_mut(
|
pub fn find_by_character_id_mut(
|
||||||
|
|
@ -137,9 +125,9 @@ impl Village {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn execute(&mut self, characters: &[CharacterId]) -> Result<Option<GameOver>> {
|
pub fn execute(&mut self, characters: &[CharacterId]) -> Result<Option<GameOver>> {
|
||||||
let day = match self.date_time {
|
let day = match self.time {
|
||||||
DateTime::Day { number } => number,
|
GameTime::Day { number } => number,
|
||||||
DateTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
|
GameTime::Night { number: _ } => return Err(GameError::NoExecutionsAtNight),
|
||||||
};
|
};
|
||||||
|
|
||||||
let targets = self
|
let targets = self
|
||||||
|
|
@ -151,16 +139,19 @@ impl Village {
|
||||||
t.execute(day)?;
|
t.execute(day)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.date_time = self.date_time.next();
|
if let Some(game_over) = self.is_game_over() {
|
||||||
Ok(self.is_game_over())
|
return Ok(Some(game_over));
|
||||||
|
}
|
||||||
|
self.time = self.time.next();
|
||||||
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_day(&mut self) -> Result<DateTime> {
|
pub fn to_day(&mut self) -> Result<GameTime> {
|
||||||
if self.date_time.is_day() {
|
if self.time.is_day() {
|
||||||
return Err(GameError::AlreadyDaytime);
|
return Err(GameError::AlreadyDaytime);
|
||||||
}
|
}
|
||||||
self.date_time = self.date_time.next();
|
self.time = self.time.next();
|
||||||
Ok(self.date_time)
|
Ok(self.time)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
pub fn living_wolf_pack_players(&self) -> Box<[Character]> {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::{
|
||||||
|
GameTime, Village, kill,
|
||||||
|
night::changes::{ChangesLookup, NightChange},
|
||||||
|
story::DayDetail,
|
||||||
|
},
|
||||||
|
player::Protection,
|
||||||
|
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
||||||
|
impl Village {
|
||||||
|
pub fn with_day_changes(&self, day_changes: &[DayDetail]) -> Result<Self> {
|
||||||
|
let mut new_village = self.clone();
|
||||||
|
let executions = day_changes
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
DayDetail::Execute(c) => *c,
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
|
||||||
|
new_village.execute(&executions)?;
|
||||||
|
|
||||||
|
Ok(new_village)
|
||||||
|
}
|
||||||
|
pub fn with_night_changes(&self, all_changes: &[NightChange]) -> Result<Self> {
|
||||||
|
let night = match self.time {
|
||||||
|
GameTime::Day { .. } => return Err(GameError::NotNight),
|
||||||
|
GameTime::Night { number } => number,
|
||||||
|
};
|
||||||
|
let mut changes = ChangesLookup::new(all_changes);
|
||||||
|
|
||||||
|
let mut new_village = self.clone();
|
||||||
|
for change in all_changes {
|
||||||
|
match change {
|
||||||
|
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)?
|
||||||
|
.role_change(*role_title, GameTime::Night { number: night })?,
|
||||||
|
NightChange::HunterTarget { source, target } => {
|
||||||
|
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
||||||
|
hunter_character.hunter_mut()?.replace(*target);
|
||||||
|
if changes.killed(*source).is_some()
|
||||||
|
&& changes.protected(source).is_none()
|
||||||
|
&& changes.protected(target).is_none()
|
||||||
|
{
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*target)
|
||||||
|
.unwrap()
|
||||||
|
.kill(DiedTo::Hunter {
|
||||||
|
killer: *source,
|
||||||
|
night: NonZeroU8::new(night).unwrap(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NightChange::Kill { target, died_to } => {
|
||||||
|
if let Some(kill) =
|
||||||
|
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
||||||
|
{
|
||||||
|
kill.apply_to_village(&mut new_village)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NightChange::Shapeshift { source, into } => {
|
||||||
|
if let Some(target) = changes.wolf_pack_kill_target()
|
||||||
|
&& changes.protected(target).is_none()
|
||||||
|
{
|
||||||
|
if *target != *into {
|
||||||
|
log::error!("shapeshift into({into}) != target({target})");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let ss = new_village.character_by_id_mut(*source).unwrap();
|
||||||
|
ss.shapeshifter_mut().unwrap().replace(*target);
|
||||||
|
ss.kill(DiedTo::Shapeshift {
|
||||||
|
into: *target,
|
||||||
|
night: NonZeroU8::new(night).unwrap(),
|
||||||
|
});
|
||||||
|
// role change pushed in [apply_shapeshift]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NightChange::Protection {
|
||||||
|
target,
|
||||||
|
protection: Protection::Guardian { source, guarding },
|
||||||
|
} => {
|
||||||
|
let target = new_village.character_by_id(*target)?.identity();
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.guardian_mut()?
|
||||||
|
.replace(if *guarding {
|
||||||
|
PreviousGuardianAction::Guard(target)
|
||||||
|
} else {
|
||||||
|
PreviousGuardianAction::Protect(target)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
NightChange::RoleBlock { .. } | NightChange::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 {
|
||||||
|
night,
|
||||||
|
tried_recruiting: *recruiting,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
} 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, GameTime::Night { number: night })?;
|
||||||
|
*new_village.character_by_id_mut(*empath)?.empath_mut()? = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// black knights death
|
||||||
|
for knight in new_village
|
||||||
|
.characters_mut()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|k| k.alive())
|
||||||
|
.filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some())
|
||||||
|
.filter(|k| changes.killed(k.character_id()).is_none())
|
||||||
|
{
|
||||||
|
knight.black_knight_kill()?.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
// pyre masters death
|
||||||
|
let village_dead = new_village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| c.is_village())
|
||||||
|
.filter_map(|c| c.died_to().cloned())
|
||||||
|
.filter_map(|c| c.killer().map(|k| (k, c)))
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
for pyremaster in new_village
|
||||||
|
.living_characters_by_role_mut(RoleTitle::PyreMaster)
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| {
|
||||||
|
village_dead
|
||||||
|
.iter()
|
||||||
|
.filter(|(k, _)| *k == p.character_id())
|
||||||
|
.count()
|
||||||
|
>= PYREMASTER_VILLAGER_KILLS_TO_DIE.get() as usize
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if let Some(night) = NonZeroU8::new(night) {
|
||||||
|
pyremaster.kill(DiedTo::PyreMasterLynchMob {
|
||||||
|
night,
|
||||||
|
source: pyremaster.character_id(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if new_village.is_game_over().is_none() {
|
||||||
|
new_village.to_day()?;
|
||||||
|
}
|
||||||
|
Ok(new_village)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,15 +4,16 @@ mod role;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
|
diedto::DiedToTitle,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameOver, GameSettings, SetupRole, SetupSlot},
|
game::{Game, GameOver, GameSettings, OrRandom, SetupRole, SetupSlot},
|
||||||
message::{
|
message::{
|
||||||
CharacterState, Identification, PublicIdentity,
|
CharacterState, Identification, PublicIdentity,
|
||||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||||
},
|
},
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
role::{Alignment, RoleTitle},
|
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||||
};
|
};
|
||||||
use colored::Colorize;
|
use colored::Colorize;
|
||||||
use core::{num::NonZeroU8, ops::Range};
|
use core::{num::NonZeroU8, ops::Range};
|
||||||
|
|
@ -51,7 +52,7 @@ pub trait ActionPromptTitleExt {
|
||||||
fn gravedigger(&self);
|
fn gravedigger(&self);
|
||||||
fn hunter(&self);
|
fn hunter(&self);
|
||||||
fn militia(&self);
|
fn militia(&self);
|
||||||
fn maplewolf(&self);
|
fn maple_wolf(&self);
|
||||||
fn guardian(&self);
|
fn guardian(&self);
|
||||||
fn shapeshifter(&self);
|
fn shapeshifter(&self);
|
||||||
fn alphawolf(&self);
|
fn alphawolf(&self);
|
||||||
|
|
@ -65,9 +66,14 @@ pub trait ActionPromptTitleExt {
|
||||||
fn adjudicator(&self);
|
fn adjudicator(&self);
|
||||||
fn lone_wolf(&self);
|
fn lone_wolf(&self);
|
||||||
fn insomniac(&self);
|
fn insomniac(&self);
|
||||||
|
fn power_seer(&self);
|
||||||
|
fn mortician(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPromptTitleExt for ActionPromptTitle {
|
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
|
fn mortician(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Mortician);
|
||||||
|
}
|
||||||
fn cover_of_darkness(&self) {
|
fn cover_of_darkness(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::CoverOfDarkness);
|
assert_eq!(*self, ActionPromptTitle::CoverOfDarkness);
|
||||||
}
|
}
|
||||||
|
|
@ -95,7 +101,7 @@ impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
fn militia(&self) {
|
fn militia(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::Militia);
|
assert_eq!(*self, ActionPromptTitle::Militia);
|
||||||
}
|
}
|
||||||
fn maplewolf(&self) {
|
fn maple_wolf(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::MapleWolf);
|
assert_eq!(*self, ActionPromptTitle::MapleWolf);
|
||||||
}
|
}
|
||||||
fn guardian(&self) {
|
fn guardian(&self) {
|
||||||
|
|
@ -131,6 +137,9 @@ impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
fn adjudicator(&self) {
|
fn adjudicator(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::Adjudicator)
|
assert_eq!(*self, ActionPromptTitle::Adjudicator)
|
||||||
}
|
}
|
||||||
|
fn power_seer(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::PowerSeer)
|
||||||
|
}
|
||||||
fn empath(&self) {
|
fn empath(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::Empath)
|
assert_eq!(*self, ActionPromptTitle::Empath)
|
||||||
}
|
}
|
||||||
|
|
@ -148,9 +157,48 @@ pub trait ActionResultExt {
|
||||||
fn seer(&self) -> Alignment;
|
fn seer(&self) -> Alignment;
|
||||||
fn insomniac(&self) -> Visits;
|
fn insomniac(&self) -> Visits;
|
||||||
fn arcanist(&self) -> bool;
|
fn arcanist(&self) -> bool;
|
||||||
|
fn role_blocked(&self);
|
||||||
|
fn gravedigger(&self) -> Option<RoleTitle>;
|
||||||
|
fn power_seer(&self) -> Powerful;
|
||||||
|
fn adjudicator(&self) -> Killer;
|
||||||
|
fn mortician(&self) -> DiedToTitle;
|
||||||
|
fn empath(&self) -> bool;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionResultExt for ActionResult {
|
impl ActionResultExt for ActionResult {
|
||||||
|
fn empath(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Empath { scapegoat } => *scapegoat,
|
||||||
|
resp => panic!("expected empath, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn mortician(&self) -> DiedToTitle {
|
||||||
|
match self {
|
||||||
|
Self::Mortician(role) => *role,
|
||||||
|
resp => panic!("expected mortician, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn gravedigger(&self) -> Option<RoleTitle> {
|
||||||
|
match self {
|
||||||
|
Self::GraveDigger(role) => *role,
|
||||||
|
resp => panic!("expected gravedigger, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn role_blocked(&self) {
|
||||||
|
assert_eq!(*self, ActionResult::RoleBlocked)
|
||||||
|
}
|
||||||
|
fn adjudicator(&self) -> Killer {
|
||||||
|
match self {
|
||||||
|
Self::Adjudicator { killer } => *killer,
|
||||||
|
resp => panic!("expected adjudicator, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn power_seer(&self) -> Powerful {
|
||||||
|
match self {
|
||||||
|
Self::PowerSeer { powerful } => *powerful,
|
||||||
|
resp => panic!("expected power seer, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
fn sleep(&self) {
|
fn sleep(&self) {
|
||||||
assert_eq!(*self, ActionResult::GoBackToSleep)
|
assert_eq!(*self, ActionResult::GoBackToSleep)
|
||||||
}
|
}
|
||||||
|
|
@ -168,7 +216,7 @@ impl ActionResultExt for ActionResult {
|
||||||
|
|
||||||
fn arcanist(&self) -> bool {
|
fn arcanist(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ActionResult::Arcanist { same } => *same,
|
ActionResult::Arcanist(same) => same.same(),
|
||||||
_ => panic!("expected an arcanist result"),
|
_ => panic!("expected an arcanist result"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -734,3 +782,400 @@ fn wolfpack_kill_all_targets_valid() {
|
||||||
assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter);
|
assert_eq!(attempt.next().title(), ActionPromptTitle::Shapeshifter);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn varied_test() {
|
||||||
|
init_log();
|
||||||
|
let players = (1..32u8)
|
||||||
|
.filter_map(NonZeroU8::new)
|
||||||
|
.map(|n| Identification {
|
||||||
|
player_id: PlayerId::from_u128(n.get() as _),
|
||||||
|
public: PublicIdentity {
|
||||||
|
name: format!("Player {n}"),
|
||||||
|
pronouns: Some("he/him".into()),
|
||||||
|
number: Some(n),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
let mut players_iter = players.iter().map(|p| p.player_id);
|
||||||
|
let (
|
||||||
|
werewolf,
|
||||||
|
dire_wolf,
|
||||||
|
shapeshifter,
|
||||||
|
alpha_wolf,
|
||||||
|
seer,
|
||||||
|
arcanist,
|
||||||
|
maple_wolf,
|
||||||
|
guardian,
|
||||||
|
vindicator,
|
||||||
|
adjudicator,
|
||||||
|
power_seer,
|
||||||
|
beholder,
|
||||||
|
gravedigger,
|
||||||
|
mortician,
|
||||||
|
insomniac,
|
||||||
|
empath,
|
||||||
|
scapegoat,
|
||||||
|
hunter,
|
||||||
|
) = (
|
||||||
|
(SetupRole::Werewolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::DireWolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Shapeshifter, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::AlphaWolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Seer, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Arcanist, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::MapleWolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Guardian, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Vindicator, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Adjudicator, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::PowerSeer, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Beholder, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Gravedigger, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Mortician, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Insomniac, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Empath, players_iter.next().unwrap()),
|
||||||
|
(
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(false),
|
||||||
|
},
|
||||||
|
players_iter.next().unwrap(),
|
||||||
|
),
|
||||||
|
(SetupRole::Hunter, players_iter.next().unwrap()),
|
||||||
|
);
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(werewolf.0, werewolf.1);
|
||||||
|
settings.add_and_assign(dire_wolf.0, dire_wolf.1);
|
||||||
|
settings.add_and_assign(shapeshifter.0, shapeshifter.1);
|
||||||
|
settings.add_and_assign(alpha_wolf.0, alpha_wolf.1);
|
||||||
|
settings.add_and_assign(seer.0, seer.1);
|
||||||
|
settings.add_and_assign(arcanist.0, arcanist.1);
|
||||||
|
settings.add_and_assign(maple_wolf.0, maple_wolf.1);
|
||||||
|
settings.add_and_assign(guardian.0, guardian.1);
|
||||||
|
settings.add_and_assign(vindicator.0, vindicator.1);
|
||||||
|
settings.add_and_assign(adjudicator.0, adjudicator.1);
|
||||||
|
settings.add_and_assign(power_seer.0, power_seer.1);
|
||||||
|
settings.add_and_assign(beholder.0, beholder.1);
|
||||||
|
settings.add_and_assign(gravedigger.0, gravedigger.1);
|
||||||
|
settings.add_and_assign(mortician.0, mortician.1);
|
||||||
|
settings.add_and_assign(insomniac.0, insomniac.1);
|
||||||
|
settings.add_and_assign(empath.0, empath.1);
|
||||||
|
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||||
|
settings.add_and_assign(hunter.0, hunter.1);
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
|
||||||
|
let (
|
||||||
|
werewolf,
|
||||||
|
dire_wolf,
|
||||||
|
shapeshifter,
|
||||||
|
alpha_wolf,
|
||||||
|
seer,
|
||||||
|
arcanist,
|
||||||
|
maple_wolf,
|
||||||
|
guardian,
|
||||||
|
vindicator,
|
||||||
|
adjudicator,
|
||||||
|
power_seer,
|
||||||
|
beholder,
|
||||||
|
gravedigger,
|
||||||
|
mortician,
|
||||||
|
insomniac,
|
||||||
|
empath,
|
||||||
|
scapegoat,
|
||||||
|
hunter,
|
||||||
|
) = (
|
||||||
|
werewolf.1,
|
||||||
|
dire_wolf.1,
|
||||||
|
shapeshifter.1,
|
||||||
|
alpha_wolf.1,
|
||||||
|
seer.1,
|
||||||
|
arcanist.1,
|
||||||
|
maple_wolf.1,
|
||||||
|
guardian.1,
|
||||||
|
vindicator.1,
|
||||||
|
adjudicator.1,
|
||||||
|
power_seer.1,
|
||||||
|
beholder.1,
|
||||||
|
gravedigger.1,
|
||||||
|
mortician.1,
|
||||||
|
insomniac.1,
|
||||||
|
empath.1,
|
||||||
|
scapegoat.1,
|
||||||
|
hunter.1,
|
||||||
|
);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().wolves_intro();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().role_blocked();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(arcanist).character_id());
|
||||||
|
game.r#continue().role_blocked();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
game.mark_for_execution(game.character_by_player_id(alpha_wolf).character_id());
|
||||||
|
|
||||||
|
game.execute().title().guardian();
|
||||||
|
let protect = game.living_villager();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.response(ActionResponse::Shapeshift).sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
|
||||||
|
|
||||||
|
game.next().title().empath();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
assert!(!game.r#continue().empath());
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protect.player_id()).died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark_for_execution(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.execute().title().guardian();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().vindicator();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
|
||||||
|
|
||||||
|
game.next().title().empath();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
assert!(game.r#continue().empath());
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.execute().title().vindicator();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.response(ActionResponse::Shapeshift).r#continue();
|
||||||
|
|
||||||
|
game.next().title().role_change();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(guardian).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(guardian).character_id());
|
||||||
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack);
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(gravedigger).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(game.character_by_player_id(vindicator).character_id());
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), None);
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
assert_eq!(
|
||||||
|
game.r#continue().mortician(),
|
||||||
|
DiedToTitle::GuardianProtecting
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(game.character_by_player_id(hunter).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(mortician).character_id());
|
||||||
|
assert_eq!(
|
||||||
|
game.r#continue().mortician(),
|
||||||
|
DiedToTitle::GuardianProtecting
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next_expect_game_over();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,12 +6,14 @@ use crate::{
|
||||||
game::{Game, GameSettings, SetupRole},
|
game::{Game, GameSettings, SetupRole},
|
||||||
game_test::{
|
game_test::{
|
||||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
||||||
|
init_log,
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn beholding_seer() {
|
fn beholding_seer() {
|
||||||
|
init_log();
|
||||||
let players = gen_players(1..10);
|
let players = gen_players(1..10);
|
||||||
let seer_player_id = players[0].player_id;
|
let seer_player_id = players[0].player_id;
|
||||||
let wolf_player_id = players[1].player_id;
|
let wolf_player_id = players[1].player_id;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,203 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::{
|
||||||
|
host::{HostGameMessage, HostNightMessage},
|
||||||
|
night::{ActionPromptTitle, ActionResponse},
|
||||||
|
},
|
||||||
|
role::{PreviousGuardianAction, Role},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn guard_kills_guardian_and_attacker() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let guardian = players[0].player_id;
|
||||||
|
let wolf = players[1].player_id;
|
||||||
|
let wolf2 = players[2].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf2);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
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();
|
||||||
|
let protect = game.living_villager();
|
||||||
|
game.execute().title().guardian();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protect.player_id()).died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
match game.character_by_player_id(guardian).role() {
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: Some(last_protected),
|
||||||
|
} => assert_eq!(
|
||||||
|
*last_protected,
|
||||||
|
PreviousGuardianAction::Protect(protect.identity())
|
||||||
|
),
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: None,
|
||||||
|
} => panic!("did not record protect"),
|
||||||
|
_ => panic!("not a guardian?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
game.execute().title().guardian();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
let killing_wolf_pid = game.village().killing_wolf().unwrap().player_id();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
match game.character_by_player_id(guardian).role() {
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: Some(last_protected),
|
||||||
|
} => assert_eq!(
|
||||||
|
*last_protected,
|
||||||
|
PreviousGuardianAction::Guard(protect.identity())
|
||||||
|
),
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: None,
|
||||||
|
} => panic!("did not record protect"),
|
||||||
|
_ => panic!("not a guardian?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(guardian).died_to().cloned(),
|
||||||
|
Some(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(killing_wolf_pid).character_id(),
|
||||||
|
night: NonZeroU8::new(2).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(killing_wolf_pid)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
Some(DiedTo::GuardianProtecting {
|
||||||
|
source: game.character_by_player_id(guardian).character_id(),
|
||||||
|
protecting: protect.character_id(),
|
||||||
|
protecting_from: game.character_by_player_id(killing_wolf_pid).character_id(),
|
||||||
|
protecting_from_cause: Box::new(DiedTo::Wolfpack {
|
||||||
|
killing_wolf: game.character_by_player_id(killing_wolf_pid).character_id(),
|
||||||
|
night: NonZeroU8::new(2).unwrap()
|
||||||
|
}),
|
||||||
|
night: NonZeroU8::new(2).unwrap(),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_visit_previous_nights_guard_target() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let guardian = players[0].player_id;
|
||||||
|
let wolf = players[1].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||||
|
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();
|
||||||
|
let protect = game.living_villager();
|
||||||
|
game.execute().title().guardian();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protect.player_id()).died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
match game.character_by_player_id(guardian).role() {
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: Some(last_protected),
|
||||||
|
} => assert_eq!(
|
||||||
|
*last_protected,
|
||||||
|
PreviousGuardianAction::Protect(protect.identity())
|
||||||
|
),
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: None,
|
||||||
|
} => panic!("did not record protect"),
|
||||||
|
_ => panic!("not a guardian?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
game.execute().title().guardian();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
let killing_wolf_pid = game.village().killing_wolf().unwrap().player_id();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
match game.character_by_player_id(guardian).role() {
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: Some(last_protected),
|
||||||
|
} => assert_eq!(
|
||||||
|
*last_protected,
|
||||||
|
PreviousGuardianAction::Guard(protect.identity())
|
||||||
|
),
|
||||||
|
Role::Guardian {
|
||||||
|
last_protected: None,
|
||||||
|
} => panic!("did not record protect"),
|
||||||
|
_ => panic!("not a guardian?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(guardian).died_to().cloned(),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(killing_wolf_pid)
|
||||||
|
.died_to()
|
||||||
|
.cloned(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().guardian();
|
||||||
|
assert_eq!(
|
||||||
|
game.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::MarkTarget(protect.character_id())
|
||||||
|
))),
|
||||||
|
Err(GameError::InvalidTarget)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,77 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::{
|
||||||
|
host::{HostGameMessage, HostNightMessage},
|
||||||
|
night::{ActionPromptTitle, ActionResponse},
|
||||||
|
},
|
||||||
|
role::{PreviousGuardianAction, Role},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_change_targets() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..10);
|
||||||
|
let hunter = players[0].player_id;
|
||||||
|
let wolf = players[1].player_id;
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Hunter, hunter);
|
||||||
|
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();
|
||||||
|
let hunt_target = game.living_villager();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(hunt_target.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(hunt_target.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
match game.character_by_player_id(hunter).role() {
|
||||||
|
Role::Hunter {
|
||||||
|
target: Some(target),
|
||||||
|
} => assert_eq!(*target, hunt_target.character_id()),
|
||||||
|
Role::Hunter { target: None } => panic!("did not record target"),
|
||||||
|
_ => panic!("not a guardian?"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let hunt_target = game.living_villager_excl(hunt_target.player_id());
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(hunt_target.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(hunt_target.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
match game.character_by_player_id(hunter).role() {
|
||||||
|
Role::Hunter {
|
||||||
|
target: Some(target),
|
||||||
|
} => assert_eq!(*target, hunt_target.character_id()),
|
||||||
|
Role::Hunter { target: None } => panic!("did not record target"),
|
||||||
|
_ => panic!("not a guardian?"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ mod black_knight;
|
||||||
mod diseased;
|
mod diseased;
|
||||||
mod elder;
|
mod elder;
|
||||||
mod empath;
|
mod empath;
|
||||||
|
mod guardian;
|
||||||
|
mod hunter;
|
||||||
mod insomniac;
|
mod insomniac;
|
||||||
mod lone_wolf;
|
mod lone_wolf;
|
||||||
mod mason;
|
mod mason;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use core::num::NonZero;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole, night::NightChange},
|
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
message::night::{ActionPrompt, ActionPromptTitle},
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
player::RoleChange,
|
player::RoleChange,
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameOver, GameSettings},
|
game::{GameOver, GameSettings, story::GameStory},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity,
|
CharacterIdentity,
|
||||||
night::{ActionPrompt, ActionResponse, ActionResult},
|
night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
|
|
@ -21,10 +21,17 @@ pub enum HostMessage {
|
||||||
Lobby(HostLobbyMessage),
|
Lobby(HostLobbyMessage),
|
||||||
InGame(HostGameMessage),
|
InGame(HostGameMessage),
|
||||||
ForceRoleAckFor(CharacterId),
|
ForceRoleAckFor(CharacterId),
|
||||||
NewLobby,
|
PostGame(PostGameMessage),
|
||||||
Echo(ServerToHostMessage),
|
Echo(ServerToHostMessage),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub enum PostGameMessage {
|
||||||
|
NewLobby,
|
||||||
|
NextPage,
|
||||||
|
PreviousPage,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum HostGameMessage {
|
pub enum HostGameMessage {
|
||||||
Day(HostDayMessage),
|
Day(HostDayMessage),
|
||||||
|
|
@ -84,4 +91,8 @@ pub enum ServerToHostMessage {
|
||||||
ackd: Box<[CharacterIdentity]>,
|
ackd: Box<[CharacterIdentity]>,
|
||||||
waiting: Box<[CharacterIdentity]>,
|
waiting: Box<[CharacterIdentity]>,
|
||||||
},
|
},
|
||||||
|
Story {
|
||||||
|
story: GameStory,
|
||||||
|
page: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,14 +24,19 @@ pub struct CharacterIdentity {
|
||||||
pub pronouns: Option<String>,
|
pub pronouns: Option<String>,
|
||||||
pub number: NonZeroU8,
|
pub number: NonZeroU8,
|
||||||
}
|
}
|
||||||
|
impl CharacterIdentity {
|
||||||
|
pub fn into_public(self) -> PublicIdentity {
|
||||||
|
PublicIdentity {
|
||||||
|
name: self.name,
|
||||||
|
pronouns: self.pronouns,
|
||||||
|
number: Some(self.number),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl From<CharacterIdentity> for PublicIdentity {
|
impl From<CharacterIdentity> for PublicIdentity {
|
||||||
fn from(c: CharacterIdentity) -> Self {
|
fn from(c: CharacterIdentity) -> Self {
|
||||||
Self {
|
c.into_public()
|
||||||
name: c.name,
|
|
||||||
pronouns: c.pronouns,
|
|
||||||
number: Some(c.number),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use crate::{
|
||||||
diedto::DiedToTitle,
|
diedto::DiedToTitle,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
message::CharacterIdentity,
|
message::CharacterIdentity,
|
||||||
role::{Alignment, PreviousGuardianAction, RoleTitle},
|
role::{Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
@ -435,9 +435,9 @@ pub enum ActionResponse {
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
RoleBlocked,
|
RoleBlocked,
|
||||||
Seer(Alignment),
|
Seer(Alignment),
|
||||||
PowerSeer { powerful: bool },
|
PowerSeer { powerful: Powerful },
|
||||||
Adjudicator { killer: bool },
|
Adjudicator { killer: Killer },
|
||||||
Arcanist { same: bool },
|
Arcanist(AlignmentEq),
|
||||||
GraveDigger(Option<RoleTitle>),
|
GraveDigger(Option<RoleTitle>),
|
||||||
Mortician(DiedToTitle),
|
Mortician(DiedToTitle),
|
||||||
Insomniac(Visits),
|
Insomniac(Visits),
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,8 @@
|
||||||
use core::num::NonZeroU8;
|
use core::{
|
||||||
|
fmt::Display,
|
||||||
|
num::NonZeroU8,
|
||||||
|
ops::{Deref, Not},
|
||||||
|
};
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::{ChecksAs, Titles};
|
use werewolves_macros::{ChecksAs, Titles};
|
||||||
|
|
@ -6,107 +10,219 @@ use werewolves_macros::{ChecksAs, Titles};
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{DateTime, Village},
|
game::{GameTime, Village},
|
||||||
message::CharacterIdentity,
|
message::CharacterIdentity,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)]
|
||||||
|
pub enum Killer {
|
||||||
|
Killer,
|
||||||
|
#[default]
|
||||||
|
NotKiller,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Killer {
|
||||||
|
pub const fn killer(&self) -> bool {
|
||||||
|
matches!(self, Killer::Killer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Killer {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Killer::Killer => f.write_str("Killer"),
|
||||||
|
Killer::NotKiller => f.write_str("Not a Killer"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Not for Killer {
|
||||||
|
type Output = Killer;
|
||||||
|
|
||||||
|
fn not(self) -> Self::Output {
|
||||||
|
match self {
|
||||||
|
Killer::Killer => Killer::NotKiller,
|
||||||
|
Killer::NotKiller => Killer::Killer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)]
|
||||||
|
pub enum Powerful {
|
||||||
|
Powerful,
|
||||||
|
#[default]
|
||||||
|
NotPowerful,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Powerful {
|
||||||
|
pub const fn powerful(&self) -> bool {
|
||||||
|
matches!(self, Powerful::Powerful)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Powerful {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Powerful::Powerful => f.write_str("Powerful"),
|
||||||
|
Powerful::NotPowerful => f.write_str("Not Powerful"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Not for Powerful {
|
||||||
|
type Output = Powerful;
|
||||||
|
|
||||||
|
fn not(self) -> Self::Output {
|
||||||
|
match self {
|
||||||
|
Powerful::Powerful => Powerful::NotPowerful,
|
||||||
|
Powerful::NotPowerful => Powerful::Powerful,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)]
|
||||||
|
pub enum AlignmentEq {
|
||||||
|
Same,
|
||||||
|
Different,
|
||||||
|
}
|
||||||
|
impl Not for AlignmentEq {
|
||||||
|
type Output = AlignmentEq;
|
||||||
|
|
||||||
|
fn not(self) -> Self::Output {
|
||||||
|
match self {
|
||||||
|
AlignmentEq::Same => Self::Different,
|
||||||
|
AlignmentEq::Different => Self::Same,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl AlignmentEq {
|
||||||
|
pub const fn new(same: bool) -> Self {
|
||||||
|
match same {
|
||||||
|
true => Self::Same,
|
||||||
|
false => Self::Different,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub const fn same(&self) -> bool {
|
||||||
|
matches!(self, Self::Same)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, ChecksAs, Titles)]
|
||||||
pub enum Role {
|
pub enum Role {
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
|
#[checks(Powerful::NotPowerful)]
|
||||||
Villager,
|
Villager,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
Scapegoat { redeemed: bool },
|
Scapegoat { redeemed: bool },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Seer,
|
Seer,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Arcanist,
|
Arcanist,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Adjudicator,
|
Adjudicator,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
PowerSeer,
|
PowerSeer,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Mortician,
|
Mortician,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Beholder,
|
Beholder,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
MasonLeader {
|
MasonLeader {
|
||||||
recruits_available: u8,
|
recruits_available: u8,
|
||||||
recruits: Box<[CharacterId]>,
|
recruits: Box<[CharacterId]>,
|
||||||
},
|
},
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
Empath { cursed: bool },
|
Empath { cursed: bool },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Vindicator,
|
Vindicator,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Diseased,
|
Diseased,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
BlackKnight { attacked: Option<DiedTo> },
|
BlackKnight { attacked: Option<DiedTo> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Weightlifter,
|
Weightlifter,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::Killer)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
PyreMaster { villagers_killed: u8 },
|
PyreMaster { villagers_killed: u8 },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Gravedigger,
|
Gravedigger,
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
#[checks]
|
#[checks]
|
||||||
Hunter { target: Option<CharacterId> },
|
Hunter { target: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Militia { targeted: Option<CharacterId> },
|
Militia { targeted: Option<CharacterId> },
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
MapleWolf { last_kill_on_night: u8 },
|
MapleWolf { last_kill_on_night: u8 },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Guardian {
|
Guardian {
|
||||||
last_protected: Option<PreviousGuardianAction>,
|
last_protected: Option<PreviousGuardianAction>,
|
||||||
},
|
},
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Protector { last_protected: Option<CharacterId> },
|
Protector { last_protected: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
Apprentice(RoleTitle),
|
Apprentice(RoleTitle),
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
#[checks("is_mentor")]
|
#[checks("is_mentor")]
|
||||||
Elder {
|
Elder {
|
||||||
knows_on_night: NonZeroU8,
|
knows_on_night: NonZeroU8,
|
||||||
|
|
@ -114,32 +230,33 @@ pub enum Role {
|
||||||
lost_protection_night: Option<NonZeroU8>,
|
lost_protection_night: Option<NonZeroU8>,
|
||||||
},
|
},
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks(Killer::NotKiller)]
|
||||||
Insomniac,
|
Insomniac,
|
||||||
|
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
Werewolf,
|
Werewolf,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
AlphaWolf { killed: Option<CharacterId> },
|
AlphaWolf { killed: Option<CharacterId> },
|
||||||
#[checks(Alignment::Village)]
|
#[checks(Alignment::Village)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
DireWolf,
|
DireWolf,
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
Shapeshifter { shifted_into: Option<CharacterId> },
|
Shapeshifter { shifted_into: Option<CharacterId> },
|
||||||
#[checks(Alignment::Wolves)]
|
#[checks(Alignment::Wolves)]
|
||||||
#[checks("killer")]
|
#[checks(Killer::Killer)]
|
||||||
#[checks("powerful")]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
LoneWolf,
|
LoneWolf,
|
||||||
}
|
}
|
||||||
|
|
@ -153,6 +270,41 @@ impl Role {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn killing_wolf_order(&self) -> Option<KillingWolfOrder> {
|
||||||
|
Some(match self {
|
||||||
|
Role::Villager
|
||||||
|
| Role::Scapegoat { .. }
|
||||||
|
| Role::Seer
|
||||||
|
| Role::Arcanist
|
||||||
|
| Role::Adjudicator
|
||||||
|
| Role::PowerSeer
|
||||||
|
| Role::Mortician
|
||||||
|
| Role::Beholder
|
||||||
|
| Role::MasonLeader { .. }
|
||||||
|
| Role::Empath { .. }
|
||||||
|
| Role::Vindicator
|
||||||
|
| Role::Diseased
|
||||||
|
| Role::BlackKnight { .. }
|
||||||
|
| Role::Weightlifter
|
||||||
|
| Role::PyreMaster { .. }
|
||||||
|
| Role::Gravedigger
|
||||||
|
| Role::Hunter { .. }
|
||||||
|
| Role::Militia { .. }
|
||||||
|
| Role::MapleWolf { .. }
|
||||||
|
| Role::Guardian { .. }
|
||||||
|
| Role::Protector { .. }
|
||||||
|
| Role::Apprentice(..)
|
||||||
|
| Role::Elder { .. }
|
||||||
|
| Role::Insomniac => return None,
|
||||||
|
|
||||||
|
Role::Werewolf => KillingWolfOrder::Werewolf,
|
||||||
|
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
||||||
|
Role::DireWolf => KillingWolfOrder::DireWolf,
|
||||||
|
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
||||||
|
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn wakes_night_zero(&self) -> bool {
|
pub const fn wakes_night_zero(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Role::Insomniac
|
Role::Insomniac
|
||||||
|
|
@ -189,9 +341,9 @@ impl Role {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn wakes(&self, village: &Village) -> bool {
|
pub fn wakes(&self, village: &Village) -> bool {
|
||||||
let night_zero = match village.date_time() {
|
let night_zero = match village.time() {
|
||||||
DateTime::Day { number: _ } => return false,
|
GameTime::Day { number: _ } => return false,
|
||||||
DateTime::Night { number } => number == 0,
|
GameTime::Night { number } => number == 0,
|
||||||
};
|
};
|
||||||
if night_zero {
|
if night_zero {
|
||||||
return self.wakes_night_zero();
|
return self.wakes_night_zero();
|
||||||
|
|
@ -205,9 +357,9 @@ impl Role {
|
||||||
| Role::BlackKnight { .. }
|
| Role::BlackKnight { .. }
|
||||||
| Role::Villager => false,
|
| Role::Villager => false,
|
||||||
|
|
||||||
Role::LoneWolf => match village.date_time() {
|
Role::LoneWolf => match village.time() {
|
||||||
DateTime::Day { number: _ } => return false,
|
GameTime::Day { number: _ } => return false,
|
||||||
DateTime::Night { number } => NonZeroU8::new(number),
|
GameTime::Night { number } => NonZeroU8::new(number),
|
||||||
}
|
}
|
||||||
.map(|night| village.executions_on_day(night))
|
.map(|night| village.executions_on_day(night))
|
||||||
.map(|execs| execs.iter().any(|e| e.is_wolf()))
|
.map(|execs| execs.iter().any(|e| e.is_wolf()))
|
||||||
|
|
@ -247,8 +399,8 @@ impl Role {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
!woken_for_reveal
|
!woken_for_reveal
|
||||||
&& match village.date_time() {
|
&& match village.time() {
|
||||||
DateTime::Night { number } => number == knows_on_night.get(),
|
GameTime::Night { number } => number == knows_on_night.get(),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -256,12 +408,21 @@ impl Role {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
|
||||||
pub enum Alignment {
|
pub enum Alignment {
|
||||||
Village,
|
Village,
|
||||||
Wolves,
|
Wolves,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Display for Alignment {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Alignment::Village => f.write_str("Village"),
|
||||||
|
Alignment::Wolves => f.write_str("Wolves"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs)]
|
#[derive(Debug, Clone, Serialize, Deserialize, ChecksAs)]
|
||||||
pub enum ArcanistCheck {
|
pub enum ArcanistCheck {
|
||||||
#[checks]
|
#[checks]
|
||||||
|
|
@ -283,3 +444,16 @@ pub enum PreviousGuardianAction {
|
||||||
Protect(CharacterIdentity),
|
Protect(CharacterIdentity),
|
||||||
Guard(CharacterIdentity),
|
Guard(CharacterIdentity),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
|
||||||
|
pub enum KillingWolfOrder {
|
||||||
|
Werewolf,
|
||||||
|
AlphaWolf,
|
||||||
|
Shapeshifter,
|
||||||
|
Berserker,
|
||||||
|
Psion,
|
||||||
|
Bloodletter,
|
||||||
|
Bloodhound,
|
||||||
|
DireWolf,
|
||||||
|
LoneWolf,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,8 @@ thiserror = { version = "2" }
|
||||||
ciborium = { version = "0.2", optional = true }
|
ciborium = { version = "0.2", optional = true }
|
||||||
colored = { version = "3.0" }
|
colored = { version = "3.0" }
|
||||||
fast_qr = { version = "0.13", features = ["svg"] }
|
fast_qr = { version = "0.13", features = ["svg"] }
|
||||||
|
ron = "0.8"
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["cbor"]
|
default = ["cbor"]
|
||||||
|
|
|
||||||
|
|
@ -5,22 +5,12 @@ use werewolves_proto::error::GameError;
|
||||||
use crate::runner::IdentifiedClientMessage;
|
use crate::runner::IdentifiedClientMessage;
|
||||||
|
|
||||||
pub struct PlayerIdComms {
|
pub struct PlayerIdComms {
|
||||||
// joined_players: JoinedPlayers,
|
|
||||||
message_recv: Receiver<IdentifiedClientMessage>,
|
message_recv: Receiver<IdentifiedClientMessage>,
|
||||||
// connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayerIdComms {
|
impl PlayerIdComms {
|
||||||
pub fn new(
|
pub fn new(message_recv: Receiver<IdentifiedClientMessage>) -> Self {
|
||||||
// joined_players: JoinedPlayers,
|
Self { message_recv }
|
||||||
message_recv: Receiver<IdentifiedClientMessage>,
|
|
||||||
// connect_recv: tokio::sync::broadcast::Receiver<(PlayerId, bool)>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
// joined_players,
|
|
||||||
message_recv,
|
|
||||||
// connect_recv,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn recv(&mut self) -> Result<IdentifiedClientMessage, GameError> {
|
pub async fn recv(&mut self) -> Result<IdentifiedClientMessage, GameError> {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ use werewolves_proto::{
|
||||||
game::{Game, GameOver, Village},
|
game::{Game, GameOver, Village},
|
||||||
message::{
|
message::{
|
||||||
ClientMessage, Identification, ServerMessage,
|
ClientMessage, Identification, ServerMessage,
|
||||||
host::{HostGameMessage, HostMessage, ServerToHostMessage},
|
host::{HostGameMessage, HostMessage, PostGameMessage, ServerToHostMessage},
|
||||||
},
|
},
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
|
|
@ -58,6 +58,7 @@ impl GameRunner {
|
||||||
self.joined_players,
|
self.joined_players,
|
||||||
LobbyComms::new(self.comms, self.connect_recv),
|
LobbyComms::new(self.comms, self.connect_recv),
|
||||||
);
|
);
|
||||||
|
lobby.set_settings(self.game.village().settings());
|
||||||
lobby.set_players_in_lobby(self.player_sender);
|
lobby.set_players_in_lobby(self.player_sender);
|
||||||
lobby
|
lobby
|
||||||
}
|
}
|
||||||
|
|
@ -268,7 +269,7 @@ impl GameRunner {
|
||||||
match message {
|
match message {
|
||||||
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
||||||
HostMessage::InGame(msg) => self.game.process(msg),
|
HostMessage::InGame(msg) => self.game.process(msg),
|
||||||
HostMessage::Lobby(_) | HostMessage::NewLobby | HostMessage::ForceRoleAckFor(_) => {
|
HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => {
|
||||||
Err(GameError::InvalidMessageForGameState)
|
Err(GameError::InvalidMessageForGameState)
|
||||||
}
|
}
|
||||||
HostMessage::Echo(echo) => Ok(echo),
|
HostMessage::Echo(echo) => Ok(echo),
|
||||||
|
|
@ -278,6 +279,7 @@ impl GameRunner {
|
||||||
|
|
||||||
pub struct GameEnd {
|
pub struct GameEnd {
|
||||||
game: Option<GameRunner>,
|
game: Option<GameRunner>,
|
||||||
|
page: Option<usize>,
|
||||||
result: GameOver,
|
result: GameOver,
|
||||||
last_error_log: Instant,
|
last_error_log: Instant,
|
||||||
}
|
}
|
||||||
|
|
@ -286,6 +288,7 @@ impl GameEnd {
|
||||||
pub fn new(game: GameRunner, result: GameOver) -> Self {
|
pub fn new(game: GameRunner, result: GameOver) -> Self {
|
||||||
Self {
|
Self {
|
||||||
result,
|
result,
|
||||||
|
page: None,
|
||||||
game: Some(game),
|
game: Some(game),
|
||||||
last_error_log: Instant::now() - core::time::Duration::from_secs(60),
|
last_error_log: Instant::now() - core::time::Duration::from_secs(60),
|
||||||
}
|
}
|
||||||
|
|
@ -310,32 +313,65 @@ impl GameEnd {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
match msg {
|
self.process(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn process(&mut self, message: Message) -> Option<Lobby> {
|
||||||
|
match message {
|
||||||
Message::Host(HostMessage::Echo(msg)) => {
|
Message::Host(HostMessage::Echo(msg)) => {
|
||||||
self.game().unwrap().comms.host().send(msg).log_debug();
|
self.game().unwrap().comms.host().send(msg).log_debug();
|
||||||
}
|
}
|
||||||
Message::Host(HostMessage::GetState) => {
|
Message::Host(HostMessage::GetState) => {
|
||||||
let result = self.result;
|
if let Some(page) = self.page {
|
||||||
self.game()
|
let story = self.game().ok()?.game.story();
|
||||||
.unwrap()
|
self.game()
|
||||||
.comms
|
.ok()?
|
||||||
.host()
|
.comms
|
||||||
.send(ServerToHostMessage::GameOver(result))
|
.host()
|
||||||
.log_debug()
|
.send(ServerToHostMessage::Story { story, page })
|
||||||
|
.log_debug();
|
||||||
|
} else {
|
||||||
|
let result = self.result;
|
||||||
|
self.game()
|
||||||
|
.ok()?
|
||||||
|
.comms
|
||||||
|
.host()
|
||||||
|
.send(ServerToHostMessage::GameOver(result))
|
||||||
|
.log_debug();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Message::Host(HostMessage::NewLobby) => {
|
Message::Host(HostMessage::PostGame(PostGameMessage::PreviousPage)) => {
|
||||||
|
if let Some(page) = self.page.as_mut() {
|
||||||
|
if *page == 0 {
|
||||||
|
self.page = None;
|
||||||
|
} else {
|
||||||
|
*page -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return self.process(Message::Host(HostMessage::GetState));
|
||||||
|
}
|
||||||
|
Message::Host(HostMessage::PostGame(PostGameMessage::NextPage)) => {
|
||||||
|
if let Some(page) = self.page.as_mut() {
|
||||||
|
*page += 1;
|
||||||
|
} else {
|
||||||
|
self.page = Some(0);
|
||||||
|
}
|
||||||
|
return self.process(Message::Host(HostMessage::GetState));
|
||||||
|
}
|
||||||
|
Message::Host(HostMessage::PostGame(PostGameMessage::NewLobby)) => {
|
||||||
self.game()
|
self.game()
|
||||||
.unwrap()
|
.ok()?
|
||||||
.comms
|
.comms
|
||||||
.host()
|
.host()
|
||||||
.send(ServerToHostMessage::Lobby(Box::new([])))
|
.send(ServerToHostMessage::Lobby(Box::new([])))
|
||||||
.log_debug();
|
.log_debug();
|
||||||
let lobby = self.game.take().unwrap().into_lobby();
|
let lobby = self.game.take()?.into_lobby();
|
||||||
return Some(lobby);
|
return Some(lobby);
|
||||||
}
|
}
|
||||||
|
|
||||||
Message::Host(_) => self
|
Message::Host(_) => self
|
||||||
.game()
|
.game()
|
||||||
.unwrap()
|
.ok()?
|
||||||
.comms
|
.comms
|
||||||
.host()
|
.host()
|
||||||
.send(ServerToHostMessage::Error(
|
.send(ServerToHostMessage::Error(
|
||||||
|
|
@ -348,7 +384,7 @@ impl GameEnd {
|
||||||
}) => {
|
}) => {
|
||||||
let result = self.result;
|
let result = self.result;
|
||||||
self.game()
|
self.game()
|
||||||
.unwrap()
|
.ok()?
|
||||||
.player_sender
|
.player_sender
|
||||||
.send_if_present(identity.player_id, ServerMessage::GameOver(result))
|
.send_if_present(identity.player_id, ServerMessage::GameOver(result))
|
||||||
.log_debug();
|
.log_debug();
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use core::{
|
||||||
ops::{Deref, DerefMut},
|
ops::{Deref, DerefMut},
|
||||||
};
|
};
|
||||||
|
|
||||||
use tokio::sync::broadcast::{self, Sender};
|
use tokio::sync::broadcast::Sender;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{Game, GameSettings},
|
game::{Game, GameSettings},
|
||||||
|
|
@ -41,6 +41,16 @@ impl Lobby {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_settings(&mut self, settings: GameSettings) {
|
||||||
|
self.settings = settings.clone();
|
||||||
|
if let Ok(comms) = self.comms() {
|
||||||
|
comms
|
||||||
|
.host()
|
||||||
|
.send(ServerToHostMessage::GameSettings(settings))
|
||||||
|
.log_debug();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) {
|
pub fn set_players_in_lobby(&mut self, players_in_lobby: LobbyPlayers) {
|
||||||
self.players_in_lobby = players_in_lobby
|
self.players_in_lobby = players_in_lobby
|
||||||
}
|
}
|
||||||
|
|
@ -175,7 +185,7 @@ impl Lobby {
|
||||||
| Message::Host(HostMessage::ForceRoleAckFor(_)) => {
|
| Message::Host(HostMessage::ForceRoleAckFor(_)) => {
|
||||||
return Err(GameError::InvalidMessageForGameState);
|
return Err(GameError::InvalidMessageForGameState);
|
||||||
}
|
}
|
||||||
Message::Host(HostMessage::NewLobby) => self
|
Message::Host(HostMessage::PostGame(_)) => self
|
||||||
.comms()
|
.comms()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.host()
|
.host()
|
||||||
|
|
|
||||||
|
|
@ -14,8 +14,8 @@ pub trait Saver: Clone + Send + 'static {
|
||||||
pub enum FileSaverError {
|
pub enum FileSaverError {
|
||||||
#[error("io error: {0}")]
|
#[error("io error: {0}")]
|
||||||
IoError(std::io::Error),
|
IoError(std::io::Error),
|
||||||
#[error("serialization error")]
|
#[error("serialization error: {0}")]
|
||||||
SerializationError(#[from] serde_json::Error),
|
SerializationError(#[from] ron::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::io::Error> for FileSaverError {
|
impl From<std::io::Error> for FileSaverError {
|
||||||
|
|
@ -39,10 +39,11 @@ impl Saver for FileSaver {
|
||||||
type Error = FileSaverError;
|
type Error = FileSaverError;
|
||||||
|
|
||||||
fn save(&mut self, game: &Game) -> Result<String, Self::Error> {
|
fn save(&mut self, game: &Game) -> Result<String, Self::Error> {
|
||||||
let name = format!("werewolves_{}.json", chrono::Utc::now().timestamp());
|
let name = format!("werewolves_{}.ron", chrono::Utc::now().timestamp());
|
||||||
let path = self.path.join(name.clone());
|
let path = self.path.join(name.clone());
|
||||||
let mut file = std::fs::File::create_new(path.clone())?;
|
let mut file = std::fs::File::create_new(path.clone())?;
|
||||||
serde_json::to_writer_pretty(&mut file, &game)?;
|
// serde_json::to_writer_pretty(&mut file, &game)?;
|
||||||
|
ron::ser::to_writer_pretty(&mut file, &game.story(), ron::ser::PrettyConfig::new())?;
|
||||||
file.flush()?;
|
file.flush()?;
|
||||||
Ok(path.to_str().map(|s| s.to_string()).unwrap_or(name))
|
Ok(path.to_str().map(|s| s.to_string()).unwrap_or(name))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,5 +9,6 @@ inject_scripts = true # Whether to inject scripts (and module preloads) in
|
||||||
offline = false # Run without network access
|
offline = false # Run without network access
|
||||||
frozen = false # Require Cargo.lock and cache are up to date
|
frozen = false # Require Cargo.lock and cache are up to date
|
||||||
locked = false # Require Cargo.lock is up to date
|
locked = false # Require Cargo.lock is up to date
|
||||||
|
# minify = "on_release" # Control minification: can be one of: never, on_release, always
|
||||||
minify = "always" # Control minification: can be one of: never, on_release, always
|
minify = "always" # Control minification: can be one of: never, on_release, always
|
||||||
no_sri = false # Allow disabling sub-resource integrity (SRI)
|
no_sri = false # Allow disabling sub-resource integrity (SRI)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,53 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="14.519332mm"
|
||||||
|
height="14.518607mm"
|
||||||
|
viewBox="0 0 14.519332 14.518607"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2"
|
||||||
|
inkscape:cx="92.75"
|
||||||
|
inkscape:cy="546.5"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="-3.6500685e-13"
|
||||||
|
y="0"
|
||||||
|
width="14.519333"
|
||||||
|
height="14.518607"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-81.081288,-167.88827)"><path
|
||||||
|
id="rect172"
|
||||||
|
style="fill:#7f7f7f;fill-opacity:1;stroke:#000000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="m 94.511966,168.08826 -8.159189,8.15918 -1.539827,-1.53983 -3.324114,3.32339 -0.207549,4.17587 4.176605,-0.20681 3.32375,-3.32376 -1.540192,-1.54019 8.159186,-8.15919 z" /><path
|
||||||
|
id="path145-7"
|
||||||
|
style="fill:#a0a0a0;fill-opacity:0.5;stroke:#545454;stroke-width:0.232;stroke-dasharray:none;stroke-opacity:0.7"
|
||||||
|
d="m 88.300286,169.67427 c -0.48287,0.009 -0.965274,0.0916 -1.420584,0.25373 -1.738714,0.59694 -3.342068,0.9627 -3.03444,4.1274 -0.06576,0.94137 -0.0783,1.89737 0.06408,2.83239 0.07766,0.53633 0.382678,1.09991 0.888836,1.32446 0.446178,0.0891 0.985208,-0.0139 1.31258,0.39223 0.88847,0.68196 0.349966,2.06997 1.028362,1.973 0.4264,-0.0609 0.136174,-0.73886 0.260966,-0.80202 0.192692,-0.0975 0.181336,-0.0608 0.241846,0.18087 0.03243,0.38247 -0.05245,0.69761 0.28112,0.66198 0.550572,-0.0588 0.28691,-0.48129 0.328144,-0.95808 0.0732,-0.0163 0.08707,-0.0125 0.0894,-0.0114 h 5.18e-4 c 0.0023,-0.001 0.01617,-0.005 0.0894,0.0114 0.04123,0.47679 -0.222428,0.89925 0.328144,0.95808 0.333568,0.0356 0.248694,-0.27951 0.28112,-0.66198 0.06051,-0.2417 0.04915,-0.27839 0.241846,-0.18087 0.124792,0.0632 -0.165434,0.74107 0.260966,0.80202 0.678396,0.097 0.139892,-1.29104 1.028362,-1.973 0.327372,-0.40615 0.866402,-0.30311 1.31258,-0.39223 0.506158,-0.22455 0.81169,-0.78813 0.889352,-1.32446 0.14238,-0.93502 0.129318,-1.89102 0.06356,-2.83239 0.307628,-3.1647 -1.295726,-3.53046 -3.03444,-4.1274 -0.45531,-0.16216 -0.937716,-0.24475 -1.420586,-0.25373 -0.0135,0 -0.02664,-1e-4 -0.04031,0 -0.01367,-1e-4 -0.02733,0 -0.04083,0 z m -2.07274,4.18424 c 0.703538,-0.004 1.021816,0.49934 1.094506,0.7059 0.462654,1.11185 -0.373098,1.45665 -1.174088,1.42576 -0.699984,0.0989 -1.258414,0.0508 -1.342554,-1.01286 -0.01001,-0.33209 0.308906,-0.96177 1.096574,-1.08366 0.116648,-0.0235 0.225056,-0.0346 0.325562,-0.0351 z m 4.226614,0 c 0.100506,5.1e-4 0.208914,0.0116 0.325562,0.0351 0.787668,0.12189 1.106584,0.75157 1.096574,1.08366 -0.08414,1.06367 -0.64257,1.11173 -1.342554,1.01286 -0.80099,0.0309 -1.636742,-0.31391 -1.174088,-1.42576 0.07269,-0.20656 0.390968,-0.70943 1.094506,-0.7059 z m -2.113048,2.94194 c 0.215198,0.0216 0.284064,0.38489 0.583426,0.77618 0.276858,0.50031 -0.16476,0.69652 -0.409794,0.48318 -0.146516,-0.11734 -0.171114,-0.12967 -0.173632,-0.13074 -0.0025,0.001 -0.02763,0.0134 -0.17415,0.13074 -0.245034,0.21334 -0.686652,0.0171 -0.409794,-0.48318 0.299362,-0.39129 0.368746,-0.75459 0.583944,-0.77618 z" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
|
|
@ -0,0 +1,98 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="49.5746mm"
|
||||||
|
height="46.331978mm"
|
||||||
|
viewBox="0 0 49.5746 46.331978"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:cx="290.26733"
|
||||||
|
inkscape:cy="686.60068"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="-1.2367246e-13"
|
||||||
|
y="0"
|
||||||
|
width="49.574596"
|
||||||
|
height="46.331978"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="mirror_symmetry"
|
||||||
|
start_point="86.254165,185.51628"
|
||||||
|
end_point="86.385181,235.08332"
|
||||||
|
center_point="86.319673,210.2998"
|
||||||
|
id="path-effect149"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.2"
|
||||||
|
lpesatellites=""
|
||||||
|
mode="free"
|
||||||
|
discard_orig_path="false"
|
||||||
|
fuse_paths="false"
|
||||||
|
oposite_fuse="false"
|
||||||
|
split_items="false"
|
||||||
|
split_open="false"
|
||||||
|
link_styles="false" /></defs><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-61.512867,-189.01705)"><g
|
||||||
|
id="g151"><path
|
||||||
|
style="fill:#ff2424;fill-opacity:1;stroke:#ff2424;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 86.254166,235.08229 c 0,0 -24.302335,-18.46127 -24.606251,-31.8823 -0.126849,-5.60168 4.153389,-12.70855 9.657292,-13.75832 5.852331,-1.11623 14.123929,1.87077 14.932422,9.82265 z m 0.262022,-6.9e-4 c 0,0 24.204402,-18.58949 24.437362,-32.01193 0.0972,-5.60228 -4.22051,-12.68642 -9.72988,-13.70708 -5.858155,-1.08528 -14.113847,1.94541 -14.880292,9.90145 z"
|
||||||
|
id="path148"
|
||||||
|
sodipodi:nodetypes="caacc"
|
||||||
|
inkscape:path-effect="#path-effect149"
|
||||||
|
inkscape:original-d="m 86.254166,235.08229 c 0,0 -24.302335,-18.46127 -24.606251,-31.8823 -0.126849,-5.60168 4.153389,-12.70855 9.657292,-13.75832 5.852331,-1.11623 14.123929,1.87077 14.932422,9.82265 z" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path150"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="4"
|
||||||
|
sodipodi:cx="79.886864"
|
||||||
|
sodipodi:cy="208.97804"
|
||||||
|
sodipodi:r1="5.2418227"
|
||||||
|
sodipodi:r2="2.8830025"
|
||||||
|
sodipodi:arg1="0.607802"
|
||||||
|
sodipodi:arg2="1.3932002"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 84.189903,211.97146 c -1.084141,1.55845 -1.92512,-0.49115 -3.793716,-0.15576 -1.868596,0.33539 -1.944289,2.54952 -3.502742,1.46538 -1.558453,-1.08414 0.491151,-1.92512 0.155762,-3.79371 -0.335389,-1.8686 -2.549524,-1.94429 -1.465383,-3.50275 1.084141,-1.55845 1.925121,0.49116 3.793717,0.15577 1.868596,-0.33539 1.944289,-2.54953 3.502742,-1.46539 1.558452,1.08414 -0.491152,1.92512 -0.155763,3.79372 0.335389,1.8686 2.549524,1.94429 1.465383,3.50274 z"
|
||||||
|
transform="rotate(28.628814,62.558026,193.09515)" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#fff001;fill-opacity:1;stroke:#ff2424;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path151"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="4"
|
||||||
|
sodipodi:cx="68.848625"
|
||||||
|
sodipodi:cy="200.93324"
|
||||||
|
sodipodi:r1="6.7481718"
|
||||||
|
sodipodi:r2="3.3373971"
|
||||||
|
sodipodi:arg1="0.80500349"
|
||||||
|
sodipodi:arg2="1.5925996"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 73.525842,205.79755 c -1.802133,1.72521 -2.255654,-1.4788 -4.749977,-1.5277 -2.4873,-0.0488 -3.071191,3.13767 -4.791545,1.34061 -1.725212,-1.80213 1.478793,-2.25565 1.527701,-4.74998 0.04877,-2.4873 -3.137671,-3.07119 -1.340613,-4.79154 1.802133,-1.72521 2.255655,1.47879 4.749978,1.5277 2.487299,0.0488 3.07119,-3.13767 4.791545,-1.34061 1.725212,1.80213 -1.478794,2.25565 -1.527702,4.74997 -0.04877,2.4873 3.137671,3.07119 1.340613,4.79155 z" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 4.7 KiB |
|
|
@ -0,0 +1,144 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="24.399992mm"
|
||||||
|
height="24.399998mm"
|
||||||
|
viewBox="0 0 24.399992 24.399998"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="-77"
|
||||||
|
inkscape:cy="466"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="24.399992"
|
||||||
|
height="24.399998"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="mirror_symmetry"
|
||||||
|
start_point="50.372741,180.48552"
|
||||||
|
end_point="50.372741,189.138"
|
||||||
|
center_point="50.372741,184.81176"
|
||||||
|
id="path-effect166-2"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.2"
|
||||||
|
lpesatellites=""
|
||||||
|
mode="free"
|
||||||
|
discard_orig_path="false"
|
||||||
|
fuse_paths="true"
|
||||||
|
oposite_fuse="false"
|
||||||
|
split_items="false"
|
||||||
|
split_open="false"
|
||||||
|
link_styles="false" /></defs><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-1.6153444,-96.526492)"><g
|
||||||
|
id="g179"><g
|
||||||
|
id="g166-2"
|
||||||
|
inkscape:path-effect="#path-effect166-2"
|
||||||
|
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-dasharray:none;stroke-opacity:0.5"
|
||||||
|
transform="translate(-36.557573,-76.085222)"
|
||||||
|
inkscape:export-filename="../../src/werewolves/werewolves/img/hunter.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08"><path
|
||||||
|
id="path164-8"
|
||||||
|
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
|
||||||
|
d="m 50.373047,183.43359 c -1.534096,0.26874 -1.029204,1.24619 -1.832031,2.63672 -0.65564,1.13559 -1.854319,1.39291 -1.029297,2.41211 0.94012,1.1614 1.512735,0.45231 2.861328,0.33008 1.348593,0.12223 1.921208,0.83132 2.861328,-0.33008 0.825022,-1.0192 -0.373657,-1.27652 -1.029297,-2.41211 -0.802827,-1.39053 -0.297935,-2.36798 -1.832031,-2.63672 z"
|
||||||
|
inkscape:original-d="m 50.372741,183.43371 c -1.534096,0.26874 -1.028586,1.24549 -1.831413,2.63602 -0.65564,1.13559 -1.854933,1.39253 -1.029911,2.41173 0.94012,1.1614 1.512731,0.45348 2.861324,0.33125 z" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
|
||||||
|
id="path165-3"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="2"
|
||||||
|
sodipodi:cx="51.634216"
|
||||||
|
sodipodi:cy="182.45293"
|
||||||
|
sodipodi:r1="1.3725731"
|
||||||
|
sodipodi:r2="0.82291079"
|
||||||
|
sodipodi:arg1="1.1071487"
|
||||||
|
sodipodi:arg2="2.677945"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 5.18186,0 c 0.7157,0.35785 0.992016,-0.14395 1.349867,-0.85965 0.35785,-0.7157 0.5935,-1.23783 -0.122201,-1.59568 -0.715701,-0.35785 -0.992016,0.14395 -1.349867,0.85965 -0.35785,0.7157 -0.5935,1.23783 0.122201,1.59568 z"
|
||||||
|
inkscape:transform-center-x="0.12681959"
|
||||||
|
inkscape:transform-center-y="0.079724714"
|
||||||
|
transform="translate(-4.4662386,1.8414355)" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#ff0505;fill-opacity:0.7;stroke:#c20000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
|
||||||
|
id="path165-6-8"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="2"
|
||||||
|
sodipodi:cx="51.634216"
|
||||||
|
sodipodi:cy="182.45293"
|
||||||
|
sodipodi:r1="1.3725731"
|
||||||
|
sodipodi:r2="0.82291079"
|
||||||
|
sodipodi:arg1="1.1071487"
|
||||||
|
sodipodi:arg2="2.677945"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 2.374602,-1.10775 c 0.734072,-0.31847 0.527114,-0.85263 0.208644,-1.5867 -0.31847,-0.73407 -0.567138,-1.25013 -1.30121,-0.93166 -0.734072,0.31847 -0.527114,0.85263 -0.208644,1.5867 0.31847,0.73407 0.567138,1.25013 1.30121,0.93166 z"
|
||||||
|
inkscape:transform-center-x="0.14863452"
|
||||||
|
inkscape:transform-center-y="0.01863483"
|
||||||
|
transform="rotate(25.009099,51.670619,176.27381)" /></g><g
|
||||||
|
id="g178"><path
|
||||||
|
id="path166-0"
|
||||||
|
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="m 13.815426,98.72668 c -5.522776,5e-5 -10.000374,4.47713 -10.000423,9.99991 4.9e-5,5.52277 4.477647,9.99985 10.000423,9.9999 5.522777,-5e-5 9.999857,-4.47713 9.999907,-9.9999 -5e-5,-5.52278 -4.47713,-9.99986 -9.999907,-9.99991 z m 0,1.99988 c 4.418297,-2e-5 8.000047,3.58172 8.000027,8.00003 2e-5,4.4183 -3.58173,8.00004 -8.000027,8.00002 -4.418301,2e-5 -8.000045,-3.58172 -8.000029,-8.00002 -1.6e-5,-4.41831 3.581728,-8.00005 8.000029,-8.00003 z" /><rect
|
||||||
|
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect167-8-0-4"
|
||||||
|
width="1.5"
|
||||||
|
height="6"
|
||||||
|
x="-109.47657"
|
||||||
|
y="19.815336"
|
||||||
|
transform="rotate(-90)"
|
||||||
|
ry="0" /><rect
|
||||||
|
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect167-8-0-9-2"
|
||||||
|
width="1.5"
|
||||||
|
height="6"
|
||||||
|
x="-109.47657"
|
||||||
|
y="1.8153445"
|
||||||
|
transform="rotate(-90)"
|
||||||
|
ry="0" /><rect
|
||||||
|
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect167-8-0-1-2"
|
||||||
|
width="1.5"
|
||||||
|
height="6"
|
||||||
|
x="-14.56517"
|
||||||
|
y="-120.72649"
|
||||||
|
transform="scale(-1)"
|
||||||
|
ry="0" /><rect
|
||||||
|
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="rect167-8-0-1-7-2"
|
||||||
|
width="1.5"
|
||||||
|
height="6"
|
||||||
|
x="-14.56517"
|
||||||
|
y="-102.72649"
|
||||||
|
transform="scale(-1)"
|
||||||
|
ry="0" /></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 7.6 KiB |
|
|
@ -0,0 +1,56 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="21.653456mm"
|
||||||
|
height="25.003128mm"
|
||||||
|
viewBox="0 0 21.653456 25.003128"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="4"
|
||||||
|
inkscape:cx="95.125"
|
||||||
|
inkscape:cy="747.25"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="-1.7160703e-13"
|
||||||
|
y="1.0183469e-14"
|
||||||
|
width="21.653458"
|
||||||
|
height="25.003126"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-21.652939,-198.29178)"><path
|
||||||
|
id="path135"
|
||||||
|
style="fill:#bb07ff;fill-opacity:0.5;stroke-width:0.15"
|
||||||
|
d="m 21.652942,198.29177 v 3.06235 0.67024 17.53795 0.33693 3.39566 l 21.653459,-12.50157 z m 3.227193,5.59604 11.961027,6.90552 -11.961027,6.90553 z"
|
||||||
|
inkscape:export-filename="../../src/werewolves/werewolves/img/li.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08" /><path
|
||||||
|
d="m 24.88,203.888 v 13.81103 l 11.961027,-6.9055 z"
|
||||||
|
style="fill:#b906ff;fill-opacity:0.25;stroke-width:0.15"
|
||||||
|
id="path136" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
|
|
@ -0,0 +1,130 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="16.688932mm"
|
||||||
|
height="14.296382mm"
|
||||||
|
viewBox="0 0 16.688932 14.296382"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="-77"
|
||||||
|
inkscape:cy="466"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="16.688932"
|
||||||
|
height="14.296382"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="mirror_symmetry"
|
||||||
|
start_point="88.608019,180.36521"
|
||||||
|
end_point="88.608019,194.83255"
|
||||||
|
center_point="88.608019,187.59888"
|
||||||
|
id="path-effect171"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.2"
|
||||||
|
lpesatellites=""
|
||||||
|
mode="free"
|
||||||
|
discard_orig_path="false"
|
||||||
|
fuse_paths="true"
|
||||||
|
oposite_fuse="false"
|
||||||
|
split_items="false"
|
||||||
|
split_open="false"
|
||||||
|
link_styles="false" /><inkscape:path-effect
|
||||||
|
effect="mirror_symmetry"
|
||||||
|
start_point="50.372741,180.48552"
|
||||||
|
end_point="50.372741,189.138"
|
||||||
|
center_point="50.372741,184.81176"
|
||||||
|
id="path-effect166-9"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.2"
|
||||||
|
lpesatellites=""
|
||||||
|
mode="free"
|
||||||
|
discard_orig_path="false"
|
||||||
|
fuse_paths="true"
|
||||||
|
oposite_fuse="false"
|
||||||
|
split_items="false"
|
||||||
|
split_open="false"
|
||||||
|
link_styles="false" /></defs><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-66.137764,-154.64787)"><path
|
||||||
|
id="path168"
|
||||||
|
style="fill:#dd2e44;fill-opacity:1;stroke:#8c1725;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="m 88.607422,182.44336 -0.701172,3.80859 -1.953125,-1.31836 1.234375,4.75782 -4.384766,-4.51172 0.972657,2.91797 -3.3125,-1.20899 2.748046,2.58789 -2.310546,0.55664 4.65039,2.92969 -1.697265,1.86914 4.134765,-0.27148 v 1.77929 l 0.619141,-0.004 0.621094,0.004 v -1.77929 l 4.134765,0.27148 -1.697265,-1.86914 4.65039,-2.92969 -2.310547,-0.55664 2.746094,-2.58789 -3.310547,1.20899 0.972656,-2.91797 -4.386718,4.51172 1.234375,-4.75782 -1.953125,1.31836 z"
|
||||||
|
inkscape:path-effect="#path-effect171"
|
||||||
|
inkscape:original-d="m 88.655,182.18911 -0.747931,4.06282 -1.953642,-1.31777 1.23441,4.75775 -4.385923,-4.51236 0.973124,2.91846 -3.311647,-1.21027 2.747117,2.58892 -2.310287,0.55711 4.650449,2.92913 -1.697906,1.86965 4.134651,-0.27147 v 1.77805 l 1.207698,-0.007 -0.281059,-1.89723 z"
|
||||||
|
sodipodi:nodetypes="cccccccccccccccc"
|
||||||
|
inkscape:label="maple"
|
||||||
|
inkscape:export-filename="../../src/werewolves/werewolves/img/maple-wolf.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08"
|
||||||
|
transform="translate(-14.125194,-27.595579)" /><g
|
||||||
|
id="g166-0"
|
||||||
|
inkscape:path-effect="#path-effect166-9"
|
||||||
|
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-dasharray:none;stroke-opacity:0.5"
|
||||||
|
transform="matrix(0.84,0,0,0.84,32.089386,7.544321)"><path
|
||||||
|
id="path164-5"
|
||||||
|
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
|
||||||
|
d="m 50.373047,183.43359 c -1.534096,0.26874 -1.029204,1.24619 -1.832031,2.63672 -0.65564,1.13559 -1.854319,1.39291 -1.029297,2.41211 0.94012,1.1614 1.512735,0.45231 2.861328,0.33008 1.348593,0.12223 1.921208,0.83132 2.861328,-0.33008 0.825022,-1.0192 -0.373657,-1.27652 -1.029297,-2.41211 -0.802827,-1.39053 -0.297935,-2.36798 -1.832031,-2.63672 z"
|
||||||
|
inkscape:original-d="m 50.372741,183.43371 c -1.534096,0.26874 -1.028586,1.24549 -1.831413,2.63602 -0.65564,1.13559 -1.854933,1.39253 -1.029911,2.41173 0.94012,1.1614 1.512731,0.45348 2.861324,0.33125 z" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
|
||||||
|
id="path165-0"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="2"
|
||||||
|
sodipodi:cx="51.634216"
|
||||||
|
sodipodi:cy="182.45293"
|
||||||
|
sodipodi:r1="1.3725731"
|
||||||
|
sodipodi:r2="0.82291079"
|
||||||
|
sodipodi:arg1="1.1071487"
|
||||||
|
sodipodi:arg2="2.677945"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 5.18186,0 c 0.7157,0.35785 0.992016,-0.14395 1.349867,-0.85965 0.35785,-0.7157 0.5935,-1.23783 -0.122201,-1.59568 -0.715701,-0.35785 -0.992016,0.14395 -1.349867,0.85965 -0.35785,0.7157 -0.5935,1.23783 0.122201,1.59568 z"
|
||||||
|
inkscape:transform-center-x="0.12681959"
|
||||||
|
inkscape:transform-center-y="0.079724714"
|
||||||
|
transform="translate(-4.4662386,1.8414355)" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#0f07ff;fill-opacity:0.5;stroke:#05009e;stroke-width:0.47619;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:0.5;paint-order:normal"
|
||||||
|
id="path165-6-2"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="2"
|
||||||
|
sodipodi:cx="51.634216"
|
||||||
|
sodipodi:cy="182.45293"
|
||||||
|
sodipodi:r1="1.3725731"
|
||||||
|
sodipodi:r2="0.82291079"
|
||||||
|
sodipodi:arg1="1.1071487"
|
||||||
|
sodipodi:arg2="2.677945"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 2.374602,-1.10775 c 0.734072,-0.31847 0.527114,-0.85263 0.208644,-1.5867 -0.31847,-0.73407 -0.567138,-1.25013 -1.30121,-0.93166 -0.734072,0.31847 -0.527114,0.85263 -0.208644,1.5867 0.31847,0.73407 0.567138,1.25013 1.30121,0.93166 z"
|
||||||
|
inkscape:transform-center-x="0.14863452"
|
||||||
|
inkscape:transform-center-y="0.01863483"
|
||||||
|
transform="rotate(25.009099,51.670619,176.27381)" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
|
|
@ -0,0 +1,80 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="50.149998mm"
|
||||||
|
height="50.150002mm"
|
||||||
|
viewBox="0 0 50.149998 50.150002"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2"
|
||||||
|
inkscape:cx="92.75"
|
||||||
|
inkscape:cy="546.5"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="50.149998"
|
||||||
|
height="50.150002"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-4.4921871,-117.73386)"><g
|
||||||
|
id="g172"
|
||||||
|
transform="translate(-7.14375,-0.396875)"><circle
|
||||||
|
style="fill:#cfd099;fill-opacity:1;stroke:#ffffff;stroke-width:0.15;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path76"
|
||||||
|
cx="36.710938"
|
||||||
|
cy="143.20573"
|
||||||
|
r="25" /><path
|
||||||
|
id="path99"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.200209"
|
||||||
|
d="m 38.581863,146.75538 c -1.916877,1.18904 2.453505,2.42082 0.462124,0.43584 z m 4.389463,2.77679 c 0.137019,2.01253 -0.0156,4.7805 -2.550624,2.73406 -2.861437,0.11654 0.195479,3.17733 -1.523948,3.39965 -0.92667,3.02839 5.356204,3.68705 4.890562,-0.0422 0.53779,-2.01884 0.406862,-4.33848 -0.81599,-6.09151 z m 0.674308,9.56386 c -0.803242,0.7159 1.034521,0.46688 0,0 z" /><path
|
||||||
|
d="m 51.442339,133.50762 c -5.397597,0.56281 -10.551279,3.63338 -13.382225,8.29965 2.124451,1.1777 3.300948,0.84485 4.632117,-1.20688 2.551245,-1.70683 7.705503,4.70413 6.019673,-0.60498 -3.580177,-0.9319 -4.108073,-5.59673 0.261566,-5.11191 1.037773,0.5368 6.145034,-0.98564 2.468869,-1.37588 z m -2.801896,6.55774 c 0.531865,0.82548 -0.977438,-0.12296 0,0 z"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
|
||||||
|
id="path112" /><path
|
||||||
|
d="m 47.385221,123.15268 c -2.116527,-0.3773 -0.374219,4.06696 0.12442,2.12772 0.315898,-1.44874 1.060285,1.80984 2.221196,0.90171 1.889911,-1.11878 0.27636,-3.33696 -1.527064,-2.87862 -0.275754,-0.0313 -0.551969,-0.0713 -0.818552,-0.15081 z"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
|
||||||
|
id="path111" /><path
|
||||||
|
d="m 37.685038,119.98854 c -3.108408,2.92746 3.876651,0.26308 0.283578,0.27699 l -0.163948,-0.0794 z"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
|
||||||
|
id="path110" /><path
|
||||||
|
id="path109"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
|
||||||
|
d="m 32.397523,124.98657 c -1.520733,-2.58364 -1.771355,1.74661 0,0 z m -2.111335,-0.81943 c -0.420973,-0.68681 -0.277666,0.72793 0,0 z m -1.369925,0.2192 c -0.736577,-0.45274 0.113448,0.83066 0,0 z m 0.864925,-2.83663 c -2.403682,-1.73005 -7.217942,3.67933 -4.141043,4.75194 2.481815,0.24594 3.0505,-3.21526 4.141043,-4.75194 z m -2.934509,6.73595 c -0.382718,-1.12289 -0.773242,0.75887 0,0 z m -1.987823,-4.90838 c 0.800465,-0.75074 -0.699944,0.19494 0,0 z m -1.024981,1.17776 c -1.210082,-0.16455 0.156879,1.24643 0,0 z m -0.931645,1.49925 c -1.598736,0.091 -0.357093,1.80583 0,0 z"
|
||||||
|
inkscape:transform-center-x="-2.2830464"
|
||||||
|
inkscape:transform-center-y="-0.34255434" /><path
|
||||||
|
d="m 49.749135,150.49283 c 0.406653,2.6723 -0.700738,5.52316 0.490654,8.07174 1.038538,1.49687 2.463402,-0.25164 3.656675,-0.36492 1.343063,0.19135 2.990671,-0.35794 2.841528,-1.98822 0.208749,-1.45986 0.414602,-3.01659 -0.454517,-4.33018 -1.013182,-1.77262 -2.945795,-3.11543 -5.010966,-3.17524 -0.833932,0.14614 -1.55328,0.92034 -1.523374,1.78682 z m 3.944243,-0.76662 c 0.606103,-0.0492 -0.41019,0.42034 0,0 z m -2.130915,6.41908 c 0.979395,0.87352 -1.618533,0.77939 -0.243726,0.0418 z m 4.670422,-3.31817 c 0.654926,-0.61253 -0.03189,0.78519 0,0 z"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
|
||||||
|
id="path101" /><path
|
||||||
|
id="path97"
|
||||||
|
style="fill:#80813b;fill-opacity:1;stroke:none;stroke-width:0.231443"
|
||||||
|
d="m 41.903566,125.93725 c -6.044997,-0.64486 -8.64443,8.42233 -2.19329,10.10398 3.792226,2.96513 10.687212,2.61967 12.734453,-2.19823 2.1165,-7.75207 -9.058032,-2.72396 -10.541163,-7.90575 z m -15.060723,2.99137 c -5.528617,-3.05247 -10.922749,6.92225 -5.197617,9.72036 4.862793,2.09409 10.803366,-4.70252 6.587612,-8.7891 l -0.646271,-0.52979 z m 10.777872,7.1222 c -2.278654,1.80276 2.469375,0.14035 0,0 z m -4.303958,2.37974 c -2.574255,1.2736 2.388024,0.82299 0,0 z m -0.963816,4.17231 c -4.785091,0.19047 3.229165,5.66543 0.667405,0.56979 z m 4.893345,0.43918 c -1.855402,0.88455 1.710607,0.24689 0,0 z m -15.056031,7.1523 c -4.525471,3.17001 0.856774,8.93027 5.149694,7.52189 3.500644,-1.07544 10.261668,1.14637 11.357974,-3.16195 -3.000623,-4.60817 -9.108436,0.13477 -13.410645,-2.25697 -1.192258,-0.41483 -2.282793,-1.13383 -3.097023,-2.10297 z m 16.129285,0.72871 c -1.725064,-2.16335 0.0097,1.68564 0,0 z m -9.224662,9.24983 c -2.494907,3.43877 4.629782,0.13898 0,0 z" /><path
|
||||||
|
d="m 13.749471,143.49274 c -1.556131,4.09194 5.086128,3.65147 6.733592,6.11814 4.00606,-0.92451 -2.800253,-3.95742 0.489662,-6.1443 2.381473,-1.43043 1.913537,-4.55032 -1.152068,-3.69537 -2.409897,0.40461 -4.454496,1.96416 -6.071186,3.72153 z"
|
||||||
|
style="fill:#a9aa51;fill-opacity:1;stroke:none;stroke-width:0.15"
|
||||||
|
id="path1" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 6.1 KiB |
|
|
@ -0,0 +1,65 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="22.67716mm"
|
||||||
|
height="10.522518mm"
|
||||||
|
viewBox="0 0 22.67716 10.522518"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="0.5"
|
||||||
|
inkscape:cx="-77"
|
||||||
|
inkscape:cy="466"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="1.3155397e-14"
|
||||||
|
width="22.67716"
|
||||||
|
height="10.522517"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-86.867671,-166.75493)"><g
|
||||||
|
id="g176"><g
|
||||||
|
id="g75-4"
|
||||||
|
transform="matrix(0.14,0,0,0.14,77.802364,152.21471)"><circle
|
||||||
|
style="fill:#54ffff;fill-opacity:0.146665;stroke:#00adc1;stroke-width:2.348;stroke-opacity:1"
|
||||||
|
id="path9-1"
|
||||||
|
cx="145.74207"
|
||||||
|
cy="141.43904"
|
||||||
|
r="25"
|
||||||
|
inkscape:export-filename="../../src/werewolves/werewolves/img/powerful.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08" /><path
|
||||||
|
style="fill:#00adc1;fill-opacity:1;stroke:#00adc1;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 146.7354,116.77348 -12.81832,29.07761 11.35727,-0.99768 -9.76266,20.13334 23.61744,-27.67112 -15.11625,2.07403 12.86097,-20.49482 z"
|
||||||
|
id="path10-2"
|
||||||
|
sodipodi:nodetypes="cccccccc" /></g><path
|
||||||
|
d="m 98.447322,166.75526 c -6.10441,-0.0452 -10.77234,4.43526 -11.57965,5.26015 0.8552,0.87172 5.50621,5.3067 11.57965,5.26169 6.216538,-0.19268 10.307138,-4.38351 11.097508,-5.26118 -0.996,-1.0793 -5.19612,-5.21642 -11.098028,-5.26014 z m -0.24081,1.26091 a 4,4 0 0 1 3.999748,3.99975 4,4 0 0 1 -3.999748,4.00028 4,4 0 0 1 -4.00027,-4.00028 4,4 0 0 1 4.00027,-3.99975 z m -6.69985,2.99982 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99994,-0.99994 1,1 0 0 1 0.99994,-0.99993 z m 13.400208,0 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99993,-0.99994 1,1 0 0 1 0.99993,-0.99993 z"
|
||||||
|
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#00adc1;fill-opacity:1;enable-background:accumulate;stop-color:#000000"
|
||||||
|
id="path124-6" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -0,0 +1,50 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="22.677168mm"
|
||||||
|
height="10.522534mm"
|
||||||
|
viewBox="0 0 22.677168 10.522534"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="300.80764"
|
||||||
|
inkscape:cy="633.52348"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="1.2392502e-14"
|
||||||
|
width="22.677168"
|
||||||
|
height="10.522533"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-133.55426,-135.5815)"><path
|
||||||
|
d="m 145.13392,135.58183 c -6.10441,-0.0452 -10.77234,4.43526 -11.57965,5.26015 0.8552,0.87172 5.50621,5.3067 11.57965,5.26169 6.21654,-0.19268 10.30714,-4.38351 11.09751,-5.26118 -0.996,-1.0793 -5.19612,-5.21642 -11.09803,-5.26014 z m -0.24081,1.26091 a 4,4 0 0 1 3.99975,3.99975 4,4 0 0 1 -3.99975,4.00028 4,4 0 0 1 -4.00027,-4.00028 4,4 0 0 1 4.00027,-3.99975 z m -6.69985,2.99982 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99994,-0.99994 1,1 0 0 1 0.99994,-0.99993 z m 13.40021,0 a 1,1 0 0 1 0.99994,0.99993 1,1 0 0 1 -0.99994,0.99994 1,1 0 0 1 -0.99993,-0.99994 1,1 0 0 1 0.99993,-0.99993 z"
|
||||||
|
style="baseline-shift:baseline;display:inline;overflow:visible;vector-effect:none;fill:#0f07ff;enable-background:accumulate;stop-color:#000000"
|
||||||
|
id="path124" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.3 KiB |
|
|
@ -0,0 +1,74 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="21.913561mm"
|
||||||
|
height="34.790867mm"
|
||||||
|
viewBox="0 0 21.913561 34.790867"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="22.627417"
|
||||||
|
inkscape:cx="300.80764"
|
||||||
|
inkscape:cy="633.52348"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="21.913561"
|
||||||
|
height="34.790867"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-66.647994,-150.10879)"><path
|
||||||
|
id="path153-4"
|
||||||
|
style="fill:#675e00;fill-opacity:1;stroke:#957700;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="m 85.596185,159.31129 c -1.66567,-0.0492 -4.25335,0.61677 -7.991239,1.07073 -3.737893,-0.45396 -6.326587,-1.1194 -7.992277,-1.07022 -2.77615,0.082 -2.990253,2.15033 -1.701188,10.50014 1.120954,7.26088 7.414595,11.26672 9.693465,14.58774 2.278869,-3.32102 8.572509,-7.32686 9.693469,-14.58774 1.28906,-8.34981 1.0739,-10.41872 -1.70223,-10.50065 z"
|
||||||
|
inkscape:export-filename="../../src/werewolves/werewolves/img/shield-and-sword.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08" /><path
|
||||||
|
style="fill:#9d9a00;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 83.045329,161.77646 c -0.283124,0.54265 0.392812,4.5956 0,8.13594 -0.392763,3.5399 -5.097171,11.89492 -5.097171,11.89492 4.172167,-3.05047 7.003337,-7.94259 7.477566,-8.87397 1.558474,-3.06081 1.883779,-10.92635 1.27574,-11.46423 -0.700643,-0.6198 -3.342424,-0.29394 -3.656135,0.30734 z"
|
||||||
|
id="path156"
|
||||||
|
sodipodi:nodetypes="sscsss" /><path
|
||||||
|
id="path4-9-8"
|
||||||
|
style="fill:#e1e1e1;fill-opacity:1;stroke-width:0.679044"
|
||||||
|
inkscape:transform-center-x="-0.11920648"
|
||||||
|
inkscape:transform-center-y="-9.0503731"
|
||||||
|
d="m 74.576497,155.93257 v 20.3295 l 2.991999,4.80224 2.992334,-4.80224 v -20.3295 z"
|
||||||
|
sodipodi:nodetypes="cccccc" /><path
|
||||||
|
id="path4-1-2-1"
|
||||||
|
style="fill:#000000;fill-opacity:0.504713;stroke-width:0.339521"
|
||||||
|
inkscape:transform-center-x="-0.11920648"
|
||||||
|
inkscape:transform-center-y="-9.0503731"
|
||||||
|
d="m 79.00946,156.45806 h -2.881013 l 1.440049,22.4915 z" /><path
|
||||||
|
id="path159"
|
||||||
|
style="fill:#7f6600;fill-opacity:1;stroke-width:0.339521"
|
||||||
|
inkscape:transform-center-x="-0.11920648"
|
||||||
|
inkscape:transform-center-y="-9.0503731"
|
||||||
|
d="m 76.626193,150.59392 v 5.3295 h -2.935667 c -0.178214,0 -0.178214,0.53464 0,0.53464 0.02659,0 7.755941,0 7.755941,0 0.2661,0 0.283695,-0.53464 0,-0.53464 h -2.935333 v -5.3295 c 0,-0.59902 -1.884941,-0.693 -1.884941,0 z"
|
||||||
|
sodipodi:nodetypes="zcsssscsz" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.7 KiB |
|
|
@ -0,0 +1,71 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="21.913561mm"
|
||||||
|
height="26.091125mm"
|
||||||
|
viewBox="0 0 21.913561 26.091125"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="2"
|
||||||
|
inkscape:cx="307.25"
|
||||||
|
inkscape:cy="795"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="4.4822163e-22"
|
||||||
|
y="0"
|
||||||
|
width="21.913561"
|
||||||
|
height="26.091125"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-69.386984,-204.0026)"><g
|
||||||
|
id="g156"><path
|
||||||
|
id="path153-4"
|
||||||
|
style="fill:#fff001;fill-opacity:1;stroke:#daaf00;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
d="m 106.9933,181.0502 c -1.66567,-0.0492 -4.25335,0.61677 -7.991239,1.07073 -3.737893,-0.45396 -6.326587,-1.1194 -7.992277,-1.07022 -2.77615,0.082 -2.990253,2.15033 -1.701188,10.50014 1.120954,7.26088 7.414595,11.26672 9.693465,14.58774 2.278869,-3.32102 8.572509,-7.32686 9.693469,-14.58774 1.28906,-8.34981 1.0739,-10.41872 -1.70223,-10.50065 z"
|
||||||
|
transform="translate(-18.658128,23.45515)" /><path
|
||||||
|
style="fill:#fffbae;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||||
|
d="m 85.724999,207.43333 c -0.283124,0.54265 0.921927,4.69525 0,8.13594 -0.92193,3.44068 -5.097171,11.89492 -5.097171,11.89492 4.172167,-3.05047 7.003337,-7.94259 7.477566,-8.87397 1.558474,-3.06081 1.883779,-10.92635 1.27574,-11.46423 -0.700643,-0.6198 -3.342424,-0.29394 -3.656135,0.30734 z"
|
||||||
|
id="path156"
|
||||||
|
sodipodi:nodetypes="sscsss" /><path
|
||||||
|
sodipodi:type="star"
|
||||||
|
style="fill:#7f6500;fill-opacity:1;stroke:#daaf00;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||||
|
id="path155-0"
|
||||||
|
inkscape:flatsided="false"
|
||||||
|
sodipodi:sides="6"
|
||||||
|
sodipodi:cx="99.020317"
|
||||||
|
sodipodi:cy="191.42604"
|
||||||
|
sodipodi:r1="7.1881213"
|
||||||
|
sodipodi:r2="4.5083508"
|
||||||
|
sodipodi:arg1="1.906205"
|
||||||
|
sodipodi:arg2="1.8672678"
|
||||||
|
inkscape:rounded="0.5"
|
||||||
|
inkscape:randomized="0"
|
||||||
|
d="m 96.65431,198.21361 c -1.309919,0.3028 1.927067,-1.45786 1.048904,-2.4759 -2.111403,-2.44772 -3.538875,-0.60338 -5.744108,-2.96691 -0.917188,-0.98302 2.22608,0.93996 2.668647,-0.32957 1.064081,-3.05239 -1.2469,-3.36644 -0.30264,-6.458 0.392731,-1.28582 0.299014,2.39782 1.619743,2.14633 3.175484,-0.60467 2.291975,-2.76307 5.441464,-3.49109 1.30992,-0.30279 -1.927062,1.45787 -1.0489,2.4759 2.1114,2.44772 3.53888,0.60338 5.74411,2.96691 0.91719,0.98302 -2.22608,-0.93996 -2.66865,0.32957 -1.06408,3.05239 1.2469,3.36645 0.30264,6.458 -0.39273,1.28582 -0.29901,-2.39782 -1.61974,-2.14633 -3.175485,0.60467 -2.291977,2.76307 -5.44147,3.49109 z"
|
||||||
|
transform="translate(-18.678228,23.718064)" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
|
|
@ -0,0 +1,72 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="45.428978mm"
|
||||||
|
height="54.734951mm"
|
||||||
|
viewBox="0 0 45.428978 54.734951"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
|
sodipodi:docname="icons.svg"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="true"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:document-units="mm"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:zoom="1.4142136"
|
||||||
|
inkscape:cx="204.00031"
|
||||||
|
inkscape:cy="749.53319"
|
||||||
|
inkscape:window-width="1918"
|
||||||
|
inkscape:window-height="1061"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="17"
|
||||||
|
inkscape:window-maximized="0"
|
||||||
|
inkscape:current-layer="layer4"><inkscape:page
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="45.428978"
|
||||||
|
height="54.734951"
|
||||||
|
id="page2"
|
||||||
|
margin="0"
|
||||||
|
bleed="0" /></sodipodi:namedview><defs
|
||||||
|
id="defs1"><inkscape:path-effect
|
||||||
|
effect="mirror_symmetry"
|
||||||
|
start_point="145.93901,200.00018"
|
||||||
|
end_point="145.93901,257.23118"
|
||||||
|
center_point="145.93901,228.61568"
|
||||||
|
id="path-effect146"
|
||||||
|
is_visible="true"
|
||||||
|
lpeversion="1.2"
|
||||||
|
lpesatellites=""
|
||||||
|
mode="free"
|
||||||
|
discard_orig_path="false"
|
||||||
|
fuse_paths="false"
|
||||||
|
oposite_fuse="false"
|
||||||
|
split_items="false"
|
||||||
|
split_open="false"
|
||||||
|
link_styles="false" /></defs><g
|
||||||
|
inkscape:groupmode="layer"
|
||||||
|
id="layer4"
|
||||||
|
inkscape:label="Layer 4"
|
||||||
|
transform="translate(-102.64477,-201.30981)"><path
|
||||||
|
d="m 145.93901,249.87364 c 0,0 -0.0303,-0.0367 -0.44909,0.0563 -0.20617,2.38398 1.11335,4.49732 -1.63951,4.79143 -1.66784,0.17819 -1.24406,-1.39832 -1.40619,-3.31064 -0.30255,-1.20853 -0.24531,-1.39312 -1.20877,-0.90551 -0.62396,0.31579 0.82573,3.70676 -1.30627,4.01151 -3.39198,0.48485 -0.69804,-6.45466 -5.14039,-9.8645 -1.63686,-2.03072 -4.33202,-1.51761 -6.56291,-1.96319 -2.53079,-1.12277 -4.05741,-3.94017 -4.44572,-6.62182 -0.7119,-4.67507 -0.64763,-9.45558 -0.31885,-14.16244 -1.53814,-15.82349 6.47812,-17.65127 15.17169,-20.63595 2.27655,-0.8108 4.68806,-1.22375 7.10241,-1.26865 0.0675,0 0.13527,2e-5 0.2036,5.1e-4 v 35.63039 c -1.07599,0.10793 -1.42136,1.92498 -2.91817,3.88142 -1.38429,2.50152 0.8238,3.48208 2.04897,2.41535 0.78196,-0.62623 0.8692,-0.65371 0.8692,-0.65371 z m -17.67696,-23.3593 c 0.4207,5.31835 3.21309,5.55909 6.71301,5.06471 4.00495,0.15448 8.18296,-1.56893 5.86969,-7.12815 -0.41537,-1.18035 -2.43304,-4.29435 -7.09899,-3.3542 -3.93834,0.60943 -5.53376,3.75721 -5.48371,5.41764 z m 17.67696,23.3593 c 0,0 0.0303,-0.0367 0.44909,0.0563 0.20617,2.38398 -1.11335,4.49732 1.63951,4.79143 1.66784,0.17819 1.24406,-1.39832 1.40619,-3.31064 0.30255,-1.20853 0.24531,-1.39312 1.20877,-0.90551 0.62396,0.31579 -0.82573,3.70676 1.30627,4.01151 3.39198,0.48485 0.69804,-6.45466 5.14039,-9.8645 1.63686,-2.03072 4.33202,-1.51761 6.56291,-1.96319 2.53079,-1.12277 4.05741,-3.94017 4.44572,-6.62182 0.7119,-4.67507 0.64763,-9.45558 0.31885,-14.16244 1.53814,-15.82349 -6.47812,-17.65127 -15.17169,-20.63595 -2.27655,-0.8108 -4.68806,-1.22375 -7.10241,-1.26865 -0.0675,0 -0.13527,2e-5 -0.2036,5.1e-4 v 35.63039 c 1.07599,0.10793 1.42136,1.92498 2.91817,3.88142 1.38429,2.50152 -0.8238,3.48208 -2.04897,2.41535 -0.78196,-0.62623 -0.8692,-0.65371 -0.8692,-0.65371 z m 17.67696,-23.3593 c -0.4207,5.31835 -3.21309,5.55909 -6.71301,5.06471 -4.00495,0.15448 -8.18296,-1.56893 -5.86969,-7.12815 0.41537,-1.18035 2.43304,-4.29435 7.09899,-3.3542 3.93834,0.60943 5.53376,3.75721 5.48371,5.41764 z"
|
||||||
|
style="fill:#a0a0a0;fill-opacity:1;stroke-width:0.161404"
|
||||||
|
id="path145"
|
||||||
|
inkscape:original-d="m 145.93901,249.87364 c 0,0 -0.0303,-0.0367 -0.44909,0.0563 -0.20617,2.38398 1.11335,4.49732 -1.63951,4.79143 -1.66784,0.17819 -1.24406,-1.39832 -1.40619,-3.31064 -0.30255,-1.20853 -0.24531,-1.39312 -1.20877,-0.90551 -0.62396,0.31579 0.82573,3.70676 -1.30627,4.01151 -3.39198,0.48485 -0.69804,-6.45466 -5.14039,-9.8645 -1.63686,-2.03072 -4.33202,-1.51761 -6.56291,-1.96319 -2.53079,-1.12277 -4.05741,-3.94017 -4.44572,-6.62182 -0.7119,-4.67507 -0.64763,-9.45558 -0.31885,-14.16244 -1.53814,-15.82349 6.47812,-17.65127 15.17169,-20.63595 2.27655,-0.8108 4.68806,-1.22375 7.10241,-1.26865 0.0675,0 0.13527,2e-5 0.2036,5.1e-4 v 35.63039 c -1.07599,0.10793 -1.42136,1.92498 -2.91817,3.88142 -1.38429,2.50152 0.8238,3.48208 2.04897,2.41535 0.78196,-0.62623 0.8692,-0.65371 0.8692,-0.65371 z m -17.67696,-23.3593 c 0.4207,5.31835 3.21309,5.55909 6.71301,5.06471 4.00495,0.15448 8.18296,-1.56893 5.86969,-7.12815 -0.41537,-1.18035 -2.43304,-4.29435 -7.09899,-3.3542 -3.93834,0.60943 -5.53376,3.75721 -5.48371,5.41764 z"
|
||||||
|
inkscape:path-effect="#path-effect146"
|
||||||
|
transform="translate(-20.579754,1.3096207)"
|
||||||
|
sodipodi:nodetypes="ccscssccccccccccccccccc"
|
||||||
|
inkscape:export-filename="../../src/werewolves/werewolves/img/skull;.svg"
|
||||||
|
inkscape:export-xdpi="900.08"
|
||||||
|
inkscape:export-ydpi="900.08" /></g></svg>
|
||||||
|
After Width: | Height: | Size: 5.3 KiB |
|
|
@ -18,6 +18,13 @@ $offensive_border: color.change($offensive_color, $alpha: 1.0);
|
||||||
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
||||||
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
$starts_as_villager_border: color.change($starts_as_villager_color, $alpha: 1.0);
|
||||||
|
|
||||||
|
$wolves_border_faint: color.change($wolves_border, $alpha: 0.3);
|
||||||
|
$village_border_faint: color.change($village_border, $alpha: 0.3);
|
||||||
|
$offensive_border_faint: color.change($offensive_border, $alpha: 0.3);
|
||||||
|
$defensive_border_faint: color.change($defensive_border, $alpha: 0.3);
|
||||||
|
$intel_border_faint: color.change($intel_border, $alpha: 0.3);
|
||||||
|
$starts_as_villager_border_faint: color.change($starts_as_villager_border, $alpha: 0.3);
|
||||||
|
|
||||||
|
|
||||||
@mixin flexbox() {
|
@mixin flexbox() {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
@ -350,8 +357,6 @@ button {
|
||||||
.role {
|
.role {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
|
|
@ -1042,6 +1047,10 @@ input {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $wolves_border;
|
background-color: $wolves_border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $wolves_border_faint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.intel {
|
.intel {
|
||||||
|
|
@ -1052,6 +1061,10 @@ input {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $intel_border;
|
background-color: $intel_border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $intel_border_faint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.defensive {
|
.defensive {
|
||||||
|
|
@ -1062,6 +1075,10 @@ input {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $defensive_border;
|
background-color: $defensive_border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $defensive_border_faint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.offensive {
|
.offensive {
|
||||||
|
|
@ -1072,6 +1089,10 @@ input {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $offensive_border;
|
background-color: $offensive_border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $offensive_border_faint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.starts-as-villager {
|
.starts-as-villager {
|
||||||
|
|
@ -1082,6 +1103,10 @@ input {
|
||||||
color: white;
|
color: white;
|
||||||
background-color: $starts_as_villager_border;
|
background-color: $starts_as_villager_border;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $starts_as_villager_border_faint;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.assignments {
|
.assignments {
|
||||||
|
|
@ -1176,15 +1201,6 @@ input {
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
.icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
filter: contrast(120%) brightness(120%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.role {
|
.role {
|
||||||
|
|
@ -1202,6 +1218,15 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
filter: contrast(120%) brightness(120%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.inactive {
|
.inactive {
|
||||||
filter: grayscale(100%) brightness(30%);
|
filter: grayscale(100%) brightness(30%);
|
||||||
}
|
}
|
||||||
|
|
@ -1346,3 +1371,227 @@ input {
|
||||||
font-size: 1rem !important;
|
font-size: 1rem !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.story {
|
||||||
|
user-select: text;
|
||||||
|
|
||||||
|
.time-period {
|
||||||
|
.day {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.executed {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&>span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.night {
|
||||||
|
|
||||||
|
ul.changes,
|
||||||
|
ul.choices {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
&>li {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
& span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute-span {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: baseline;
|
||||||
|
align-content: baseline;
|
||||||
|
justify-content: baseline;
|
||||||
|
justify-items: baseline;
|
||||||
|
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
padding-left: 5px;
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
&:has(.killer) {
|
||||||
|
border: 1px solid rgba(212, 85, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.powerful) {
|
||||||
|
border: 1px solid rgba(0, 173, 193, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:has(.inactive) {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.execution {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.3);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wolves {
|
||||||
|
background-color: color.change($wolves_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.intel {
|
||||||
|
background-color: color.change($intel_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.defensive {
|
||||||
|
background-color: color.change($defensive_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offensive {
|
||||||
|
background-color: color.change($offensive_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.village {
|
||||||
|
background-color: color.change($village_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.starts-as-villager {
|
||||||
|
background-color: color.change($starts_as_villager_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.alignment-eq {
|
||||||
|
img {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
border: 1px solid $intel_border;
|
||||||
|
background-color: color.change($intel_color, $alpha: 0.1);
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.character-span {
|
||||||
|
height: max-content;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: baseline;
|
||||||
|
align-content: baseline;
|
||||||
|
justify-content: baseline;
|
||||||
|
justify-items: baseline;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
.role {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
img {
|
||||||
|
vertical-align: sub;
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-top: 5px;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
padding-left: 10px;
|
||||||
|
padding-right: 10px;
|
||||||
|
height: 100%;
|
||||||
|
border: none;
|
||||||
|
gap: 5px;
|
||||||
|
|
||||||
|
&.wolves {
|
||||||
|
background-color: color.change($wolves_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.intel {
|
||||||
|
background-color: color.change($intel_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.defensive {
|
||||||
|
background-color: color.change($defensive_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offensive {
|
||||||
|
background-color: color.change($offensive_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.village {
|
||||||
|
background-color: color.change($village_color, $alpha: 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
padding-right: 10px;
|
||||||
|
|
||||||
|
&.wolves,
|
||||||
|
&.intel,
|
||||||
|
&.offensive,
|
||||||
|
&.defensive,
|
||||||
|
&.village {
|
||||||
|
background-color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.wolves {
|
||||||
|
border: 1px solid $wolves_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.intel {
|
||||||
|
border: 1px solid $intel_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.defensive {
|
||||||
|
border: 1px solid $defensive_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.offensive {
|
||||||
|
border: 1px solid $offensive_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.village {
|
||||||
|
border: 1px solid $village_border_faint;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.post-game {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ use serde::Serialize;
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameOver, GameSettings},
|
game::{GameOver, GameSettings, story::GameStory},
|
||||||
message::{
|
message::{
|
||||||
CharacterIdentity, CharacterState, PlayerState, PublicIdentity,
|
CharacterIdentity, CharacterState, PlayerState, PublicIdentity,
|
||||||
host::{
|
host::{
|
||||||
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
HostDayMessage, HostGameMessage, HostLobbyMessage, HostMessage, HostNightMessage,
|
||||||
ServerToHostMessage,
|
PostGameMessage, ServerToHostMessage,
|
||||||
},
|
},
|
||||||
night::{ActionPrompt, ActionResult},
|
night::{ActionPrompt, ActionResult},
|
||||||
},
|
},
|
||||||
|
|
@ -27,7 +27,7 @@ use yew::{html::Scope, prelude::*};
|
||||||
use crate::{
|
use crate::{
|
||||||
callback,
|
callback,
|
||||||
components::{
|
components::{
|
||||||
Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings,
|
Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story,
|
||||||
action::{ActionResultView, Prompt},
|
action::{ActionResultView, Prompt},
|
||||||
host::{DaytimePlayerList, Setup},
|
host::{DaytimePlayerList, Setup},
|
||||||
},
|
},
|
||||||
|
|
@ -208,6 +208,10 @@ pub enum HostState {
|
||||||
},
|
},
|
||||||
Prompt(ActionPrompt, usize),
|
Prompt(ActionPrompt, usize),
|
||||||
Result(Option<CharacterIdentity>, ActionResult),
|
Result(Option<CharacterIdentity>, ActionResult),
|
||||||
|
Story {
|
||||||
|
story: GameStory,
|
||||||
|
page: usize,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ServerToHostMessage> for HostEvent {
|
impl From<ServerToHostMessage> for HostEvent {
|
||||||
|
|
@ -242,6 +246,10 @@ impl From<ServerToHostMessage> for HostEvent {
|
||||||
ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => {
|
ServerToHostMessage::WaitingForRoleRevealAcks { ackd, waiting } => {
|
||||||
HostEvent::SetState(HostState::RoleReveal { ackd, waiting })
|
HostEvent::SetState(HostState::RoleReveal { ackd, waiting })
|
||||||
}
|
}
|
||||||
|
ServerToHostMessage::Story { story, page } => {
|
||||||
|
log::info!("story page: {page}");
|
||||||
|
HostEvent::SetState(HostState::Story { story, page })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -289,9 +297,24 @@ impl Component for Host {
|
||||||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||||
log::trace!("state: {:?}", self.state);
|
log::trace!("state: {:?}", self.state);
|
||||||
let content = match self.state.clone() {
|
let content = match self.state.clone() {
|
||||||
|
HostState::Story { story, page } => {
|
||||||
|
let new_lobby_click = crate::callback::send_message(
|
||||||
|
HostMessage::PostGame(PostGameMessage::NewLobby),
|
||||||
|
self.send.clone(),
|
||||||
|
);
|
||||||
|
html! {
|
||||||
|
<div class="post-game">
|
||||||
|
<Story story={story} />
|
||||||
|
<Button on_click={new_lobby_click}>{"new lobby"}</Button>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
HostState::GameOver { result } => {
|
HostState::GameOver { result } => {
|
||||||
let new_lobby = self.big_screen.not().then(|| {
|
let cont = self.big_screen.not().then(|| {
|
||||||
crate::callback::send_message(HostMessage::NewLobby, self.send.clone())
|
crate::callback::send_message(
|
||||||
|
HostMessage::PostGame(PostGameMessage::NextPage),
|
||||||
|
self.send.clone(),
|
||||||
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
|
|
@ -300,9 +323,9 @@ impl Component for Host {
|
||||||
GameOver::VillageWins => "village wins",
|
GameOver::VillageWins => "village wins",
|
||||||
GameOver::WolvesWin => "wolves win",
|
GameOver::WolvesWin => "wolves win",
|
||||||
}}
|
}}
|
||||||
next={new_lobby}
|
next={cont}
|
||||||
>
|
>
|
||||||
{"new lobby"}
|
{"continue"}
|
||||||
</CoverOfDarkness>
|
</CoverOfDarkness>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -449,12 +472,20 @@ impl Component for Host {
|
||||||
<Button on_click={to_big}>{"big screen 📺"}</Button>
|
<Button on_click={to_big}>{"big screen 📺"}</Button>
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let s = _ctx.link().clone();
|
||||||
|
let story_on_click = Callback::from(move |_| {
|
||||||
|
s.send_message(HostEvent::SetState(HostState::Story {
|
||||||
|
story: crate::clients::host::story_test::test_story(),
|
||||||
|
page: 0,
|
||||||
|
}));
|
||||||
|
});
|
||||||
html! {
|
html! {
|
||||||
<nav class="debug-nav" style="z-index: 3;">
|
<nav class="debug-nav" style="z-index: 3;">
|
||||||
<Button on_click={on_error_click}>{"error"}</Button>
|
<Button on_click={on_error_click}>{"error"}</Button>
|
||||||
<Button on_click={on_prev_click}>{"previous"}</Button>
|
<Button on_click={on_prev_click}>{"previous"}</Button>
|
||||||
{screen}
|
{screen}
|
||||||
<Button on_click={client_click}>{"client"}</Button>
|
<Button on_click={client_click}>{"client"}</Button>
|
||||||
|
<Button on_click={story_on_click}>{"story"}</Button>
|
||||||
</nav>
|
</nav>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
@ -468,7 +499,7 @@ impl Component for Host {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
fn update(&mut self, _ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||||
match msg {
|
match msg {
|
||||||
HostEvent::QrMode(mode) => {
|
HostEvent::QrMode(mode) => {
|
||||||
self.qr_mode = mode;
|
self.qr_mode = mode;
|
||||||
|
|
@ -488,7 +519,9 @@ impl Component for Host {
|
||||||
players: p,
|
players: p,
|
||||||
settings: _,
|
settings: _,
|
||||||
} => *p = players.into_iter().collect(),
|
} => *p = players.into_iter().collect(),
|
||||||
HostState::Disconnected | HostState::GameOver { result: _ } => {
|
HostState::Story { .. }
|
||||||
|
| HostState::Disconnected
|
||||||
|
| HostState::GameOver { .. } => {
|
||||||
let mut send = self.send.clone();
|
let mut send = self.send.clone();
|
||||||
let on_err = self.error_callback.clone();
|
let on_err = self.error_callback.clone();
|
||||||
|
|
||||||
|
|
@ -531,7 +564,8 @@ impl Component for Host {
|
||||||
*s = settings;
|
*s = settings;
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
HostState::Prompt(_, _)
|
HostState::Story { .. }
|
||||||
|
| HostState::Prompt(_, _)
|
||||||
| HostState::Result(_, _)
|
| HostState::Result(_, _)
|
||||||
| HostState::Disconnected
|
| HostState::Disconnected
|
||||||
| HostState::RoleReveal {
|
| HostState::RoleReveal {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,931 @@
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use werewolves_proto::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
|
diedto::{DiedTo, DiedToTitle},
|
||||||
|
game::{self, Game, GameOver, GameSettings, OrRandom, SetupRole, Village, story::GameStory},
|
||||||
|
message::{
|
||||||
|
CharacterState, Identification, PublicIdentity,
|
||||||
|
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||||
|
night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||||
|
},
|
||||||
|
player::PlayerId,
|
||||||
|
role::{Alignment, AlignmentEq, Killer, Powerful, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
trait GameSettingsExt {
|
||||||
|
fn add_and_assign(&mut self, role: SetupRole, player_id: PlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameSettingsExt for GameSettings {
|
||||||
|
fn add_and_assign(&mut self, role: SetupRole, player_id: PlayerId) {
|
||||||
|
let slot_id = self.new_slot(role.clone().into());
|
||||||
|
let mut slot = self.get_slot_by_id(slot_id).unwrap().clone();
|
||||||
|
slot.role = role;
|
||||||
|
slot.assign_to.replace(player_id);
|
||||||
|
self.update_slot(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn test_story() -> GameStory {
|
||||||
|
let players = (1..32u8)
|
||||||
|
.filter_map(NonZeroU8::new)
|
||||||
|
.map(|n| Identification {
|
||||||
|
player_id: PlayerId::from_u128(n.get() as _),
|
||||||
|
public: PublicIdentity {
|
||||||
|
name: format!("Player {n}"),
|
||||||
|
pronouns: Some("he/him".into()),
|
||||||
|
number: Some(n),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
let mut players_iter = players.iter().map(|p| p.player_id);
|
||||||
|
let (
|
||||||
|
werewolf,
|
||||||
|
dire_wolf,
|
||||||
|
shapeshifter,
|
||||||
|
alpha_wolf,
|
||||||
|
seer,
|
||||||
|
arcanist,
|
||||||
|
maple_wolf,
|
||||||
|
guardian,
|
||||||
|
vindicator,
|
||||||
|
adjudicator,
|
||||||
|
power_seer,
|
||||||
|
beholder,
|
||||||
|
gravedigger,
|
||||||
|
mortician,
|
||||||
|
insomniac,
|
||||||
|
empath,
|
||||||
|
scapegoat,
|
||||||
|
hunter,
|
||||||
|
) = (
|
||||||
|
(SetupRole::Werewolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::DireWolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Shapeshifter, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::AlphaWolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Seer, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Arcanist, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::MapleWolf, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Guardian, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Vindicator, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Adjudicator, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::PowerSeer, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Beholder, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Gravedigger, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Mortician, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Insomniac, players_iter.next().unwrap()),
|
||||||
|
(SetupRole::Empath, players_iter.next().unwrap()),
|
||||||
|
(
|
||||||
|
SetupRole::Scapegoat {
|
||||||
|
redeemed: OrRandom::Determined(false),
|
||||||
|
},
|
||||||
|
players_iter.next().unwrap(),
|
||||||
|
),
|
||||||
|
(SetupRole::Hunter, players_iter.next().unwrap()),
|
||||||
|
);
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(werewolf.0, werewolf.1);
|
||||||
|
settings.add_and_assign(dire_wolf.0, dire_wolf.1);
|
||||||
|
settings.add_and_assign(shapeshifter.0, shapeshifter.1);
|
||||||
|
settings.add_and_assign(alpha_wolf.0, alpha_wolf.1);
|
||||||
|
settings.add_and_assign(seer.0, seer.1);
|
||||||
|
settings.add_and_assign(arcanist.0, arcanist.1);
|
||||||
|
settings.add_and_assign(maple_wolf.0, maple_wolf.1);
|
||||||
|
settings.add_and_assign(guardian.0, guardian.1);
|
||||||
|
settings.add_and_assign(vindicator.0, vindicator.1);
|
||||||
|
settings.add_and_assign(adjudicator.0, adjudicator.1);
|
||||||
|
settings.add_and_assign(power_seer.0, power_seer.1);
|
||||||
|
settings.add_and_assign(beholder.0, beholder.1);
|
||||||
|
settings.add_and_assign(gravedigger.0, gravedigger.1);
|
||||||
|
settings.add_and_assign(mortician.0, mortician.1);
|
||||||
|
settings.add_and_assign(insomniac.0, insomniac.1);
|
||||||
|
settings.add_and_assign(empath.0, empath.1);
|
||||||
|
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||||
|
settings.add_and_assign(hunter.0, hunter.1);
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
|
||||||
|
let (
|
||||||
|
werewolf,
|
||||||
|
dire_wolf,
|
||||||
|
shapeshifter,
|
||||||
|
alpha_wolf,
|
||||||
|
seer,
|
||||||
|
arcanist,
|
||||||
|
maple_wolf,
|
||||||
|
guardian,
|
||||||
|
vindicator,
|
||||||
|
adjudicator,
|
||||||
|
power_seer,
|
||||||
|
beholder,
|
||||||
|
gravedigger,
|
||||||
|
mortician,
|
||||||
|
insomniac,
|
||||||
|
empath,
|
||||||
|
scapegoat,
|
||||||
|
hunter,
|
||||||
|
) = (
|
||||||
|
werewolf.1,
|
||||||
|
dire_wolf.1,
|
||||||
|
shapeshifter.1,
|
||||||
|
alpha_wolf.1,
|
||||||
|
seer.1,
|
||||||
|
arcanist.1,
|
||||||
|
maple_wolf.1,
|
||||||
|
guardian.1,
|
||||||
|
vindicator.1,
|
||||||
|
adjudicator.1,
|
||||||
|
power_seer.1,
|
||||||
|
beholder.1,
|
||||||
|
gravedigger.1,
|
||||||
|
mortician.1,
|
||||||
|
insomniac.1,
|
||||||
|
empath.1,
|
||||||
|
scapegoat.1,
|
||||||
|
hunter.1,
|
||||||
|
);
|
||||||
|
// let village = Village::new(&players, settings).unwrap();
|
||||||
|
let mut game = game::Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
game.next().title().wolves_intro();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().role_blocked();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(arcanist).character_id());
|
||||||
|
game.r#continue().role_blocked();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
game.mark_for_execution(game.character_by_player_id(alpha_wolf).character_id());
|
||||||
|
|
||||||
|
game.execute().title().guardian();
|
||||||
|
let protect = game.living_villager();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.response(ActionResponse::Shapeshift).sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
|
||||||
|
|
||||||
|
game.next().title().empath();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
assert!(!game.r#continue().empath());
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protect.player_id()).died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark_for_execution(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.execute().title().guardian();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().vindicator();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(protect.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::DireWolf));
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(dire_wolf).character_id());
|
||||||
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Execution);
|
||||||
|
|
||||||
|
game.next().title().empath();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
assert!(game.r#continue().empath());
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(power_seer).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(
|
||||||
|
game.living_villager_excl(protect.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.execute().title().vindicator();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
game.response(ActionResponse::Shapeshift).r#continue();
|
||||||
|
|
||||||
|
game.next().title().role_change();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(guardian).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(guardian).character_id());
|
||||||
|
assert_eq!(game.r#continue().mortician(), DiedToTitle::Wolfpack);
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(gravedigger).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), Some(RoleTitle::Guardian));
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(game.character_by_player_id(vindicator).character_id());
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager().character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.r#continue().seer();
|
||||||
|
|
||||||
|
game.next().title().arcanist();
|
||||||
|
game.mark(game.character_by_player_id(insomniac).character_id());
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().arcanist();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().adjudicator();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().power_seer();
|
||||||
|
|
||||||
|
game.next().title().gravedigger();
|
||||||
|
game.mark(game.character_by_player_id(shapeshifter).character_id());
|
||||||
|
assert_eq!(game.r#continue().gravedigger(), None);
|
||||||
|
|
||||||
|
game.next().title().mortician();
|
||||||
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
assert_eq!(
|
||||||
|
game.r#continue().mortician(),
|
||||||
|
DiedToTitle::GuardianProtecting
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(game.character_by_player_id(hunter).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().hunter();
|
||||||
|
game.mark(game.character_by_player_id(empath).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().insomniac();
|
||||||
|
game.r#continue().insomniac();
|
||||||
|
|
||||||
|
game.next().title().beholder();
|
||||||
|
game.mark(game.character_by_player_id(mortician).character_id());
|
||||||
|
assert_eq!(
|
||||||
|
game.r#continue().mortician(),
|
||||||
|
DiedToTitle::GuardianProtecting
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next_expect_game_over();
|
||||||
|
|
||||||
|
game.story()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub trait ActionPromptTitleExt {
|
||||||
|
fn wolf_pack_kill(&self);
|
||||||
|
fn cover_of_darkness(&self);
|
||||||
|
fn wolves_intro(&self);
|
||||||
|
fn role_change(&self);
|
||||||
|
fn seer(&self);
|
||||||
|
fn protector(&self);
|
||||||
|
fn arcanist(&self);
|
||||||
|
fn gravedigger(&self);
|
||||||
|
fn hunter(&self);
|
||||||
|
fn militia(&self);
|
||||||
|
fn maple_wolf(&self);
|
||||||
|
fn guardian(&self);
|
||||||
|
fn shapeshifter(&self);
|
||||||
|
fn alphawolf(&self);
|
||||||
|
fn direwolf(&self);
|
||||||
|
fn masons_wake(&self);
|
||||||
|
fn masons_leader_recruit(&self);
|
||||||
|
fn beholder(&self);
|
||||||
|
fn vindicator(&self);
|
||||||
|
fn pyremaster(&self);
|
||||||
|
fn empath(&self);
|
||||||
|
fn adjudicator(&self);
|
||||||
|
fn lone_wolf(&self);
|
||||||
|
fn insomniac(&self);
|
||||||
|
fn power_seer(&self);
|
||||||
|
fn mortician(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
|
fn mortician(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Mortician);
|
||||||
|
}
|
||||||
|
fn cover_of_darkness(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::CoverOfDarkness);
|
||||||
|
}
|
||||||
|
fn wolves_intro(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::WolvesIntro);
|
||||||
|
}
|
||||||
|
fn role_change(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::RoleChange);
|
||||||
|
}
|
||||||
|
fn seer(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Seer);
|
||||||
|
}
|
||||||
|
fn protector(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Protector);
|
||||||
|
}
|
||||||
|
fn arcanist(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Arcanist);
|
||||||
|
}
|
||||||
|
fn gravedigger(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Gravedigger);
|
||||||
|
}
|
||||||
|
fn hunter(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Hunter);
|
||||||
|
}
|
||||||
|
fn militia(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Militia);
|
||||||
|
}
|
||||||
|
fn maple_wolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::MapleWolf);
|
||||||
|
}
|
||||||
|
fn guardian(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Guardian);
|
||||||
|
}
|
||||||
|
fn shapeshifter(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Shapeshifter);
|
||||||
|
}
|
||||||
|
fn alphawolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::AlphaWolf);
|
||||||
|
}
|
||||||
|
fn direwolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::DireWolf);
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
fn beholder(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Beholder)
|
||||||
|
}
|
||||||
|
fn vindicator(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Vindicator)
|
||||||
|
}
|
||||||
|
fn pyremaster(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::PyreMaster)
|
||||||
|
}
|
||||||
|
fn adjudicator(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Adjudicator)
|
||||||
|
}
|
||||||
|
fn empath(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Empath)
|
||||||
|
}
|
||||||
|
fn lone_wolf(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::LoneWolfKill)
|
||||||
|
}
|
||||||
|
fn insomniac(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Insomniac)
|
||||||
|
}
|
||||||
|
fn power_seer(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::PowerSeer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ActionResultExt {
|
||||||
|
fn sleep(&self);
|
||||||
|
fn r#continue(&self);
|
||||||
|
fn seer(&self) -> Alignment;
|
||||||
|
fn insomniac(&self) -> Visits;
|
||||||
|
fn arcanist(&self) -> AlignmentEq;
|
||||||
|
fn adjudicator(&self) -> Killer;
|
||||||
|
fn role_blocked(&self);
|
||||||
|
fn gravedigger(&self) -> Option<RoleTitle>;
|
||||||
|
fn power_seer(&self) -> Powerful;
|
||||||
|
fn mortician(&self) -> DiedToTitle;
|
||||||
|
fn empath(&self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ActionResultExt for ActionResult {
|
||||||
|
fn empath(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Empath { scapegoat } => *scapegoat,
|
||||||
|
resp => panic!("expected empath, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn mortician(&self) -> DiedToTitle {
|
||||||
|
match self {
|
||||||
|
Self::Mortician(role) => *role,
|
||||||
|
resp => panic!("expected mortician, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn gravedigger(&self) -> Option<RoleTitle> {
|
||||||
|
match self {
|
||||||
|
Self::GraveDigger(role) => *role,
|
||||||
|
resp => panic!("expected gravedigger, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn adjudicator(&self) -> Killer {
|
||||||
|
match self {
|
||||||
|
Self::Adjudicator { killer } => *killer,
|
||||||
|
resp => panic!("expected adjudicator, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn power_seer(&self) -> Powerful {
|
||||||
|
match self {
|
||||||
|
Self::PowerSeer { powerful } => *powerful,
|
||||||
|
resp => panic!("expected power seer, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn sleep(&self) {
|
||||||
|
assert_eq!(*self, ActionResult::GoBackToSleep)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn role_blocked(&self) {
|
||||||
|
assert_eq!(*self, ActionResult::RoleBlocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn r#continue(&self) {
|
||||||
|
assert_eq!(*self, ActionResult::Continue)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn seer(&self) -> Alignment {
|
||||||
|
match self {
|
||||||
|
ActionResult::Seer(a) => a.clone(),
|
||||||
|
_ => panic!("expected a seer result"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn arcanist(&self) -> AlignmentEq {
|
||||||
|
match self {
|
||||||
|
ActionResult::Arcanist(same) => *same,
|
||||||
|
_ => panic!("expected an arcanist result"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insomniac(&self) -> Visits {
|
||||||
|
match self {
|
||||||
|
ActionResult::Insomniac(v) => v.clone(),
|
||||||
|
_ => panic!("expected an insomniac result"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AlignmentExt {
|
||||||
|
fn village(&self);
|
||||||
|
fn wolves(&self);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AlignmentExt for Alignment {
|
||||||
|
fn village(&self) {
|
||||||
|
assert_eq!(*self, Alignment::Village)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn wolves(&self) {
|
||||||
|
assert_eq!(*self, Alignment::Wolves)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ServerToHostMessageExt {
|
||||||
|
fn prompt(self) -> ActionPrompt;
|
||||||
|
fn result(self) -> ActionResult;
|
||||||
|
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ServerToHostMessageExt for ServerToHostMessage {
|
||||||
|
fn daytime(self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
|
||||||
|
match self {
|
||||||
|
Self::Daytime {
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
day,
|
||||||
|
..
|
||||||
|
} => (characters, marked, day),
|
||||||
|
resp => panic!("expected daytime, got {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(self) -> ActionPrompt {
|
||||||
|
match self {
|
||||||
|
Self::ActionPrompt(prompt, _) => prompt,
|
||||||
|
Self::Daytime { .. } => panic!("{}", "[got daytime]"),
|
||||||
|
msg => panic!("expected server message <<{msg:?}>> to be an ActionPrompt"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn result(self) -> ActionResult {
|
||||||
|
match self {
|
||||||
|
Self::ActionResult(_, res) => res,
|
||||||
|
msg => panic!("expected server message <<{msg:?}>> to be an ActionResult"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait GameExt {
|
||||||
|
fn villager_character_ids(&self) -> Box<[CharacterId]>;
|
||||||
|
fn character_by_player_id(&self, player_id: PlayerId) -> Character;
|
||||||
|
fn character_by_character_id(&self, character_id: CharacterId) -> Character;
|
||||||
|
fn next(&mut self) -> ActionPrompt;
|
||||||
|
fn r#continue(&mut self) -> ActionResult;
|
||||||
|
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||||
|
fn mark(&mut self, mark: CharacterId) -> ActionPrompt;
|
||||||
|
fn mark_and_check(&mut self, mark: CharacterId);
|
||||||
|
fn response(&mut self, resp: ActionResponse) -> ActionResult;
|
||||||
|
fn execute(&mut self) -> ActionPrompt;
|
||||||
|
fn mark_for_execution(
|
||||||
|
&mut self,
|
||||||
|
target: CharacterId,
|
||||||
|
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8);
|
||||||
|
fn living_villager_excl(&self, excl: PlayerId) -> Character;
|
||||||
|
fn living_villager(&self) -> Character;
|
||||||
|
#[allow(unused)]
|
||||||
|
fn get_state(&mut self) -> ServerToHostMessage;
|
||||||
|
fn next_expect_game_over(&mut self) -> GameOver;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameExt for Game {
|
||||||
|
fn next_expect_game_over(&mut self) -> GameOver {
|
||||||
|
match self
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::GameOver(outcome) => outcome,
|
||||||
|
resp => panic!("expected game to be over, got: {resp:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn get_state(&mut self) -> ServerToHostMessage {
|
||||||
|
self.process(HostGameMessage::GetState).unwrap()
|
||||||
|
}
|
||||||
|
fn living_villager(&self) -> Character {
|
||||||
|
self.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.alive() && matches!(c.role_title(), RoleTitle::Villager))
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn living_villager_excl(&self, excl: PlayerId) -> Character {
|
||||||
|
self.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| {
|
||||||
|
c.alive() && matches!(c.role_title(), RoleTitle::Villager) && c.player_id() != excl
|
||||||
|
})
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn villager_character_ids(&self) -> Box<[CharacterId]> {
|
||||||
|
self.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|c| {
|
||||||
|
(c.alive() && matches!(c.role_title(), RoleTitle::Villager))
|
||||||
|
.then_some(c.character_id())
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn character_by_player_id(&self, player_id: PlayerId) -> Character {
|
||||||
|
self.village()
|
||||||
|
.character_by_player_id(player_id)
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn character_by_character_id(&self, character_id: CharacterId) -> Character {
|
||||||
|
self.village()
|
||||||
|
.character_by_id(character_id)
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn r#continue(&mut self) -> ActionResult {
|
||||||
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Continue,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
.result()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark(&mut self, mark: CharacterId) -> ActionPrompt {
|
||||||
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::MarkTarget(mark),
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
.prompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||||
|
let prompt = self.mark(mark);
|
||||||
|
match prompt {
|
||||||
|
ActionPrompt::Insomniac { .. }
|
||||||
|
| ActionPrompt::MasonsWake { .. }
|
||||||
|
| ActionPrompt::ElderReveal { .. }
|
||||||
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
||||||
|
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
||||||
|
|
||||||
|
ActionPrompt::LoneWolfKill {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Seer {
|
||||||
|
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),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| 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::WolfPackKill {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::AlphaWolf {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::DireWolf {
|
||||||
|
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, .. }
|
||||||
|
| ActionPrompt::Militia { marked: None, .. }
|
||||||
|
| ActionPrompt::MapleWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::Guardian { marked: None, .. }
|
||||||
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||||
|
| ActionPrompt::AlphaWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::DireWolf { marked: None, .. }
|
||||||
|
| ActionPrompt::LoneWolfKill { marked: None, .. } => panic!("no mark"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next(&mut self) -> ActionPrompt {
|
||||||
|
self.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||||
|
.unwrap()
|
||||||
|
.prompt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_expect_day(&mut self) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
|
||||||
|
match self
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::Next))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::Daytime {
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
day,
|
||||||
|
..
|
||||||
|
} => (characters, marked, day),
|
||||||
|
res => panic!("unexpected response to next_expect_day: {res:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn response(&mut self, resp: ActionResponse) -> ActionResult {
|
||||||
|
self.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
resp,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
.result()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_for_execution(
|
||||||
|
&mut self,
|
||||||
|
target: CharacterId,
|
||||||
|
) -> (Box<[CharacterState]>, Box<[CharacterId]>, NonZeroU8) {
|
||||||
|
match self
|
||||||
|
.process(HostGameMessage::Day(HostDayMessage::MarkForExecution(
|
||||||
|
target,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::Daytime {
|
||||||
|
characters,
|
||||||
|
marked,
|
||||||
|
day,
|
||||||
|
..
|
||||||
|
} => (characters, marked, day),
|
||||||
|
res => panic!("unexpected response to mark_for_execution: {res:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn execute(&mut self) -> ActionPrompt {
|
||||||
|
assert_eq!(
|
||||||
|
self.process(HostGameMessage::Day(HostDayMessage::Execute))
|
||||||
|
.unwrap()
|
||||||
|
.prompt(),
|
||||||
|
ActionPrompt::CoverOfDarkness
|
||||||
|
);
|
||||||
|
self.r#continue().r#continue();
|
||||||
|
self.next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -5,9 +5,25 @@ pub mod client {
|
||||||
}
|
}
|
||||||
pub mod host {
|
pub mod host {
|
||||||
mod host;
|
mod host;
|
||||||
|
pub mod story_test;
|
||||||
pub use host::*;
|
pub use host::*;
|
||||||
}
|
}
|
||||||
// mod socket;
|
// mod socket;
|
||||||
|
|
||||||
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/";
|
const DEBUG_URL: &str = "ws://192.168.1.162:8080/connect/";
|
||||||
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/";
|
const LIVE_URL: &str = "wss://wolf.emilis.dev/connect/";
|
||||||
|
|
||||||
|
pub mod story_test {
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::Story;
|
||||||
|
#[function_component]
|
||||||
|
pub fn StoryTest() -> Html {
|
||||||
|
let story = crate::clients::host::story_test::test_story();
|
||||||
|
html! {
|
||||||
|
<div class="content">
|
||||||
|
<Story story={story}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,8 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
|
.then(|| html! {<Button on_click={on_complete}>{"continue"}</Button>});
|
||||||
let body = match &props.result {
|
let body = match &props.result {
|
||||||
ActionResult::PowerSeer { powerful } => {
|
ActionResult::PowerSeer { powerful } => {
|
||||||
let inactive = powerful.not().then_some("inactive");
|
let inactive = powerful.powerful().not().then_some("inactive");
|
||||||
let text = if *powerful {
|
let text = if powerful.powerful() {
|
||||||
"powerful"
|
"powerful"
|
||||||
} else {
|
} else {
|
||||||
"not powerful"
|
"not powerful"
|
||||||
|
|
@ -56,7 +56,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ActionResult::Adjudicator { killer } => {
|
ActionResult::Adjudicator { killer } => {
|
||||||
let text = if *killer {
|
let text = if killer.killer() {
|
||||||
"is a killer"
|
"is a killer"
|
||||||
} else {
|
} else {
|
||||||
"is NOT a killer"
|
"is NOT a killer"
|
||||||
|
|
@ -64,7 +64,7 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<h1>{"your target..."}</h1>
|
<h1>{"your target..."}</h1>
|
||||||
<Icon source={IconSource::Killer} inactive={!*killer}/>
|
<Icon source={IconSource::Killer} inactive={killer.killer().not()}/>
|
||||||
<h2>{text}</h2>
|
<h2>{text}</h2>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
@ -124,8 +124,8 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
}}
|
}}
|
||||||
</>
|
</>
|
||||||
},
|
},
|
||||||
ActionResult::Arcanist { same } => {
|
ActionResult::Arcanist(same) => {
|
||||||
let outcome = if *same {
|
let outcome = if same.same() {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<div class="arcanist-result">
|
<div class="arcanist-result">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
use werewolves_proto::{game::Category, role};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||||
|
pub struct AlignmentSpanProps {
|
||||||
|
pub alignment: role::Alignment,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> Html {
|
||||||
|
let class = match alignment {
|
||||||
|
role::Alignment::Village => "village",
|
||||||
|
role::Alignment::Wolves => "wolves",
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<span class={classes!("attribute-span", "faint", class)}>
|
||||||
|
<div>
|
||||||
|
<Icon source={alignment.icon()} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{alignment.to_string()}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct CategorySpanProps {
|
||||||
|
pub category: Category,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub icon: Option<IconSource>,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub children: Html,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn CategorySpan(
|
||||||
|
CategorySpanProps {
|
||||||
|
category,
|
||||||
|
icon,
|
||||||
|
children,
|
||||||
|
}: &CategorySpanProps,
|
||||||
|
) -> Html {
|
||||||
|
let class = category.class();
|
||||||
|
let icon = icon.unwrap_or(match category {
|
||||||
|
Category::Wolves => IconSource::Wolves,
|
||||||
|
Category::Villager
|
||||||
|
| Category::Intel
|
||||||
|
| Category::Defensive
|
||||||
|
| Category::Offensive
|
||||||
|
| Category::StartsAsVillager => IconSource::Village,
|
||||||
|
});
|
||||||
|
html! {
|
||||||
|
<span class={classes!("attribute-span", "faint", class)}>
|
||||||
|
<div>
|
||||||
|
<Icon source={icon} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{children.clone()}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,35 @@
|
||||||
|
use werewolves_proto::role::AlignmentEq;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconSource, IconType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||||
|
pub struct AlignmentComparisonSpanProps {
|
||||||
|
pub comparison: AlignmentEq,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn AlignmentComparisonSpan(
|
||||||
|
AlignmentComparisonSpanProps { comparison }: &AlignmentComparisonSpanProps,
|
||||||
|
) -> Html {
|
||||||
|
match comparison {
|
||||||
|
AlignmentEq::Same => html! {
|
||||||
|
<span class="alignment-eq">
|
||||||
|
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||||
|
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||||
|
{"the same"}
|
||||||
|
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||||
|
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
AlignmentEq::Different => html! {
|
||||||
|
<span class="alignment-eq">
|
||||||
|
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||||
|
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||||
|
{"different"}
|
||||||
|
<div><Icon source={IconSource::Wolves} icon_type={IconType::Small}/></div>
|
||||||
|
<div><Icon source={IconSource::Village} icon_type={IconType::Small}/></div>
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{
|
||||||
|
diedto::{DiedTo, DiedToTitle},
|
||||||
|
game::{Category, SetupRoleTitle},
|
||||||
|
role,
|
||||||
|
};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{AssociatedIcon, Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||||
|
pub struct DiedToSpanProps {
|
||||||
|
pub died_to: DiedToTitle,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn DiedToSpan(DiedToSpanProps { died_to }: &DiedToSpanProps) -> Html {
|
||||||
|
let class = match died_to {
|
||||||
|
DiedToTitle::Execution => "execution",
|
||||||
|
DiedToTitle::MapleWolfStarved | DiedToTitle::MapleWolf => {
|
||||||
|
SetupRoleTitle::MapleWolf.category().class()
|
||||||
|
}
|
||||||
|
DiedToTitle::Militia => SetupRoleTitle::Militia.category().class(),
|
||||||
|
DiedToTitle::LoneWolf
|
||||||
|
| DiedToTitle::AlphaWolf
|
||||||
|
| DiedToTitle::Shapeshift
|
||||||
|
| DiedToTitle::Wolfpack => SetupRoleTitle::Werewolf.category().class(),
|
||||||
|
DiedToTitle::Hunter => SetupRoleTitle::Hunter.category().class(),
|
||||||
|
DiedToTitle::GuardianProtecting => SetupRoleTitle::Guardian.category().class(),
|
||||||
|
DiedToTitle::PyreMasterLynchMob | DiedToTitle::PyreMaster => {
|
||||||
|
SetupRoleTitle::PyreMaster.category().class()
|
||||||
|
}
|
||||||
|
DiedToTitle::MasonLeaderRecruitFail => SetupRoleTitle::MasonLeader.category().class(),
|
||||||
|
};
|
||||||
|
let icon = died_to.icon().unwrap_or(IconSource::Skull);
|
||||||
|
html! {
|
||||||
|
<span class={classes!("attribute-span", "faint", class)}>
|
||||||
|
<div>
|
||||||
|
<Icon source={icon} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{died_to.to_string().to_case(Case::Title)}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
use werewolves_proto::role::{self, Killer};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{AssociatedIcon, Icon, IconType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||||
|
pub struct KillerSpanProps {
|
||||||
|
pub killer: Killer,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn KillerSpan(KillerSpanProps { killer }: &KillerSpanProps) -> Html {
|
||||||
|
let class = match killer {
|
||||||
|
Killer::Killer => "killer",
|
||||||
|
Killer::NotKiller => "inactive",
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<span class={classes!("attribute-span", "faint")}>
|
||||||
|
<div class={classes!(class)}>
|
||||||
|
<Icon source={killer.icon()} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{killer.to_string()}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
use werewolves_proto::role::Powerful;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{AssociatedIcon, Icon, IconType};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||||
|
pub struct PowerfulSpanProps {
|
||||||
|
pub powerful: Powerful,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn PowerfulSpan(PowerfulSpanProps { powerful }: &PowerfulSpanProps) -> Html {
|
||||||
|
let class = match powerful {
|
||||||
|
Powerful::Powerful => "powerful",
|
||||||
|
Powerful::NotPowerful => "inactive",
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<span class={classes!("attribute-span", "faint")}>
|
||||||
|
<div class={classes!(class)}>
|
||||||
|
<Icon source={powerful.icon()} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{powerful.to_string()}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,44 @@
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{character::Character, game::SetupRole};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct CharacterCardProps {
|
||||||
|
pub char: Character,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn CharacterCard(CharacterCardProps { char }: &CharacterCardProps) -> Html {
|
||||||
|
let class = Into::<SetupRole>::into(char.role_title())
|
||||||
|
.category()
|
||||||
|
.class();
|
||||||
|
let role = char.role_title().to_string().to_case(Case::Title);
|
||||||
|
let ident = char.identity();
|
||||||
|
let pronouns = ident.pronouns.as_ref().map(|p| {
|
||||||
|
html! {
|
||||||
|
<span class="pronouns">{"("}{p}{")"}</span>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let name = ident.name.clone();
|
||||||
|
let source = char.role_title().icon().unwrap_or(if char.is_village() {
|
||||||
|
IconSource::Village
|
||||||
|
} else {
|
||||||
|
IconSource::Wolves
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<span class={classes!("character-span", class)}>
|
||||||
|
<span class={classes!("role", class)}>
|
||||||
|
<div>
|
||||||
|
<Icon source={source} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{role}
|
||||||
|
</span>
|
||||||
|
<span class={classes!("number")}><b>{ident.number.get()}</b></span>
|
||||||
|
<span class="name">{name}</span>
|
||||||
|
{pronouns}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -2,7 +2,7 @@ use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
game::DateTime,
|
game::GameTime,
|
||||||
message::{CharacterState, PublicIdentity},
|
message::{CharacterState, PublicIdentity},
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
@ -41,8 +41,8 @@ pub fn DaytimePlayerList(
|
||||||
.contains(&c.identity.character_id)
|
.contains(&c.identity.character_id)
|
||||||
.then_some(MarkState::Marked),
|
.then_some(MarkState::Marked),
|
||||||
Some(died_to) => match died_to.date_time() {
|
Some(died_to) => match died_to.date_time() {
|
||||||
DateTime::Day { .. } => Some(MarkState::Dead),
|
GameTime::Day { .. } => Some(MarkState::Dead),
|
||||||
DateTime::Night { number } => {
|
GameTime::Night { number } => {
|
||||||
if number == day.get() - 1 {
|
if number == day.get() - 1 {
|
||||||
Some(MarkState::DiedLastNight)
|
Some(MarkState::DiedLastNight)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
||||||
|
|
@ -124,8 +124,8 @@ pub fn SetupCategory(
|
||||||
<span class="count">{count}</span>
|
<span class="count">{count}</span>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let killer_inactive = as_role.killer().not().then_some("inactive");
|
let killer_inactive = as_role.killer().killer().not().then_some("inactive");
|
||||||
let powerful_inactive = as_role.powerful().not().then_some("inactive");
|
let powerful_inactive = as_role.powerful().powerful().not().then_some("inactive");
|
||||||
let alignment = match as_role.alignment() {
|
let alignment = match as_role.alignment() {
|
||||||
Alignment::Village => "/img/village.svg",
|
Alignment::Village => "/img/village.svg",
|
||||||
Alignment::Wolves => "/img/wolf.svg",
|
Alignment::Wolves => "/img/wolf.svg",
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
use werewolves_proto::{
|
||||||
|
diedto::DiedToTitle,
|
||||||
|
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||||
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||||
|
|
@ -6,6 +10,16 @@ pub enum IconSource {
|
||||||
Wolves,
|
Wolves,
|
||||||
Killer,
|
Killer,
|
||||||
Powerful,
|
Powerful,
|
||||||
|
ListItem,
|
||||||
|
Skull,
|
||||||
|
Heart,
|
||||||
|
Shield,
|
||||||
|
ShieldAndSword,
|
||||||
|
Seer,
|
||||||
|
Hunter,
|
||||||
|
MapleWolf,
|
||||||
|
Gravedigger,
|
||||||
|
PowerSeer,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IconSource {
|
impl IconSource {
|
||||||
|
|
@ -15,20 +29,30 @@ impl IconSource {
|
||||||
IconSource::Wolves => "/img/wolf.svg",
|
IconSource::Wolves => "/img/wolf.svg",
|
||||||
IconSource::Killer => "/img/killer.svg",
|
IconSource::Killer => "/img/killer.svg",
|
||||||
IconSource::Powerful => "/img/powerful.svg",
|
IconSource::Powerful => "/img/powerful.svg",
|
||||||
|
IconSource::ListItem => "/img/li.svg",
|
||||||
|
IconSource::Skull => "/img/skull.svg",
|
||||||
|
IconSource::Heart => "/img/heart.svg",
|
||||||
|
IconSource::Shield => "/img/shield.svg",
|
||||||
|
IconSource::ShieldAndSword => "/img/shield-and-sword.svg",
|
||||||
|
IconSource::Seer => "/img/seer.svg",
|
||||||
|
IconSource::Hunter => "/img/hunter.svg",
|
||||||
|
IconSource::MapleWolf => "/img/maple-wolf.svg",
|
||||||
|
IconSource::Gravedigger => "/img/gravedigger.svg",
|
||||||
|
IconSource::PowerSeer => "/img/power-seer.svg",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub const fn class(&self) -> Option<&'static str> {
|
pub const fn class(&self) -> Option<&'static str> {
|
||||||
match self {
|
match self {
|
||||||
IconSource::Village | IconSource::Wolves => None,
|
|
||||||
IconSource::Killer => Some("killer"),
|
IconSource::Killer => Some("killer"),
|
||||||
IconSource::Powerful => Some("powerful"),
|
IconSource::Powerful => Some("powerful"),
|
||||||
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
#[derive(Debug, Clone, Copy, PartialEq, Default)]
|
||||||
pub enum IconType {
|
pub enum IconType {
|
||||||
SetupList,
|
Small,
|
||||||
#[default]
|
#[default]
|
||||||
RoleCheck,
|
RoleCheck,
|
||||||
}
|
}
|
||||||
|
|
@ -36,7 +60,7 @@ pub enum IconType {
|
||||||
impl IconType {
|
impl IconType {
|
||||||
pub const fn class(&self) -> &'static str {
|
pub const fn class(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
IconType::SetupList => "icon",
|
IconType::Small => "icon",
|
||||||
IconType::RoleCheck => "check-icon",
|
IconType::RoleCheck => "check-icon",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -65,3 +89,87 @@ pub fn Icon(
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub trait PartialAssociatedIcon {
|
||||||
|
fn icon(&self) -> Option<IconSource>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait AssociatedIcon {
|
||||||
|
fn icon(&self) -> IconSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssociatedIcon for Alignment {
|
||||||
|
fn icon(&self) -> IconSource {
|
||||||
|
match self {
|
||||||
|
Alignment::Village => IconSource::Village,
|
||||||
|
Alignment::Wolves => IconSource::Wolves,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssociatedIcon for Killer {
|
||||||
|
fn icon(&self) -> IconSource {
|
||||||
|
IconSource::Killer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AssociatedIcon for Powerful {
|
||||||
|
fn icon(&self) -> IconSource {
|
||||||
|
IconSource::Powerful
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialAssociatedIcon for RoleTitle {
|
||||||
|
fn icon(&self) -> Option<IconSource> {
|
||||||
|
Some(match self {
|
||||||
|
RoleTitle::Scapegoat
|
||||||
|
| RoleTitle::Arcanist
|
||||||
|
| RoleTitle::Adjudicator
|
||||||
|
| RoleTitle::Mortician
|
||||||
|
| RoleTitle::Beholder
|
||||||
|
| RoleTitle::MasonLeader
|
||||||
|
| RoleTitle::Diseased
|
||||||
|
| RoleTitle::BlackKnight
|
||||||
|
| RoleTitle::Weightlifter
|
||||||
|
| RoleTitle::PyreMaster
|
||||||
|
| RoleTitle::Militia
|
||||||
|
| RoleTitle::Apprentice
|
||||||
|
| RoleTitle::Elder
|
||||||
|
| RoleTitle::Insomniac
|
||||||
|
| RoleTitle::Werewolf
|
||||||
|
| RoleTitle::AlphaWolf
|
||||||
|
| RoleTitle::DireWolf
|
||||||
|
| RoleTitle::Shapeshifter
|
||||||
|
| RoleTitle::LoneWolf
|
||||||
|
| RoleTitle::Villager => return None,
|
||||||
|
|
||||||
|
RoleTitle::PowerSeer => IconSource::PowerSeer,
|
||||||
|
RoleTitle::Gravedigger => IconSource::Gravedigger,
|
||||||
|
RoleTitle::MapleWolf => IconSource::MapleWolf,
|
||||||
|
RoleTitle::Hunter => IconSource::Hunter,
|
||||||
|
RoleTitle::Empath => IconSource::Heart,
|
||||||
|
RoleTitle::Seer => IconSource::Seer,
|
||||||
|
RoleTitle::Guardian => IconSource::ShieldAndSword,
|
||||||
|
RoleTitle::Vindicator | RoleTitle::Protector => IconSource::Shield,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialAssociatedIcon for DiedToTitle {
|
||||||
|
fn icon(&self) -> Option<IconSource> {
|
||||||
|
match self {
|
||||||
|
DiedToTitle::Execution => Some(IconSource::Skull),
|
||||||
|
DiedToTitle::MapleWolf | DiedToTitle::MapleWolfStarved => Some(IconSource::MapleWolf),
|
||||||
|
DiedToTitle::Militia => Some(IconSource::Killer),
|
||||||
|
DiedToTitle::Wolfpack => None,
|
||||||
|
DiedToTitle::AlphaWolf => None,
|
||||||
|
DiedToTitle::Shapeshift => None,
|
||||||
|
DiedToTitle::Hunter => Some(IconSource::Hunter),
|
||||||
|
DiedToTitle::GuardianProtecting => Some(IconSource::ShieldAndSword),
|
||||||
|
DiedToTitle::PyreMaster => None,
|
||||||
|
DiedToTitle::PyreMasterLynchMob => None,
|
||||||
|
DiedToTitle::MasonLeaderRecruitFail => None,
|
||||||
|
DiedToTitle::LoneWolf => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,543 @@
|
||||||
|
use core::ops::Not;
|
||||||
|
use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{
|
||||||
|
character::{Character, CharacterId},
|
||||||
|
game::{
|
||||||
|
GameTime, SetupRole,
|
||||||
|
night::changes::NightChange,
|
||||||
|
story::{
|
||||||
|
DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
role::Alignment,
|
||||||
|
};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{
|
||||||
|
CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon,
|
||||||
|
attributes::{
|
||||||
|
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct StoryProps {
|
||||||
|
pub story: GameStory,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
||||||
|
let characters = Rc::new(
|
||||||
|
story
|
||||||
|
.starting_village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| (c.character_id(), c))
|
||||||
|
.collect::<HashMap<CharacterId, Character>>(),
|
||||||
|
);
|
||||||
|
let bits = story
|
||||||
|
.iter()
|
||||||
|
.map(|(time, changes)| {
|
||||||
|
let characters = story
|
||||||
|
.village_at(match time {
|
||||||
|
GameTime::Day { .. } => {
|
||||||
|
time
|
||||||
|
},
|
||||||
|
GameTime::Night { .. } => {
|
||||||
|
time.previous().unwrap_or(time)
|
||||||
|
},
|
||||||
|
}).ok().flatten()
|
||||||
|
.map(|v| Rc::new(v.characters().into_iter()
|
||||||
|
.map(|c| (c.character_id(), c))
|
||||||
|
.collect::<HashMap<CharacterId, Character>>())).unwrap_or_else(|| characters.clone());
|
||||||
|
let changes = match changes {
|
||||||
|
GameActions::DayDetails(day_changes) => {
|
||||||
|
let execute_list = if day_changes.is_empty() {
|
||||||
|
html! {<span>{"no one was executed"}</span>}
|
||||||
|
} else {
|
||||||
|
day_changes
|
||||||
|
.iter()
|
||||||
|
.map(|c| match c {
|
||||||
|
DayDetail::Execute(target) => *target,
|
||||||
|
})
|
||||||
|
.filter_map(|c| story.starting_village.character_by_id(c).ok())
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
// <span>
|
||||||
|
<CharacterCard char={c.clone()}/>
|
||||||
|
// </span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>()
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(html! {
|
||||||
|
<div class="day">
|
||||||
|
<h3>{"village executed"}</h3>
|
||||||
|
<div class="executed">
|
||||||
|
{execute_list}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
})
|
||||||
|
}
|
||||||
|
GameActions::NightDetails(details) => details.choices.is_empty().not().then_some({
|
||||||
|
let choices = details
|
||||||
|
.choices
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
<StoryNightChoice choice={c.clone()} characters={characters.clone()}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
let changes = details
|
||||||
|
.changes
|
||||||
|
.iter()
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
<li>
|
||||||
|
<StoryNightChange change={c.clone()} characters={characters.clone()}/>
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<div class="night">
|
||||||
|
<label>{"choices"}</label>
|
||||||
|
<ul class="choices">
|
||||||
|
{choices}
|
||||||
|
</ul>
|
||||||
|
<label>{"changes"}</label>
|
||||||
|
<ul class="changes">
|
||||||
|
{changes}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
|
changes
|
||||||
|
.map(|changes| {
|
||||||
|
html! {
|
||||||
|
<div class="time-period">
|
||||||
|
<h1>{"on "}{time.to_string()}{"..."}</h1>
|
||||||
|
{changes}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
html! {
|
||||||
|
<div class="story">
|
||||||
|
{bits}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct StoryNightChangeProps {
|
||||||
|
change: NightChange,
|
||||||
|
characters: Rc<HashMap<CharacterId, Character>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
||||||
|
match change {
|
||||||
|
NightChange::RoleChange(character_id, role_title) => characters
|
||||||
|
.get(character_id)
|
||||||
|
.map(|char| {
|
||||||
|
let mut new_char = char.clone();
|
||||||
|
let _ = new_char.role_change(*role_title, GameTime::Night { number: 0 });
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={char.clone()}/>
|
||||||
|
{"is now"}
|
||||||
|
<CharacterCard char={new_char.clone()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
|
||||||
|
NightChange::Kill { target, died_to } => characters
|
||||||
|
.get(target)
|
||||||
|
.map(|target| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
||||||
|
<CharacterCard char={target.clone()}/>
|
||||||
|
{"died to"}
|
||||||
|
<DiedToSpan died_to={died_to.title()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
NightChange::RoleBlock { source, target, .. } => characters
|
||||||
|
.get(source)
|
||||||
|
.and_then(|s| characters.get(target).map(|t| (s, t)))
|
||||||
|
.map(|(source, target)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={source.clone()}/>
|
||||||
|
{"role blocked"}
|
||||||
|
<CharacterCard char={target.clone()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
NightChange::Shapeshift { source, into } => characters
|
||||||
|
.get(source)
|
||||||
|
.and_then(|s| characters.get(into).map(|i| (s, i)))
|
||||||
|
.map(|(source, into)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={source.clone()}/>
|
||||||
|
{"shapeshifted into"}
|
||||||
|
<CharacterCard char={into.clone()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
|
||||||
|
NightChange::ElderReveal { elder } => characters
|
||||||
|
.get(elder)
|
||||||
|
.map(|elder| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={elder.clone()}/>
|
||||||
|
{"learned they are the Elder"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
NightChange::EmpathFoundScapegoat { empath, scapegoat } => characters
|
||||||
|
.get(empath)
|
||||||
|
.and_then(|e| characters.get(scapegoat).map(|s| (e, s)))
|
||||||
|
.map(|(empath, scapegoat)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={empath.clone()}/>
|
||||||
|
{"found the scapegoat in"}
|
||||||
|
<CharacterCard char={scapegoat.clone()}/>
|
||||||
|
{"and took on their curse"}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default(),
|
||||||
|
|
||||||
|
NightChange::HunterTarget { .. }
|
||||||
|
| NightChange::MasonRecruit { .. }
|
||||||
|
| NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct StoryNightResultProps {
|
||||||
|
result: StoryActionResult,
|
||||||
|
characters: Rc<HashMap<CharacterId, Character>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
||||||
|
match result {
|
||||||
|
StoryActionResult::RoleBlocked => html! {
|
||||||
|
<span>{"but was role blocked"}</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::Seer(alignment) => {
|
||||||
|
html! {
|
||||||
|
<span>
|
||||||
|
<span>{"and saw"}</span>
|
||||||
|
<AlignmentSpan alignment={*alignment}/>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StoryActionResult::PowerSeer { powerful } => {
|
||||||
|
html! {
|
||||||
|
<span>
|
||||||
|
<span>{"and discovered they are"}</span>
|
||||||
|
<PowerfulSpan powerful={*powerful}/>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StoryActionResult::Adjudicator { killer } => html! {
|
||||||
|
<span>
|
||||||
|
<span>{"and saw"}</span>
|
||||||
|
<KillerSpan killer={*killer}/>
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::Arcanist(same) => html! {
|
||||||
|
<span>
|
||||||
|
<span>{"and saw"}</span>
|
||||||
|
<AlignmentComparisonSpan comparison={*same}/>
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::GraveDigger(None) => html! {
|
||||||
|
<span>
|
||||||
|
{"found an empty grave"}
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::GraveDigger(Some(role_title)) => {
|
||||||
|
let category = Into::<SetupRole>::into(*role_title).category();
|
||||||
|
html! {
|
||||||
|
<span>
|
||||||
|
<span>{"found the body of a"}</span>
|
||||||
|
<CategorySpan category={category} icon={role_title.icon()}>
|
||||||
|
{role_title.to_string().to_case(Case::Title)}
|
||||||
|
</CategorySpan>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
StoryActionResult::Mortician(died_to_title) => html! {
|
||||||
|
<>
|
||||||
|
{"and found the cause of death to be"}
|
||||||
|
<DiedToSpan died_to={*died_to_title}/>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Insomniac { visits } => {
|
||||||
|
let visitors = visits
|
||||||
|
.iter()
|
||||||
|
.filter_map(|c| characters.get(c))
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
<CharacterCard char={c.clone()}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
html! {
|
||||||
|
{visitors}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StoryActionResult::Empath { scapegoat: false } => html! {
|
||||||
|
<>
|
||||||
|
{"and saw that they are"}
|
||||||
|
<span class="attribute-span faint">
|
||||||
|
<div class="inactive">
|
||||||
|
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{"Not The Scapegoat"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
StoryActionResult::Empath { scapegoat: true } => html! {
|
||||||
|
<>
|
||||||
|
{"and saw that they are"}
|
||||||
|
<span class="attribute-span faint wolves">
|
||||||
|
<div>
|
||||||
|
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
{"The Scapegoat"}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct StoryNightChoiceProps {
|
||||||
|
choice: NightChoice,
|
||||||
|
characters: Rc<HashMap<CharacterId, Character>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightChoiceProps) -> Html {
|
||||||
|
let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
||||||
|
characters
|
||||||
|
.get(character_id)
|
||||||
|
.and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
||||||
|
.map(|(char, chosen)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={char.clone()}/>
|
||||||
|
<span>{action}</span>
|
||||||
|
<CharacterCard char={chosen.clone()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let choice_body = match &choice.prompt {
|
||||||
|
StoryActionPrompt::Arcanist {
|
||||||
|
character_id,
|
||||||
|
chosen: (chosen1, chosen2),
|
||||||
|
} => characters
|
||||||
|
.get(character_id)
|
||||||
|
.and_then(|arcanist| characters.get(chosen1).map(|c| (arcanist, c)))
|
||||||
|
.and_then(|(arcanist, chosen1)| {
|
||||||
|
characters
|
||||||
|
.get(chosen2)
|
||||||
|
.map(|chosen2| (arcanist, chosen1, chosen2))
|
||||||
|
})
|
||||||
|
.map(|(arcanist, chosen1, chosen2)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={arcanist.clone()}/>
|
||||||
|
<span>{"compared"}</span>
|
||||||
|
<CharacterCard char={chosen1.clone()}/>
|
||||||
|
<span>{"and"}</span>
|
||||||
|
<CharacterCard char={chosen2.clone()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::MasonsWake { leader, masons } => characters.get(leader).map(|leader| {
|
||||||
|
let masons = masons
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| characters.get(m))
|
||||||
|
.map(|c| {
|
||||||
|
html! {
|
||||||
|
<CharacterCard char={c.clone()}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={leader.clone()}/>
|
||||||
|
<span>{"'s masons"}</span>
|
||||||
|
{masons}
|
||||||
|
<span>{"convened in secret"}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::Vindicator {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
}
|
||||||
|
| StoryActionPrompt::Protector {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "protected"),
|
||||||
|
StoryActionPrompt::Gravedigger {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "dug up"),
|
||||||
|
StoryActionPrompt::Adjudicator {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
}
|
||||||
|
| StoryActionPrompt::PowerSeer {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
}
|
||||||
|
| StoryActionPrompt::Empath {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
}
|
||||||
|
| StoryActionPrompt::Seer {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "checked"),
|
||||||
|
StoryActionPrompt::Hunter {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "set a trap for"),
|
||||||
|
StoryActionPrompt::Militia {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "shot"),
|
||||||
|
StoryActionPrompt::MapleWolf {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => characters
|
||||||
|
.get(character_id)
|
||||||
|
.and_then(|char| characters.get(chosen).map(|c| (char, c)))
|
||||||
|
.map(|(char, chosen)| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={char.clone()}/>
|
||||||
|
<span>{"invited"}</span>
|
||||||
|
<CharacterCard char={chosen.clone()}/>
|
||||||
|
<span>{"for dinner"}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
StoryActionPrompt::Guardian {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
guarding,
|
||||||
|
} => generate(
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
if *guarding { "guarded" } else { "protected" },
|
||||||
|
),
|
||||||
|
StoryActionPrompt::Mortician {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "examined"),
|
||||||
|
StoryActionPrompt::Beholder {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "👁️"),
|
||||||
|
|
||||||
|
StoryActionPrompt::MasonLeaderRecruit {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "tried recruiting"),
|
||||||
|
StoryActionPrompt::PyreMaster {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "torched"),
|
||||||
|
StoryActionPrompt::WolfPackKill { chosen } => {
|
||||||
|
characters.get(chosen).map(|chosen: &Character| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<AlignmentSpan alignment={Alignment::Wolves}/>
|
||||||
|
<span>{"attempted a kill on"}</span>
|
||||||
|
<CharacterCard char={chosen.clone()} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
StoryActionPrompt::Shapeshifter { character_id } => {
|
||||||
|
characters.get(character_id).map(|shifter| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={shifter.clone()} />
|
||||||
|
<span>{"decided to shapeshift into the wolf kill target"}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
StoryActionPrompt::AlphaWolf {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "took a stab at"),
|
||||||
|
StoryActionPrompt::DireWolf {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "roleblocked"),
|
||||||
|
StoryActionPrompt::LoneWolfKill {
|
||||||
|
character_id,
|
||||||
|
chosen,
|
||||||
|
} => generate(character_id, chosen, "sought vengeance from"),
|
||||||
|
StoryActionPrompt::Insomniac { character_id } => {
|
||||||
|
characters.get(character_id).map(|insomniac| {
|
||||||
|
html! {
|
||||||
|
<>
|
||||||
|
<CharacterCard char={insomniac.clone()} />
|
||||||
|
<span>{"witnessed visits from"}</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let result = choice.result.as_ref().map(|result| {
|
||||||
|
html! {
|
||||||
|
<StoryNightResult result={result.clone()} characters={characters.clone()}/>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
choice_body
|
||||||
|
.map(|choice_body| {
|
||||||
|
html! {
|
||||||
|
<li>
|
||||||
|
<Icon source={IconSource::ListItem} icon_type={IconType::Small}/>
|
||||||
|
{choice_body}
|
||||||
|
{result}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,9 @@ mod clients;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod components {
|
mod components {
|
||||||
werewolves_macros::include_path!("werewolves/src/components");
|
werewolves_macros::include_path!("werewolves/src/components");
|
||||||
|
pub mod attributes {
|
||||||
|
werewolves_macros::include_path!("werewolves/src/components/attributes");
|
||||||
|
}
|
||||||
pub mod client {
|
pub mod client {
|
||||||
werewolves_macros::include_path!("werewolves/src/components/client");
|
werewolves_macros::include_path!("werewolves/src/components/client");
|
||||||
}
|
}
|
||||||
|
|
@ -48,12 +51,17 @@ fn main() {
|
||||||
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
|
Callback::from(move |err: Option<WerewolfError>| cb_clone.send_message(err));
|
||||||
|
|
||||||
gloo::utils::document().set_title("werewolves");
|
gloo::utils::document().set_title("werewolves");
|
||||||
|
if path.starts_with("/host/story") {
|
||||||
|
let host = yew::Renderer::<clients::story_test::StoryTest>::with_root(app_element).render();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if path.starts_with("/host") {
|
if path.starts_with("/host") {
|
||||||
let host = yew::Renderer::<Host>::with_root(app_element).render();
|
let host = yew::Renderer::<Host>::with_root(app_element).render();
|
||||||
host.send_message(HostEvent::SetErrorCallback(error_callback));
|
|
||||||
if path.starts_with("/host/big") {
|
if path.starts_with("/host/big") {
|
||||||
host.send_message(HostEvent::SetBigScreenState(true));
|
host.send_message(HostEvent::SetBigScreenState(true));
|
||||||
|
} else {
|
||||||
|
host.send_message(HostEvent::SetErrorCallback(error_callback));
|
||||||
}
|
}
|
||||||
} else if path.starts_with("/many-client") {
|
} else if path.starts_with("/many-client") {
|
||||||
let clients = document.query_selector("clients").unwrap().unwrap();
|
let clients = document.query_selector("clients").unwrap().unwrap();
|
||||||
|
|
|
||||||