aura setup & impl, shift fail screen, bugfixes
This commit is contained in:
parent
f193e4e691
commit
ac4ce81638
|
|
@ -9,7 +9,7 @@ log = { version = "0.4" }
|
||||||
serde_json = { version = "1.0" }
|
serde_json = { version = "1.0" }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||||
rand = { version = "0.9" }
|
rand = { version = "0.9", features = ["std_rng"] }
|
||||||
werewolves-macros = { path = "../werewolves-macros" }
|
werewolves-macros = { path = "../werewolves-macros" }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,3 @@
|
||||||
use core::fmt::Display;
|
|
||||||
|
|
||||||
// Copyright (C) 2025 Emilis Bliūdžius
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -14,33 +12,37 @@ use core::fmt::Display;
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::ChecksAs;
|
use werewolves_macros::{ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
bag::DrunkBag,
|
||||||
game::{GameTime, Village},
|
game::{GameTime, Village},
|
||||||
role::{Alignment, Killer, Powerful},
|
role::{Alignment, Killer, Powerful},
|
||||||
team::Team,
|
team::Team,
|
||||||
};
|
};
|
||||||
const BLOODLET_DURATION_DAYS: u8 = 2;
|
const BLOODLET_DURATION_DAYS: u8 = 2;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ChecksAs)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs, Titles)]
|
||||||
pub enum Aura {
|
pub enum Aura {
|
||||||
|
#[checks("assignable")]
|
||||||
Traitor,
|
Traitor,
|
||||||
|
#[checks("assignable")]
|
||||||
#[checks("cleansible")]
|
#[checks("cleansible")]
|
||||||
Drunk,
|
Drunk(DrunkBag),
|
||||||
|
#[checks("assignable")]
|
||||||
Insane,
|
Insane,
|
||||||
#[checks("cleansible")]
|
#[checks("cleansible")]
|
||||||
Bloodlet {
|
Bloodlet { night: u8 },
|
||||||
night: u8,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Aura {
|
impl Display for Aura {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
Aura::Traitor => "Traitor",
|
Aura::Traitor => "Traitor",
|
||||||
Aura::Drunk => "Drunk",
|
Aura::Drunk(_) => "Drunk",
|
||||||
Aura::Insane => "Insane",
|
Aura::Insane => "Insane",
|
||||||
Aura::Bloodlet { .. } => "Bloodlet",
|
Aura::Bloodlet { .. } => "Bloodlet",
|
||||||
})
|
})
|
||||||
|
|
@ -50,7 +52,7 @@ impl Display for Aura {
|
||||||
impl Aura {
|
impl Aura {
|
||||||
pub const fn expired(&self, village: &Village) -> bool {
|
pub const fn expired(&self, village: &Village) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Aura::Traitor | Aura::Drunk | Aura::Insane => false,
|
Aura::Traitor | Aura::Drunk(_) | Aura::Insane => false,
|
||||||
Aura::Bloodlet {
|
Aura::Bloodlet {
|
||||||
night: applied_night,
|
night: applied_night,
|
||||||
} => match village.time() {
|
} => match village.time() {
|
||||||
|
|
@ -88,8 +90,16 @@ impl Auras {
|
||||||
&self.0
|
&self.0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_aura(&mut self, aura: Aura) {
|
pub fn list_mut(&mut self) -> &mut [Aura] {
|
||||||
self.0.retain(|a| *a != aura);
|
&mut self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_aura(&mut self, aura: AuraTitle) {
|
||||||
|
self.0.retain(|a| a.title() != aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_mut(&mut self, aura: AuraTitle) -> Option<&mut Aura> {
|
||||||
|
self.0.iter_mut().find(|a| a.title() == aura)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// purges expired [Aura]s and returns the ones that were removed
|
/// purges expired [Aura]s and returns the ones that were removed
|
||||||
|
|
@ -129,7 +139,7 @@ impl Auras {
|
||||||
match aura {
|
match aura {
|
||||||
Aura::Traitor => return Some(Alignment::Traitor),
|
Aura::Traitor => return Some(Alignment::Traitor),
|
||||||
Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
||||||
Aura::Drunk | Aura::Insane => {}
|
Aura::Drunk(_) | Aura::Insane => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -151,3 +161,14 @@ impl Auras {
|
||||||
.then_some(Powerful::Powerful)
|
.then_some(Powerful::Powerful)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl AuraTitle {
|
||||||
|
pub fn into_aura(self) -> Aura {
|
||||||
|
match self {
|
||||||
|
AuraTitle::Traitor => Aura::Traitor,
|
||||||
|
AuraTitle::Drunk => Aura::Drunk(DrunkBag::default()),
|
||||||
|
AuraTitle::Insane => Aura::Insane,
|
||||||
|
AuraTitle::Bloodlet => Aura::Bloodlet { night: 0 },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,160 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use rand::{SeedableRng, rngs::SmallRng, seq::SliceRandom};
|
||||||
|
// #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||||
|
// pub enum BagItem<T, V> {
|
||||||
|
// Left(T),
|
||||||
|
// Right(V),
|
||||||
|
// }
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Bag<T>(Vec<T>);
|
||||||
|
|
||||||
|
impl<T> Bag<T> {
|
||||||
|
pub fn new(items: impl IntoIterator<Item = T>) -> Self {
|
||||||
|
Self(items.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull(&mut self) -> Option<T> {
|
||||||
|
self.0.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek(&self) -> Option<&T> {
|
||||||
|
self.0.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn len(&self) -> usize {
|
||||||
|
self.0.len()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
|
||||||
|
pub enum DrunkRoll {
|
||||||
|
Drunk,
|
||||||
|
#[default]
|
||||||
|
Sober,
|
||||||
|
}
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize)]
|
||||||
|
pub struct DrunkBag {
|
||||||
|
#[serde(skip)]
|
||||||
|
rng: SmallRng,
|
||||||
|
seed: u64,
|
||||||
|
bag_number: usize,
|
||||||
|
bag: Bag<DrunkRoll>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'de> Deserialize<'de> for DrunkBag {
|
||||||
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
|
where
|
||||||
|
D: serde::Deserializer<'de>,
|
||||||
|
{
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DrunkBagNoRng {
|
||||||
|
seed: u64,
|
||||||
|
bag_number: usize,
|
||||||
|
bag: Bag<DrunkRoll>,
|
||||||
|
}
|
||||||
|
let DrunkBagNoRng {
|
||||||
|
seed,
|
||||||
|
bag_number,
|
||||||
|
bag,
|
||||||
|
} = DrunkBagNoRng::deserialize(deserializer)?;
|
||||||
|
let mut rng = SmallRng::seed_from_u64(seed);
|
||||||
|
// Shuffle the default bag bag_number of times to get the smallrng to the same state
|
||||||
|
for _ in 0..bag_number {
|
||||||
|
Self::DEFAULT_BAG
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.collect::<Box<[_]>>()
|
||||||
|
.shuffle(&mut rng);
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
rng,
|
||||||
|
seed,
|
||||||
|
bag_number,
|
||||||
|
bag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DrunkBag {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DrunkBag {
|
||||||
|
const DEFAULT_BAG: &[DrunkRoll] = &[
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Sober,
|
||||||
|
DrunkRoll::Sober,
|
||||||
|
DrunkRoll::Sober,
|
||||||
|
];
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub fn all_drunk() -> Self {
|
||||||
|
Self {
|
||||||
|
rng: SmallRng::seed_from_u64(0),
|
||||||
|
seed: 0,
|
||||||
|
bag_number: 1,
|
||||||
|
bag: Bag::new([
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
DrunkRoll::Drunk,
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let seed = rand::random();
|
||||||
|
let mut rng = SmallRng::seed_from_u64(seed);
|
||||||
|
let mut starting_bag = Self::DEFAULT_BAG.iter().copied().collect::<Box<[_]>>();
|
||||||
|
starting_bag.shuffle(&mut rng);
|
||||||
|
let bag = Bag::new(starting_bag);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
rng,
|
||||||
|
seed,
|
||||||
|
bag,
|
||||||
|
bag_number: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn peek(&self) -> DrunkRoll {
|
||||||
|
self.bag.peek().copied().unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_bag(&mut self) {
|
||||||
|
let mut starting_bag = Self::DEFAULT_BAG.iter().copied().collect::<Box<[_]>>();
|
||||||
|
starting_bag.shuffle(&mut self.rng);
|
||||||
|
self.bag = Bag::new(starting_bag);
|
||||||
|
self.bag_number += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pull(&mut self) -> DrunkRoll {
|
||||||
|
if self.bag.len() < 2 {
|
||||||
|
*self = Self::new();
|
||||||
|
} else if self.bag.len() == 2 {
|
||||||
|
let pulled = self.bag.pull().unwrap_or_default();
|
||||||
|
self.next_bag();
|
||||||
|
return pulled;
|
||||||
|
}
|
||||||
|
self.bag.pull().unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::{Aura, Auras},
|
aura::{Aura, AuraTitle, Auras},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameTime, Village},
|
game::{GameTime, Village},
|
||||||
|
|
@ -207,6 +207,10 @@ impl Character {
|
||||||
self.auras.list()
|
self.auras.list()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn auras_mut(&mut self) -> &mut [Aura] {
|
||||||
|
self.auras.list_mut()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> 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);
|
||||||
|
|
@ -304,7 +308,7 @@ impl Character {
|
||||||
self.auras.add(aura);
|
self.auras.add(aura);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_aura(&mut self, aura: Aura) {
|
pub fn remove_aura(&mut self, aura: AuraTitle) {
|
||||||
self.auras.remove_aura(aura);
|
self.auras.remove_aura(aura);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -319,7 +323,13 @@ impl Character {
|
||||||
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
GameTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
Ok(Box::new([match &self.role {
|
let mut prompts = Vec::new();
|
||||||
|
if night == 0 && self.auras.list().contains(&Aura::Traitor) {
|
||||||
|
prompts.push(ActionPrompt::TraitorIntro {
|
||||||
|
character_id: self.identity(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
match &self.role {
|
||||||
Role::Empath { cursed: true }
|
Role::Empath { cursed: true }
|
||||||
| Role::Diseased
|
| Role::Diseased
|
||||||
| Role::Weightlifter
|
| Role::Weightlifter
|
||||||
|
|
@ -334,11 +344,11 @@ impl Character {
|
||||||
woken_for_reveal: true,
|
woken_for_reveal: true,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| Role::Villager => return Ok(Box::new([])),
|
| Role::Villager => {}
|
||||||
|
|
||||||
Role::Insomniac => ActionPrompt::Insomniac {
|
Role::Insomniac => prompts.push(ActionPrompt::Insomniac {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
},
|
}),
|
||||||
|
|
||||||
Role::Scapegoat { redeemed: true } => {
|
Role::Scapegoat { redeemed: true } => {
|
||||||
let mut dead = village.dead_characters();
|
let mut dead = village.dead_characters();
|
||||||
|
|
@ -347,99 +357,94 @@ impl Character {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|d| (d.is_village() && d.is_power_role()).then_some(d.role_title()))
|
.find_map(|d| (d.is_village() && d.is_power_role()).then_some(d.role_title()))
|
||||||
{
|
{
|
||||||
ActionPrompt::RoleChange {
|
prompts.push(ActionPrompt::RoleChange {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
new_role: pr,
|
new_role: pr,
|
||||||
}
|
});
|
||||||
} else {
|
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Role::Bloodletter => ActionPrompt::Bloodletter {
|
Role::Bloodletter => prompts.push(ActionPrompt::Bloodletter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_villagers(),
|
living_players: village.living_villagers(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Seer => ActionPrompt::Seer {
|
Role::Seer => prompts.push(ActionPrompt::Seer {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Arcanist => ActionPrompt::Arcanist {
|
Role::Arcanist => prompts.push(ActionPrompt::Arcanist {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: (None, None),
|
marked: (None, None),
|
||||||
},
|
}),
|
||||||
Role::Protector {
|
Role::Protector {
|
||||||
last_protected: Some(last_protected),
|
last_protected: Some(last_protected),
|
||||||
} => ActionPrompt::Protector {
|
} => prompts.push(ActionPrompt::Protector {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
targets: village.living_players_excluding(*last_protected),
|
targets: village.living_players_excluding(*last_protected),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Protector {
|
Role::Protector {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
} => ActionPrompt::Protector {
|
} => prompts.push(ActionPrompt::Protector {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
targets: village.living_players_excluding(self.character_id()),
|
targets: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
|
}),
|
||||||
|
Role::Apprentice(role) => match village.time() {
|
||||||
|
GameTime::Day { number: _ } => {}
|
||||||
|
GameTime::Night {
|
||||||
|
number: current_night,
|
||||||
|
} => {
|
||||||
|
if village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| c.role_title() == *role)
|
||||||
|
.filter_map(|char| char.died_to)
|
||||||
|
.any(|died_to| match died_to.date_time() {
|
||||||
|
GameTime::Day { number } => number.get() + 1 >= current_night,
|
||||||
|
GameTime::Night { number } => number + 1 >= current_night,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
prompts.push(ActionPrompt::RoleChange {
|
||||||
|
character_id: self.identity(),
|
||||||
|
new_role: *role,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
Role::Apprentice(role) => {
|
|
||||||
let current_night = match village.time() {
|
|
||||||
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
|
||||||
GameTime::Night { number } => number,
|
|
||||||
};
|
|
||||||
return Ok(village
|
|
||||||
.characters()
|
|
||||||
.into_iter()
|
|
||||||
.filter(|c| c.role_title() == *role)
|
|
||||||
.filter_map(|char| char.died_to)
|
|
||||||
.any(|died_to| match died_to.date_time() {
|
|
||||||
GameTime::Day { number } => number.get() + 1 >= current_night,
|
|
||||||
GameTime::Night { number } => number + 1 >= current_night,
|
|
||||||
})
|
|
||||||
.then(|| ActionPrompt::RoleChange {
|
|
||||||
character_id: self.identity(),
|
|
||||||
new_role: *role,
|
|
||||||
})
|
|
||||||
.into_iter()
|
|
||||||
.collect());
|
|
||||||
}
|
|
||||||
Role::Elder {
|
Role::Elder {
|
||||||
knows_on_night,
|
knows_on_night,
|
||||||
woken_for_reveal: false,
|
woken_for_reveal: false,
|
||||||
..
|
..
|
||||||
} => {
|
} => match village.time() {
|
||||||
let current_night = match village.time() {
|
GameTime::Day { number: _ } => {}
|
||||||
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
GameTime::Night { number } => {
|
||||||
GameTime::Night { number } => number,
|
if number >= knows_on_night.get() {
|
||||||
};
|
prompts.push(ActionPrompt::ElderReveal {
|
||||||
return Ok((current_night >= knows_on_night.get())
|
|
||||||
.then_some({
|
|
||||||
ActionPrompt::ElderReveal {
|
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
}
|
});
|
||||||
})
|
}
|
||||||
.into_iter()
|
}
|
||||||
.collect());
|
},
|
||||||
}
|
Role::Militia { targeted: None } => prompts.push(ActionPrompt::Militia {
|
||||||
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Werewolf => ActionPrompt::WolfPackKill {
|
Role::Werewolf => prompts.push(ActionPrompt::WolfPackKill {
|
||||||
living_villagers: village.living_players(),
|
living_villagers: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
Role::AlphaWolf { killed: None } => prompts.push(ActionPrompt::AlphaWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_villagers: village.living_players_excluding(self.character_id()),
|
living_villagers: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::DireWolf {
|
Role::DireWolf {
|
||||||
last_blocked: Some(last_blocked),
|
last_blocked: Some(last_blocked),
|
||||||
} => ActionPrompt::DireWolf {
|
} => prompts.push(ActionPrompt::DireWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village
|
living_players: village
|
||||||
.living_players_excluding(self.character_id())
|
.living_players_excluding(self.character_id())
|
||||||
|
|
@ -447,137 +452,124 @@ impl Character {
|
||||||
.filter(|c| c.character_id != *last_blocked)
|
.filter(|c| c.character_id != *last_blocked)
|
||||||
.collect(),
|
.collect(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::DireWolf { .. } => ActionPrompt::DireWolf {
|
Role::DireWolf { .. } => prompts.push(ActionPrompt::DireWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
Role::Shapeshifter { shifted_into: None } => prompts.push(ActionPrompt::Shapeshifter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
},
|
}),
|
||||||
Role::Gravedigger => {
|
Role::Gravedigger => {
|
||||||
let dead = village.dead_targets();
|
let dead = village.dead_targets();
|
||||||
if dead.is_empty() {
|
if !dead.is_empty() {
|
||||||
return Ok(Box::new([]));
|
prompts.push(ActionPrompt::Gravedigger {
|
||||||
}
|
character_id: self.identity(),
|
||||||
ActionPrompt::Gravedigger {
|
dead_players: village.dead_targets(),
|
||||||
character_id: self.identity(),
|
marked: None,
|
||||||
dead_players: village.dead_targets(),
|
});
|
||||||
marked: None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
Role::Hunter { target } => prompts.push(ActionPrompt::Hunter {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()),
|
current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
|
Role::MapleWolf { last_kill_on_night } => prompts.push(ActionPrompt::MapleWolf {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||||
} => ActionPrompt::Guardian {
|
} => prompts.push(ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||||
living_players: village.living_players_excluding(prev_target.character_id),
|
living_players: village.living_players_excluding(prev_target.character_id),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
||||||
} => ActionPrompt::Guardian {
|
} => prompts.push(ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
||||||
living_players: village.living_players(),
|
living_players: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Guardian {
|
Role::Guardian {
|
||||||
last_protected: None,
|
last_protected: None,
|
||||||
} => ActionPrompt::Guardian {
|
} => prompts.push(ActionPrompt::Guardian {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
previous: None,
|
previous: None,
|
||||||
living_players: village.living_players(),
|
living_players: village.living_players(),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Adjudicator => ActionPrompt::Adjudicator {
|
Role::Adjudicator => prompts.push(ActionPrompt::Adjudicator {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::PowerSeer => ActionPrompt::PowerSeer {
|
Role::PowerSeer => prompts.push(ActionPrompt::PowerSeer {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Mortician => ActionPrompt::Mortician {
|
Role::Mortician => {
|
||||||
character_id: self.identity(),
|
let dead = village.dead_targets();
|
||||||
dead_players: {
|
if !dead.is_empty() {
|
||||||
let dead = village.dead_targets();
|
prompts.push(ActionPrompt::Mortician {
|
||||||
if dead.is_empty() {
|
character_id: self.identity(),
|
||||||
return Ok(Box::new([]));
|
dead_players: dead,
|
||||||
}
|
marked: None,
|
||||||
dead
|
});
|
||||||
},
|
}
|
||||||
marked: None,
|
}
|
||||||
},
|
Role::Beholder => prompts.push(ActionPrompt::Beholder {
|
||||||
Role::Beholder => ActionPrompt::Beholder {
|
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::MasonLeader { .. } => {
|
Role::MasonLeader { .. } => {
|
||||||
log::error!(
|
log::error!(
|
||||||
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
"night_action_prompts got to MasonLeader, should be handled before the living check"
|
||||||
);
|
);
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
}
|
||||||
Role::Empath { cursed: false } => ActionPrompt::Empath {
|
Role::Empath { cursed: false } => prompts.push(ActionPrompt::Empath {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::Vindicator => {
|
Role::Vindicator => {
|
||||||
let last_day = match village.time() {
|
if night != 0
|
||||||
GameTime::Day { .. } => {
|
&& let Some(last_day) = NonZeroU8::new(night)
|
||||||
log::error!(
|
&& village
|
||||||
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
.executions_on_day(last_day)
|
||||||
);
|
.iter()
|
||||||
return Ok(Box::new([]));
|
.any(|c| c.is_village())
|
||||||
}
|
{
|
||||||
GameTime::Night { number } => {
|
prompts.push(ActionPrompt::Vindicator {
|
||||||
if number == 0 {
|
|
||||||
return Ok(Box::new([]));
|
|
||||||
}
|
|
||||||
NonZeroU8::new(number).unwrap()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
return Ok(village
|
|
||||||
.executions_on_day(last_day)
|
|
||||||
.iter()
|
|
||||||
.any(|c| c.is_village())
|
|
||||||
.then(|| ActionPrompt::Vindicator {
|
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
})
|
});
|
||||||
.into_iter()
|
}
|
||||||
.collect());
|
|
||||||
}
|
}
|
||||||
Role::PyreMaster { .. } => ActionPrompt::PyreMaster {
|
|
||||||
|
Role::PyreMaster { .. } => prompts.push(ActionPrompt::PyreMaster {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
Role::LoneWolf => ActionPrompt::LoneWolfKill {
|
Role::LoneWolf => prompts.push(ActionPrompt::LoneWolfKill {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
marked: None,
|
marked: None,
|
||||||
},
|
}),
|
||||||
}]))
|
}
|
||||||
|
Ok(prompts.into_boxed_slice())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
@ -830,6 +822,28 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const fn maple_wolf_mut<'a>(&'a mut self) -> Result<MapleWolfMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::MapleWolf { last_kill_on_night } => Ok(MapleWolfMut(last_kill_on_night)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::MapleWolf,
|
||||||
|
got: title,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn protector_mut<'a>(&'a mut self) -> Result<ProtectorMut<'a>> {
|
||||||
|
let title = self.role.title();
|
||||||
|
match &mut self.role {
|
||||||
|
Role::Protector { last_protected } => Ok(ProtectorMut(last_protected)),
|
||||||
|
_ => Err(GameError::InvalidRole {
|
||||||
|
expected: RoleTitle::Protector,
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|
@ -872,6 +886,8 @@ decl_ref_and_mut!(
|
||||||
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
||||||
Direwolf, DirewolfMut: Option<CharacterId>;
|
Direwolf, DirewolfMut: Option<CharacterId>;
|
||||||
Militia, MilitiaMut: Option<CharacterId>;
|
Militia, MilitiaMut: Option<CharacterId>;
|
||||||
|
MapleWolf, MapleWolfMut: u8;
|
||||||
|
Protector, ProtectorMut: Option<CharacterId>;
|
||||||
);
|
);
|
||||||
|
|
||||||
pub struct BlackKnightKill<'a> {
|
pub struct BlackKnightKill<'a> {
|
||||||
|
|
|
||||||
|
|
@ -97,4 +97,6 @@ pub enum GameError {
|
||||||
NoPreviousDuringDay,
|
NoPreviousDuringDay,
|
||||||
#[error("militia already spent")]
|
#[error("militia already spent")]
|
||||||
MilitiaSpent,
|
MilitiaSpent,
|
||||||
|
#[error("this role doesn't mark anyone")]
|
||||||
|
RoleDoesntMark,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,6 +72,15 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub const fn village_mut(&mut self) -> &mut Village {
|
||||||
|
match &mut self.state {
|
||||||
|
GameState::Day { village, marked: _ } => village,
|
||||||
|
GameState::Night { night } => night.village_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
pub fn process(&mut self, message: HostGameMessage) -> Result<ServerToHostMessage> {
|
||||||
match (&mut self.state, message) {
|
match (&mut self.state, message) {
|
||||||
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
|
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
|
||||||
|
|
@ -102,7 +111,6 @@ impl Game {
|
||||||
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
||||||
let time = village.time();
|
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(
|
self.history.add(
|
||||||
village.time(),
|
village.time(),
|
||||||
GameActions::DayDetails(
|
GameActions::DayDetails(
|
||||||
|
|
@ -112,7 +120,6 @@ impl Game {
|
||||||
return Ok(ServerToHostMessage::GameOver(outcome));
|
return Ok(ServerToHostMessage::GameOver(outcome));
|
||||||
}
|
}
|
||||||
let night = Night::new(village.clone())?;
|
let night = Night::new(village.clone())?;
|
||||||
log::warn!("adding to history for {time}");
|
|
||||||
self.history.add(
|
self.history.add(
|
||||||
time,
|
time,
|
||||||
GameActions::DayDetails(
|
GameActions::DayDetails(
|
||||||
|
|
@ -163,7 +170,6 @@ impl Game {
|
||||||
Err(GameError::NightOver) => {
|
Err(GameError::NightOver) => {
|
||||||
let changes = night.collect_changes()?;
|
let changes = night.collect_changes()?;
|
||||||
let village = night.village().with_night_changes(&changes)?;
|
let village = night.village().with_night_changes(&changes)?;
|
||||||
log::warn!("adding to history for {}", night.village().time());
|
|
||||||
self.history.add(
|
self.history.add(
|
||||||
night.village().time(),
|
night.village().time(),
|
||||||
GameActions::NightDetails(NightDetails::new(
|
GameActions::NightDetails(NightDetails::new(
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
pub mod changes;
|
pub mod changes;
|
||||||
|
mod next;
|
||||||
mod process;
|
mod process;
|
||||||
|
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
@ -58,7 +59,8 @@ impl From<ActionComplete> for ResponseOutcome {
|
||||||
impl ActionPrompt {
|
impl ActionPrompt {
|
||||||
fn unless(&self) -> Option<Unless> {
|
fn unless(&self) -> Option<Unless> {
|
||||||
match &self {
|
match &self {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
|
@ -532,13 +534,7 @@ impl Night {
|
||||||
// role change associated with the shapeshift
|
// role change associated with the shapeshift
|
||||||
self.action_queue.push_front(last_prompt);
|
self.action_queue.push_front(last_prompt);
|
||||||
}
|
}
|
||||||
log::warn!(
|
|
||||||
"next prompts: {:?}",
|
|
||||||
self.action_queue
|
|
||||||
.iter()
|
|
||||||
.map(ActionPrompt::title)
|
|
||||||
.collect::<Box<[_]>>()
|
|
||||||
);
|
|
||||||
*current_result = CurrentResult::None;
|
*current_result = CurrentResult::None;
|
||||||
*current_changes = Vec::new();
|
*current_changes = Vec::new();
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
@ -592,7 +588,7 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<()> {
|
fn apply_shapeshift(&mut self, source: &CharacterId) -> Result<Option<ActionResult>> {
|
||||||
if let Some(kill_target) = self
|
if let Some(kill_target) = self
|
||||||
.changes_from_actions()
|
.changes_from_actions()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
|
@ -616,7 +612,7 @@ impl Night {
|
||||||
_ => false,
|
_ => false,
|
||||||
}) {
|
}) {
|
||||||
// there is protection, so the kill doesn't happen -> no shapeshift
|
// there is protection, so the kill doesn't happen -> no shapeshift
|
||||||
return Ok(());
|
return Ok(Some(ActionResult::ShiftFailed));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.changes_from_actions().into_iter().any(|c| {
|
if self.changes_from_actions().into_iter().any(|c| {
|
||||||
|
|
@ -667,7 +663,7 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.action_queue = new_queue;
|
self.action_queue = new_queue;
|
||||||
Ok(())
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn page(&self) -> Option<usize> {
|
pub const fn page(&self) -> Option<usize> {
|
||||||
|
|
@ -681,8 +677,8 @@ impl Night {
|
||||||
match &mut self.night_state {
|
match &mut self.night_state {
|
||||||
NightState::Active { current_result, .. } => match current_result {
|
NightState::Active { current_result, .. } => match current_result {
|
||||||
CurrentResult::None => self.received_response(ActionResponse::Continue),
|
CurrentResult::None => self.received_response(ActionResponse::Continue),
|
||||||
CurrentResult::Result(ActionResult::Continue)
|
CurrentResult::GoBackToSleepAfterShown { .. }
|
||||||
| CurrentResult::GoBackToSleepAfterShown { .. }
|
| CurrentResult::Result(ActionResult::Continue)
|
||||||
| CurrentResult::Result(ActionResult::GoBackToSleep) => {
|
| CurrentResult::Result(ActionResult::GoBackToSleep) => {
|
||||||
Err(GameError::NightNeedsNext)
|
Err(GameError::NightNeedsNext)
|
||||||
}
|
}
|
||||||
|
|
@ -697,6 +693,18 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_current_result(&mut self, result: CurrentResult) -> Result<()> {
|
||||||
|
match &mut self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt: _,
|
||||||
|
current_result,
|
||||||
|
..
|
||||||
|
} => *current_result = result,
|
||||||
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
pub fn received_response(&mut self, resp: ActionResponse) -> Result<ServerAction> {
|
||||||
match self.received_response_with_role_blocks(resp)? {
|
match self.received_response_with_role_blocks(resp)? {
|
||||||
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
||||||
|
|
@ -715,18 +723,18 @@ impl Night {
|
||||||
NightState::Complete => Err(GameError::NightOver),
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
},
|
},
|
||||||
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
||||||
match &mut self.night_state {
|
self.set_current_result(result.clone().into())?;
|
||||||
NightState::Active {
|
|
||||||
current_prompt: _,
|
|
||||||
current_result,
|
|
||||||
..
|
|
||||||
} => *current_result = result.clone().into(),
|
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
|
||||||
};
|
|
||||||
if let NightChange::Shapeshift { source, .. } = &change {
|
if let NightChange::Shapeshift { source, .. } = &change {
|
||||||
// needs to be resolved _now_ so that the target can be woken
|
// needs to be resolved _now_ so that the target can be woken
|
||||||
// for the role change with the wolves
|
// for the role change with the wolves
|
||||||
self.apply_shapeshift(source)?;
|
if let Some(result) = self.apply_shapeshift(source)? {
|
||||||
|
if let NightState::Active { current_result, .. } = &mut self.night_state {
|
||||||
|
*current_result = CurrentResult::None;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Ok(ServerAction::Result(result));
|
||||||
|
}
|
||||||
|
|
||||||
return Ok(ServerAction::Result(
|
return Ok(ServerAction::Result(
|
||||||
self.action_queue
|
self.action_queue
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -766,10 +774,10 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn received_response_consecutive_same_player_no_sleep(
|
fn outcome_consecutive_same_player_no_sleep(
|
||||||
&self,
|
&self,
|
||||||
resp: ActionResponse,
|
outcome: ResponseOutcome,
|
||||||
) -> Result<ResponseOutcome> {
|
) -> ResponseOutcome {
|
||||||
let same_char = self
|
let same_char = self
|
||||||
.current_character_id()
|
.current_character_id()
|
||||||
.and_then(|curr| {
|
.and_then(|curr| {
|
||||||
|
|
@ -780,25 +788,31 @@ impl Night {
|
||||||
})
|
})
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
match (
|
match (outcome, same_char) {
|
||||||
self.received_response_consecutive_wolves_dont_sleep(resp)?,
|
(ResponseOutcome::PromptUpdate(p), _) => ResponseOutcome::PromptUpdate(p),
|
||||||
same_char,
|
|
||||||
) {
|
|
||||||
(ResponseOutcome::PromptUpdate(p), _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
|
||||||
(
|
(
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change,
|
change,
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
) => ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change,
|
change,
|
||||||
})),
|
}),
|
||||||
(act, _) => Ok(act),
|
(act, _) => act,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn received_response_consecutive_same_player_no_sleep(
|
||||||
|
&self,
|
||||||
|
resp: ActionResponse,
|
||||||
|
) -> Result<ResponseOutcome> {
|
||||||
|
Ok(self.outcome_consecutive_same_player_no_sleep(
|
||||||
|
self.received_response_consecutive_wolves_dont_sleep(resp)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
fn received_response_consecutive_wolves_dont_sleep(
|
fn received_response_consecutive_wolves_dont_sleep(
|
||||||
&self,
|
&self,
|
||||||
resp: ActionResponse,
|
resp: ActionResponse,
|
||||||
|
|
@ -826,7 +840,11 @@ impl Night {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
match (self.process(resp)?, current_wolfy, next_wolfy) {
|
match (
|
||||||
|
self.received_response_with_auras(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 {
|
||||||
|
|
@ -911,6 +929,12 @@ impl Night {
|
||||||
&self.village
|
&self.village
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
#[doc(hidden)]
|
||||||
|
pub const fn village_mut(&mut self) -> &mut Village {
|
||||||
|
&mut self.village
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn current_result(&self) -> Option<&ActionResult> {
|
pub const fn current_result(&self) -> Option<&ActionResult> {
|
||||||
match &self.night_state {
|
match &self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
|
|
@ -957,6 +981,11 @@ impl Night {
|
||||||
.and_then(|id| self.village.character_by_id(id).ok())
|
.and_then(|id| self.village.character_by_id(id).ok())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn current_character_mut(&mut self) -> Option<&mut Character> {
|
||||||
|
self.current_character_id()
|
||||||
|
.and_then(|id| self.village.character_by_id_mut(id).ok())
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn complete(&self) -> bool {
|
pub const fn complete(&self) -> bool {
|
||||||
matches!(self.night_state, NightState::Complete)
|
matches!(self.night_state, NightState::Complete)
|
||||||
}
|
}
|
||||||
|
|
@ -968,83 +997,21 @@ impl Night {
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::should_implement_trait)]
|
pub fn append_change(&mut self, change: NightChange) -> Result<()> {
|
||||||
pub fn next(&mut self) -> Result<()> {
|
match &mut self.night_state {
|
||||||
match &self.night_state {
|
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt,
|
current_changes, ..
|
||||||
current_result: CurrentResult::Result(ActionResult::Continue),
|
|
||||||
current_changes,
|
|
||||||
..
|
|
||||||
} => {
|
} => {
|
||||||
self.used_actions.push((
|
current_changes.push(change);
|
||||||
current_prompt.clone(),
|
Ok(())
|
||||||
ActionResult::Continue,
|
|
||||||
current_changes.clone(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
NightState::Active {
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
current_prompt,
|
|
||||||
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
|
|
||||||
current_changes,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.used_actions.push((
|
|
||||||
current_prompt.clone(),
|
|
||||||
ActionResult::GoBackToSleep,
|
|
||||||
current_changes.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
NightState::Active {
|
|
||||||
current_result: CurrentResult::Result(_),
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
// needs Continue, not Next
|
|
||||||
return Err(GameError::AwaitingResponse);
|
|
||||||
}
|
|
||||||
NightState::Active {
|
|
||||||
current_prompt,
|
|
||||||
current_result: CurrentResult::GoBackToSleepAfterShown { result_with_data },
|
|
||||||
current_changes,
|
|
||||||
..
|
|
||||||
} => {
|
|
||||||
self.used_actions.push((
|
|
||||||
current_prompt.clone(),
|
|
||||||
result_with_data.clone(),
|
|
||||||
current_changes.clone(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
NightState::Active {
|
|
||||||
current_prompt: _,
|
|
||||||
current_result: CurrentResult::None,
|
|
||||||
..
|
|
||||||
} => return Err(GameError::AwaitingResponse),
|
|
||||||
NightState::Complete => return Err(GameError::NightOver),
|
|
||||||
}
|
}
|
||||||
if let Some(prompt) = self.action_queue.pop_front() {
|
|
||||||
if let ActionPrompt::Insomniac { character_id } = &prompt
|
|
||||||
&& self.get_visits_for(character_id.character_id).is_empty()
|
|
||||||
{
|
|
||||||
// skip!
|
|
||||||
self.used_actions.pop(); // it will be re-added
|
|
||||||
return self.next();
|
|
||||||
}
|
|
||||||
self.night_state = NightState::Active {
|
|
||||||
current_prompt: prompt,
|
|
||||||
current_result: CurrentResult::None,
|
|
||||||
current_changes: Vec::new(),
|
|
||||||
current_page: 0,
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
self.night_state = NightState::Complete;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// resolves whether the target [CharacterId] dies tonight with the current
|
/// resolves whether the target [CharacterId] dies tonight with the current
|
||||||
/// state of the night
|
/// state of the night and returns the [DiedTo] cause of death
|
||||||
fn dies_tonight(&self, character_id: CharacterId) -> Result<bool> {
|
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
||||||
let ch = self.current_changes();
|
let ch = self.current_changes();
|
||||||
let mut changes = ChangesLookup::new(&ch);
|
let mut changes = ChangesLookup::new(&ch);
|
||||||
if let Some(died_to) = changes.killed(character_id)
|
if let Some(died_to) = changes.killed(character_id)
|
||||||
|
|
@ -1057,9 +1024,9 @@ impl Night {
|
||||||
)?
|
)?
|
||||||
.is_some()
|
.is_some()
|
||||||
{
|
{
|
||||||
Ok(true)
|
Ok(Some(died_to.clone()))
|
||||||
} else {
|
} else {
|
||||||
Ok(false)
|
Ok(None)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1202,7 +1169,8 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => (*marked == visit_char).then(|| character_id.clone()),
|
} => (*marked == visit_char).then(|| character_id.clone()),
|
||||||
|
|
||||||
ActionPrompt::Bloodletter { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Bloodletter { .. }
|
||||||
| ActionPrompt::WolfPackKill { marked: None, .. }
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||||
| ActionPrompt::Arcanist { marked: _, .. }
|
| ActionPrompt::Arcanist { marked: _, .. }
|
||||||
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,148 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
diedto::DiedTo,
|
||||||
|
error::GameError,
|
||||||
|
game::night::{CurrentResult, Night, NightState, changes::NightChange},
|
||||||
|
message::night::{ActionPrompt, ActionResult},
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::Result;
|
||||||
|
impl Night {
|
||||||
|
#[allow(clippy::should_implement_trait)]
|
||||||
|
pub fn next(&mut self) -> Result<()> {
|
||||||
|
self.next_state_process_maple_starving()?;
|
||||||
|
|
||||||
|
match &self.night_state {
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: CurrentResult::Result(ActionResult::Continue),
|
||||||
|
current_changes,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.used_actions.push((
|
||||||
|
current_prompt.clone(),
|
||||||
|
ActionResult::Continue,
|
||||||
|
current_changes.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: CurrentResult::Result(ActionResult::GoBackToSleep),
|
||||||
|
current_changes,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.used_actions.push((
|
||||||
|
current_prompt.clone(),
|
||||||
|
ActionResult::GoBackToSleep,
|
||||||
|
current_changes.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_result: CurrentResult::Result(_),
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
// needs Continue, not Next
|
||||||
|
return Err(GameError::AwaitingResponse);
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt,
|
||||||
|
current_result: CurrentResult::GoBackToSleepAfterShown { result_with_data },
|
||||||
|
current_changes,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
self.used_actions.push((
|
||||||
|
current_prompt.clone(),
|
||||||
|
result_with_data.clone(),
|
||||||
|
current_changes.clone(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
NightState::Active {
|
||||||
|
current_prompt: _,
|
||||||
|
current_result: CurrentResult::None,
|
||||||
|
..
|
||||||
|
} => return Err(GameError::AwaitingResponse),
|
||||||
|
NightState::Complete => return Err(GameError::NightOver),
|
||||||
|
}
|
||||||
|
if let Some(prompt) = self.action_queue.pop_front() {
|
||||||
|
if let ActionPrompt::Insomniac { character_id } = &prompt
|
||||||
|
&& self.get_visits_for(character_id.character_id).is_empty()
|
||||||
|
{
|
||||||
|
// skip!
|
||||||
|
self.used_actions.pop(); // it will be re-added
|
||||||
|
return self.next();
|
||||||
|
}
|
||||||
|
self.night_state = NightState::Active {
|
||||||
|
current_prompt: prompt,
|
||||||
|
current_result: CurrentResult::None,
|
||||||
|
current_changes: Vec::new(),
|
||||||
|
current_page: 0,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
self.night_state = NightState::Complete;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_state_process_maple_starving(&mut self) -> Result<()> {
|
||||||
|
let (maple_id, target) = match self.current_prompt() {
|
||||||
|
Some((
|
||||||
|
ActionPrompt::MapleWolf {
|
||||||
|
character_id,
|
||||||
|
kill_or_die,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
_,
|
||||||
|
)) => {
|
||||||
|
if *kill_or_die {
|
||||||
|
(character_id.character_id, *marked)
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) | None => return Ok(()),
|
||||||
|
};
|
||||||
|
|
||||||
|
let starve_change = if let Some(night) = NonZeroU8::new(self.night) {
|
||||||
|
NightChange::Kill {
|
||||||
|
target: maple_id,
|
||||||
|
died_to: DiedTo::MapleWolfStarved { night },
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(target) = target else {
|
||||||
|
return self.append_change(starve_change);
|
||||||
|
};
|
||||||
|
match self.died_to_tonight(target)? {
|
||||||
|
Some(DiedTo::MapleWolf { source, .. }) => {
|
||||||
|
if source != maple_id {
|
||||||
|
self.append_change(starve_change)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(_) | None => {
|
||||||
|
self.append_change(starve_change)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::Aura,
|
aura::Aura,
|
||||||
|
bag::DrunkRoll,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{
|
game::night::{
|
||||||
|
|
@ -23,7 +24,7 @@ use crate::{
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
role::{AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle},
|
role::{Alignment, AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
@ -109,10 +110,15 @@ impl Night {
|
||||||
};
|
};
|
||||||
|
|
||||||
match current_prompt {
|
match current_prompt {
|
||||||
|
ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: None,
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
ActionPrompt::Bloodletter {
|
ActionPrompt::Bloodletter {
|
||||||
character_id,
|
character_id,
|
||||||
living_players,
|
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
..
|
||||||
} => Ok(ActionComplete {
|
} => Ok(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: Some(NightChange::ApplyAura {
|
change: Some(NightChange::ApplyAura {
|
||||||
|
|
@ -236,12 +242,11 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: Some(NightChange::Kill {
|
change: NonZeroU8::new(self.night).map(|night| NightChange::Kill {
|
||||||
target: *marked,
|
target: *marked,
|
||||||
died_to: DiedTo::MapleWolf {
|
died_to: DiedTo::MapleWolf {
|
||||||
|
night,
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
night: NonZeroU8::new(self.night)
|
|
||||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
|
||||||
starves_if_fails: *kill_or_die,
|
starves_if_fails: *kill_or_die,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
|
@ -410,10 +415,11 @@ impl Night {
|
||||||
} => {
|
} => {
|
||||||
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
|
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
|
||||||
prompt.matches_beholding(*marked).then_some(result)
|
prompt.matches_beholding(*marked).then_some(result)
|
||||||
}) && self.dies_tonight(*marked)?
|
}) && self.died_to_tonight(*marked)?.is_some()
|
||||||
{
|
{
|
||||||
Ok(ActionComplete {
|
Ok(ActionComplete {
|
||||||
result: if matches!(result, ActionResult::RoleBlocked) {
|
result: if matches!(result, ActionResult::RoleBlocked | ActionResult::Drunk)
|
||||||
|
{
|
||||||
ActionResult::BeholderSawNothing
|
ActionResult::BeholderSawNothing
|
||||||
} else {
|
} else {
|
||||||
result.clone()
|
result.clone()
|
||||||
|
|
@ -534,4 +540,38 @@ impl Night {
|
||||||
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
|
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(super) fn received_response_with_auras(
|
||||||
|
&self,
|
||||||
|
resp: ActionResponse,
|
||||||
|
) -> Result<ResponseOutcome> {
|
||||||
|
let outcome = self.process(resp)?;
|
||||||
|
let mut act = match outcome {
|
||||||
|
ResponseOutcome::PromptUpdate(prompt) => {
|
||||||
|
return Ok(ResponseOutcome::PromptUpdate(prompt));
|
||||||
|
}
|
||||||
|
ResponseOutcome::ActionComplete(act) => act,
|
||||||
|
};
|
||||||
|
let Some(char) = self.current_character() else {
|
||||||
|
return Ok(ResponseOutcome::ActionComplete(act));
|
||||||
|
};
|
||||||
|
for aura in char.auras() {
|
||||||
|
match aura {
|
||||||
|
Aura::Traitor | Aura::Bloodlet { .. } => continue,
|
||||||
|
Aura::Drunk(bag) => {
|
||||||
|
if bag.peek() == DrunkRoll::Drunk {
|
||||||
|
act.change = None;
|
||||||
|
act.result = ActionResult::Drunk;
|
||||||
|
return Ok(ResponseOutcome::ActionComplete(act));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Aura::Insane => {
|
||||||
|
if let Some(insane_result) = act.result.insane() {
|
||||||
|
act.result = insane_result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(ResponseOutcome::ActionComplete(act))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use uuid::Uuid;
|
||||||
use werewolves_macros::{All, ChecksAs, Titles};
|
use werewolves_macros::{All, ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::Aura,
|
aura::{Aura, AuraTitle},
|
||||||
character::Character,
|
character::Character,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
message::Identification,
|
message::Identification,
|
||||||
|
|
@ -157,6 +157,39 @@ pub enum SetupRole {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SetupRoleTitle {
|
impl SetupRoleTitle {
|
||||||
|
pub fn can_assign_aura(&self, aura: AuraTitle) -> bool {
|
||||||
|
if self.into_role().title().wolf() {
|
||||||
|
return match aura {
|
||||||
|
AuraTitle::Traitor | AuraTitle::Bloodlet | AuraTitle::Insane => false,
|
||||||
|
AuraTitle::Drunk => !matches!(self, SetupRoleTitle::Werewolf),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
match aura {
|
||||||
|
AuraTitle::Traitor => true,
|
||||||
|
AuraTitle::Drunk => {
|
||||||
|
matches!(
|
||||||
|
self.category(),
|
||||||
|
Category::StartsAsVillager
|
||||||
|
| Category::Defensive
|
||||||
|
| Category::Intel
|
||||||
|
| Category::Offensive
|
||||||
|
) && !matches!(
|
||||||
|
self,
|
||||||
|
Self::Elder
|
||||||
|
| Self::BlackKnight
|
||||||
|
| Self::Diseased
|
||||||
|
| Self::Weightlifter
|
||||||
|
| Self::Insomniac
|
||||||
|
| Self::Mortician
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AuraTitle::Insane => {
|
||||||
|
matches!(self.category(), Category::Intel)
|
||||||
|
&& !matches!(self, Self::MasonLeader | Self::Empath)
|
||||||
|
}
|
||||||
|
AuraTitle::Bloodlet => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
pub fn into_role(self) -> Role {
|
pub fn into_role(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
SetupRoleTitle::Bloodletter => Role::Bloodletter,
|
SetupRoleTitle::Bloodletter => Role::Bloodletter,
|
||||||
|
|
@ -379,7 +412,7 @@ impl SlotId {
|
||||||
pub struct SetupSlot {
|
pub struct SetupSlot {
|
||||||
pub slot_id: SlotId,
|
pub slot_id: SlotId,
|
||||||
pub role: SetupRole,
|
pub role: SetupRole,
|
||||||
pub auras: Vec<Aura>,
|
pub auras: Vec<AuraTitle>,
|
||||||
pub assign_to: Option<PlayerId>,
|
pub assign_to: Option<PlayerId>,
|
||||||
pub created_order: u32,
|
pub created_order: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -403,7 +436,10 @@ impl SetupSlot {
|
||||||
Character::new(
|
Character::new(
|
||||||
ident.clone(),
|
ident.clone(),
|
||||||
self.role.into_role(roles_in_game)?,
|
self.role.into_role(roles_in_game)?,
|
||||||
self.auras,
|
self.auras
|
||||||
|
.into_iter()
|
||||||
|
.map(|aura| aura.into_aura())
|
||||||
|
.collect(),
|
||||||
)
|
)
|
||||||
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -84,12 +84,18 @@ pub enum StoryActionResult {
|
||||||
Insomniac { visits: Box<[CharacterId]> },
|
Insomniac { visits: Box<[CharacterId]> },
|
||||||
Empath { scapegoat: bool },
|
Empath { scapegoat: bool },
|
||||||
BeholderSawNothing,
|
BeholderSawNothing,
|
||||||
|
BeholderSawEverything,
|
||||||
|
Drunk,
|
||||||
|
ShiftFailed,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryActionResult {
|
impl StoryActionResult {
|
||||||
pub fn new(result: ActionResult) -> Option<Self> {
|
pub fn new(result: ActionResult) -> Option<Self> {
|
||||||
Some(match result {
|
Some(match result {
|
||||||
|
ActionResult::ShiftFailed => Self::ShiftFailed,
|
||||||
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
|
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
|
||||||
|
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,
|
||||||
|
ActionResult::Drunk => Self::Drunk,
|
||||||
ActionResult::RoleBlocked => Self::RoleBlocked,
|
ActionResult::RoleBlocked => Self::RoleBlocked,
|
||||||
ActionResult::Seer(alignment) => Self::Seer(alignment),
|
ActionResult::Seer(alignment) => Self::Seer(alignment),
|
||||||
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
|
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
|
||||||
|
|
@ -390,7 +396,8 @@ impl StoryActionPrompt {
|
||||||
character_id: character_id.character_id,
|
character_id: character_id.character_id,
|
||||||
},
|
},
|
||||||
|
|
||||||
ActionPrompt::Bloodletter { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Bloodletter { .. }
|
||||||
| ActionPrompt::Protector { .. }
|
| ActionPrompt::Protector { .. }
|
||||||
| ActionPrompt::Gravedigger { .. }
|
| ActionPrompt::Gravedigger { .. }
|
||||||
| ActionPrompt::Hunter { .. }
|
| ActionPrompt::Hunter { .. }
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::Aura,
|
aura::{Aura, AuraTitle},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
|
|
@ -51,11 +51,27 @@ impl Village {
|
||||||
let mut changes = ChangesLookup::new(all_changes);
|
let mut changes = ChangesLookup::new(all_changes);
|
||||||
|
|
||||||
let mut new_village = self.clone();
|
let mut new_village = self.clone();
|
||||||
|
|
||||||
|
// dispose of the current drunk token for every drunk in the village
|
||||||
|
new_village
|
||||||
|
.characters_mut()
|
||||||
|
.iter_mut()
|
||||||
|
.filter_map(|c| {
|
||||||
|
c.auras_mut().iter_mut().find_map(|a| match a {
|
||||||
|
Aura::Drunk(bag) => Some(bag),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.for_each(|bag| {
|
||||||
|
// dispose of a token
|
||||||
|
let _ = bag.pull();
|
||||||
|
});
|
||||||
|
|
||||||
for change in all_changes {
|
for change in all_changes {
|
||||||
match change {
|
match change {
|
||||||
NightChange::ApplyAura { target, aura, .. } => {
|
NightChange::ApplyAura { target, aura, .. } => {
|
||||||
let target = new_village.character_by_id_mut(*target)?;
|
let target = new_village.character_by_id_mut(*target)?;
|
||||||
target.apply_aura(*aura);
|
target.apply_aura(aura.clone());
|
||||||
}
|
}
|
||||||
NightChange::ElderReveal { elder } => {
|
NightChange::ElderReveal { elder } => {
|
||||||
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
||||||
|
|
@ -84,6 +100,11 @@ impl Village {
|
||||||
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
||||||
{
|
{
|
||||||
kill.apply_to_village(&mut new_village)?;
|
kill.apply_to_village(&mut new_village)?;
|
||||||
|
if let DiedTo::MapleWolf { source, .. } = died_to
|
||||||
|
&& let Ok(maple) = new_village.character_by_id_mut(*source)
|
||||||
|
{
|
||||||
|
*maple.maple_wolf_mut()? = night;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::Shapeshift { source, into } => {
|
NightChange::Shapeshift { source, into } => {
|
||||||
|
|
@ -157,9 +178,38 @@ impl Village {
|
||||||
NightChange::LostAura { character, aura } => {
|
NightChange::LostAura { character, aura } => {
|
||||||
new_village
|
new_village
|
||||||
.character_by_id_mut(*character)?
|
.character_by_id_mut(*character)?
|
||||||
.remove_aura(*aura);
|
.remove_aura(aura.title());
|
||||||
|
}
|
||||||
|
NightChange::Protection { protection, target } => {
|
||||||
|
let target_ident = new_village.character_by_id(*target)?.identity();
|
||||||
|
match protection {
|
||||||
|
Protection::Guardian {
|
||||||
|
source,
|
||||||
|
guarding: true,
|
||||||
|
} => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.guardian_mut()?
|
||||||
|
.replace(PreviousGuardianAction::Guard(target_ident));
|
||||||
|
}
|
||||||
|
Protection::Guardian {
|
||||||
|
source,
|
||||||
|
guarding: false,
|
||||||
|
} => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.guardian_mut()?
|
||||||
|
.replace(PreviousGuardianAction::Protect(target_ident));
|
||||||
|
}
|
||||||
|
Protection::Protector { source } => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*source)?
|
||||||
|
.protector_mut()?
|
||||||
|
.replace(*target);
|
||||||
|
}
|
||||||
|
Protection::Vindicator { .. } => {}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
NightChange::Protection { .. } => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// black knights death
|
// black knights death
|
||||||
|
|
|
||||||
|
|
@ -186,9 +186,17 @@ pub trait ActionResultExt {
|
||||||
fn adjudicator(&self) -> Killer;
|
fn adjudicator(&self) -> Killer;
|
||||||
fn mortician(&self) -> DiedToTitle;
|
fn mortician(&self) -> DiedToTitle;
|
||||||
fn empath(&self) -> bool;
|
fn empath(&self) -> bool;
|
||||||
|
fn drunk(&self);
|
||||||
|
fn shapeshift_failed(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionResultExt for ActionResult {
|
impl ActionResultExt for ActionResult {
|
||||||
|
fn shapeshift_failed(&self) {
|
||||||
|
assert_eq!(*self, Self::ShiftFailed)
|
||||||
|
}
|
||||||
|
fn drunk(&self) {
|
||||||
|
assert_eq!(*self, Self::Drunk)
|
||||||
|
}
|
||||||
fn empath(&self) -> bool {
|
fn empath(&self) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Self::Empath { scapegoat } => *scapegoat,
|
Self::Empath { scapegoat } => *scapegoat,
|
||||||
|
|
@ -322,6 +330,7 @@ pub trait GameExt {
|
||||||
fn get_state(&mut self) -> ServerToHostMessage;
|
fn get_state(&mut self) -> ServerToHostMessage;
|
||||||
fn next_expect_game_over(&mut self) -> GameOver;
|
fn next_expect_game_over(&mut self) -> GameOver;
|
||||||
fn prev(&mut self) -> ServerToHostMessage;
|
fn prev(&mut self) -> ServerToHostMessage;
|
||||||
|
fn mark_villager(&mut self) -> ActionPrompt;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameExt for Game {
|
impl GameExt for Game {
|
||||||
|
|
@ -394,10 +403,15 @@ impl GameExt for Game {
|
||||||
.prompt()
|
.prompt()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mark_villager(&mut self) -> ActionPrompt {
|
||||||
|
self.mark(self.living_villager().character_id())
|
||||||
|
}
|
||||||
|
|
||||||
fn mark_and_check(&mut self, mark: CharacterId) {
|
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||||
let prompt = self.mark(mark);
|
let prompt = self.mark(mark);
|
||||||
match prompt {
|
match prompt {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::ElderReveal { .. }
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::CoverOfDarkness
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
|
@ -923,7 +937,9 @@ fn big_game_test_based_on_story_test() {
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
game.next().title().shapeshifter();
|
game.next().title().shapeshifter();
|
||||||
game.response(ActionResponse::Shapeshift).sleep();
|
game.response(ActionResponse::Shapeshift)
|
||||||
|
.shapeshift_failed();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().seer();
|
game.next().title().seer();
|
||||||
game.mark(game.character_by_player_id(werewolf).character_id());
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,8 @@ use crate::{
|
||||||
diedto::DiedToTitle,
|
diedto::DiedToTitle,
|
||||||
game::{Game, GameSettings, GameState, OrRandom, SetupRole},
|
game::{Game, GameSettings, GameState, OrRandom, SetupRole},
|
||||||
game_test::{
|
game_test::{
|
||||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
ActionPromptTitleExt, ActionResultExt, GameExt, ServerToHostMessageExt, SettingsExt,
|
||||||
|
gen_players, init_log,
|
||||||
},
|
},
|
||||||
message::{
|
message::{
|
||||||
Identification, PublicIdentity,
|
Identification, PublicIdentity,
|
||||||
|
|
@ -353,7 +354,9 @@ fn previous_prompt() {
|
||||||
game.r#continue().r#continue();
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
game.next().title().shapeshifter();
|
game.next().title().shapeshifter();
|
||||||
game.response(ActionResponse::Shapeshift).sleep();
|
game.response(ActionResponse::Shapeshift)
|
||||||
|
.shapeshift_failed();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next().title().seer();
|
game.next().title().seer();
|
||||||
game.mark(game.character_by_player_id(werewolf).character_id());
|
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,334 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
use core::num::{NonZero, NonZeroU8};
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
|
bag::DrunkBag,
|
||||||
|
diedto::DiedTo,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::ActionPromptTitle,
|
||||||
|
role::{Alignment, Killer, Powerful},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maple_starves() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let maple = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::MapleWolf, maple);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(maple).died_to().cloned(),
|
||||||
|
Some(DiedTo::MapleWolfStarved {
|
||||||
|
night: NonZeroU8::new(3).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn maple_last_eat_counter_increments() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let maple = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::MapleWolf, maple);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
3
|
||||||
|
);
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
4
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(maple).died_to().cloned(), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drunk_maple_doesnt_eat() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let maple = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::MapleWolf, maple);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.village_mut()
|
||||||
|
.characters_mut()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.player_id() == maple)
|
||||||
|
.unwrap()
|
||||||
|
.apply_aura(Aura::Drunk(DrunkBag::all_drunk()));
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().drunk();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.village()
|
||||||
|
.character_by_id(maple_kill)
|
||||||
|
.unwrap()
|
||||||
|
.died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().drunk();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
game.village()
|
||||||
|
.character_by_id(maple_kill)
|
||||||
|
.unwrap()
|
||||||
|
.died_to(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
|
||||||
|
let (maple_kill, wolf_kill) = {
|
||||||
|
let mut cid = game
|
||||||
|
.villager_character_ids()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| game.village().character_by_id(*c).unwrap().alive());
|
||||||
|
(cid.next().unwrap(), cid.next().unwrap())
|
||||||
|
};
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(wolf_kill);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().maple_wolf();
|
||||||
|
game.mark(maple_kill);
|
||||||
|
game.r#continue().drunk();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
*game.character_by_player_id(maple).maple_wolf_mut().unwrap(),
|
||||||
|
0
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(maple).died_to().cloned(),
|
||||||
|
Some(DiedTo::MapleWolfStarved {
|
||||||
|
night: NonZeroU8::new(3).unwrap()
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -23,9 +23,11 @@ mod guardian;
|
||||||
mod hunter;
|
mod hunter;
|
||||||
mod insomniac;
|
mod insomniac;
|
||||||
mod lone_wolf;
|
mod lone_wolf;
|
||||||
|
mod maple_wolf;
|
||||||
mod mason;
|
mod mason;
|
||||||
mod militia;
|
mod militia;
|
||||||
mod mortician;
|
mod mortician;
|
||||||
|
mod protector;
|
||||||
mod pyremaster;
|
mod pyremaster;
|
||||||
mod scapegoat;
|
mod scapegoat;
|
||||||
mod shapeshifter;
|
mod shapeshifter;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,105 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
#[allow(unused)]
|
||||||
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cannot_protect_same_target() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().protector();
|
||||||
|
let prot = game.living_villager();
|
||||||
|
game.mark(prot.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(prot.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protector)
|
||||||
|
.protector_mut()
|
||||||
|
.unwrap()
|
||||||
|
.clone(),
|
||||||
|
Some(prot.character_id())
|
||||||
|
);
|
||||||
|
|
||||||
|
match game.execute() {
|
||||||
|
ActionPrompt::Protector { targets, .. } => {
|
||||||
|
assert!(
|
||||||
|
!targets
|
||||||
|
.into_iter()
|
||||||
|
.any(|c| c.character_id == prot.character_id())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
prompt => panic!("expected protector prompt, got {:?}", prompt.title()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_self_protect() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().protector();
|
||||||
|
game.mark(game.character_by_player_id(protector).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(game.character_by_player_id(protector).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(protector).died_to().cloned(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -130,7 +130,9 @@ fn protect_stops_shapeshift() {
|
||||||
|
|
||||||
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
assert_eq!(game.next().title(), ActionPromptTitle::Shapeshifter,);
|
||||||
|
|
||||||
game.response(ActionResponse::Shapeshift);
|
game.response(ActionResponse::Shapeshift)
|
||||||
|
.shapeshift_failed();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
|
|
||||||
|
|
@ -218,3 +220,60 @@ fn i_would_simply_refuse() {
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shapeshift_fail_can_continue() {
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let shapeshifter = player_ids.next().unwrap();
|
||||||
|
let direwolf = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let protector = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
|
||||||
|
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Protector, protector);
|
||||||
|
settings.fill_remaining_slots_with_villagers(20);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
let dw_target = game.living_villager();
|
||||||
|
game.mark(dw_target.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().protector();
|
||||||
|
let ss_target = game.living_villager();
|
||||||
|
game.mark(ss_target.character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().wolf_pack_kill();
|
||||||
|
game.mark(ss_target.character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().shapeshifter();
|
||||||
|
match game
|
||||||
|
.process(HostGameMessage::Night(HostNightMessage::ActionResponse(
|
||||||
|
ActionResponse::Shapeshift,
|
||||||
|
)))
|
||||||
|
.unwrap()
|
||||||
|
{
|
||||||
|
ServerToHostMessage::ActionResult(_, ActionResult::ShiftFailed) => {}
|
||||||
|
other => panic!("expected shift fail, got {other:?}"),
|
||||||
|
};
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().direwolf();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(dw_target.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,8 +13,8 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
|
|
||||||
pub mod aura;
|
pub mod aura;
|
||||||
|
pub mod bag;
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod diedto;
|
pub mod diedto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ pub enum ActionType {
|
||||||
Cover,
|
Cover,
|
||||||
#[checks("is_wolfy")]
|
#[checks("is_wolfy")]
|
||||||
WolvesIntro,
|
WolvesIntro,
|
||||||
|
TraitorIntro,
|
||||||
RoleChange,
|
RoleChange,
|
||||||
Protect,
|
Protect,
|
||||||
#[checks("is_wolfy")]
|
#[checks("is_wolfy")]
|
||||||
|
|
@ -209,12 +210,15 @@ pub enum ActionPrompt {
|
||||||
living_players: Box<[CharacterIdentity]>,
|
living_players: Box<[CharacterIdentity]>,
|
||||||
marked: Option<CharacterId>,
|
marked: Option<CharacterId>,
|
||||||
},
|
},
|
||||||
|
#[checks(ActionType::TraitorIntro)]
|
||||||
|
TraitorIntro { character_id: CharacterIdentity },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPrompt {
|
impl ActionPrompt {
|
||||||
pub(crate) const fn character_id(&self) -> Option<CharacterId> {
|
pub(crate) const fn character_id(&self) -> Option<CharacterId> {
|
||||||
match self {
|
match self {
|
||||||
ActionPrompt::Insomniac { character_id, .. }
|
ActionPrompt::TraitorIntro { character_id }
|
||||||
|
| ActionPrompt::Insomniac { character_id, .. }
|
||||||
| ActionPrompt::LoneWolfKill { character_id, .. }
|
| ActionPrompt::LoneWolfKill { character_id, .. }
|
||||||
| ActionPrompt::ElderReveal { character_id }
|
| ActionPrompt::ElderReveal { character_id }
|
||||||
| ActionPrompt::RoleChange { character_id, .. }
|
| ActionPrompt::RoleChange { character_id, .. }
|
||||||
|
|
@ -257,7 +261,8 @@ impl ActionPrompt {
|
||||||
| ActionPrompt::Mortician { character_id, .. }
|
| ActionPrompt::Mortician { character_id, .. }
|
||||||
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
|
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
|
||||||
|
|
||||||
ActionPrompt::Beholder { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Beholder { .. }
|
||||||
| ActionPrompt::CoverOfDarkness
|
| ActionPrompt::CoverOfDarkness
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
|
|
@ -281,13 +286,14 @@ impl ActionPrompt {
|
||||||
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
||||||
let mut prompt = self.clone();
|
let mut prompt = self.clone();
|
||||||
match &mut prompt {
|
match &mut prompt {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::ElderReveal { .. }
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::WolvesIntro { .. }
|
| ActionPrompt::WolvesIntro { .. }
|
||||||
| ActionPrompt::RoleChange { .. }
|
| ActionPrompt::RoleChange { .. }
|
||||||
| ActionPrompt::Shapeshifter { .. }
|
| ActionPrompt::Shapeshifter { .. }
|
||||||
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
| ActionPrompt::CoverOfDarkness => Err(GameError::RoleDoesntMark),
|
||||||
|
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
previous,
|
previous,
|
||||||
|
|
@ -489,6 +495,7 @@ pub enum ActionResponse {
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub enum ActionResult {
|
pub enum ActionResult {
|
||||||
RoleBlocked,
|
RoleBlocked,
|
||||||
|
Drunk,
|
||||||
Seer(Alignment),
|
Seer(Alignment),
|
||||||
PowerSeer { powerful: Powerful },
|
PowerSeer { powerful: Powerful },
|
||||||
Adjudicator { killer: Killer },
|
Adjudicator { killer: Killer },
|
||||||
|
|
@ -498,10 +505,42 @@ pub enum ActionResult {
|
||||||
Insomniac(Visits),
|
Insomniac(Visits),
|
||||||
Empath { scapegoat: bool },
|
Empath { scapegoat: bool },
|
||||||
BeholderSawNothing,
|
BeholderSawNothing,
|
||||||
|
BeholderSawEverything,
|
||||||
GoBackToSleep,
|
GoBackToSleep,
|
||||||
|
ShiftFailed,
|
||||||
Continue,
|
Continue,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl ActionResult {
|
||||||
|
pub fn insane(&self) -> Option<Self> {
|
||||||
|
Some(match self {
|
||||||
|
ActionResult::Seer(Alignment::Village) => ActionResult::Seer(Alignment::Wolves),
|
||||||
|
ActionResult::Seer(Alignment::Traitor) | ActionResult::Seer(Alignment::Wolves) => {
|
||||||
|
ActionResult::Seer(Alignment::Village)
|
||||||
|
}
|
||||||
|
ActionResult::PowerSeer { powerful } => ActionResult::PowerSeer {
|
||||||
|
powerful: !*powerful,
|
||||||
|
},
|
||||||
|
ActionResult::Adjudicator { killer } => ActionResult::Adjudicator { killer: !*killer },
|
||||||
|
ActionResult::Arcanist(alignment_eq) => ActionResult::Arcanist(!*alignment_eq),
|
||||||
|
ActionResult::Empath { scapegoat } => ActionResult::Empath {
|
||||||
|
scapegoat: !*scapegoat,
|
||||||
|
},
|
||||||
|
ActionResult::BeholderSawNothing => ActionResult::BeholderSawEverything,
|
||||||
|
ActionResult::BeholderSawEverything => ActionResult::BeholderSawNothing,
|
||||||
|
|
||||||
|
ActionResult::ShiftFailed
|
||||||
|
| ActionResult::RoleBlocked
|
||||||
|
| ActionResult::Drunk
|
||||||
|
| ActionResult::GraveDigger(_)
|
||||||
|
| ActionResult::Mortician(_)
|
||||||
|
| ActionResult::Insomniac(_)
|
||||||
|
| ActionResult::GoBackToSleep
|
||||||
|
| ActionResult::Continue => return None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
pub struct Visits(Box<[CharacterIdentity]>);
|
pub struct Visits(Box<[CharacterIdentity]>);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,12 @@ use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::{ChecksAs, Titles};
|
use werewolves_macros::{ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
character::CharacterId,
|
aura::Aura,
|
||||||
|
character::{Character, CharacterId},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
game::{GameTime, Village},
|
game::{GameTime, Village},
|
||||||
message::CharacterIdentity,
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionType},
|
||||||
|
player::PlayerId,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)]
|
#[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)]
|
||||||
|
|
@ -318,7 +320,7 @@ impl Role {
|
||||||
|
|
||||||
Role::Werewolf => KillingWolfOrder::Werewolf,
|
Role::Werewolf => KillingWolfOrder::Werewolf,
|
||||||
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
||||||
Role::Bloodletter { .. } => KillingWolfOrder::Bloodletter,
|
Role::Bloodletter => KillingWolfOrder::Bloodletter,
|
||||||
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
||||||
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
||||||
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
||||||
|
|
@ -469,7 +471,7 @@ impl Display for Alignment {
|
||||||
match self {
|
match self {
|
||||||
Alignment::Village => f.write_str("Village"),
|
Alignment::Village => f.write_str("Village"),
|
||||||
Alignment::Wolves => f.write_str("Wolves"),
|
Alignment::Wolves => f.write_str("Wolves"),
|
||||||
Alignment::Traitor => f.write_str("Damned"),
|
Alignment::Traitor => f.write_str("Traitor"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use colored::Colorize;
|
||||||
use tokio::sync::{broadcast::Sender, mpsc::Receiver};
|
use tokio::sync::{broadcast::Sender, mpsc::Receiver};
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -45,6 +46,10 @@ impl HostComms {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn send(&mut self, message: ServerToHostMessage) -> Result<(), GameError> {
|
pub fn send(&mut self, message: ServerToHostMessage) -> Result<(), GameError> {
|
||||||
|
log::debug!(
|
||||||
|
"sending message to host: {}",
|
||||||
|
format!("{message:?}").dimmed()
|
||||||
|
);
|
||||||
self.send
|
self.send
|
||||||
.send(message)
|
.send(message)
|
||||||
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
||||||
|
|
|
||||||
|
|
@ -146,7 +146,6 @@ impl Host {
|
||||||
msg = self.server_recv.recv() => {
|
msg = self.server_recv.recv() => {
|
||||||
match msg {
|
match msg {
|
||||||
Ok(msg) => {
|
Ok(msg) => {
|
||||||
log::debug!("sending message to host: {}", format!("{msg:?}").dimmed());
|
|
||||||
if let Err(err) = self.send_message(&msg).await {
|
if let Err(err) = self.send_message(&msg).await {
|
||||||
log::error!("{} {err}", "[host::outgoing]".bold())
|
log::error!("{} {err}", "[host::outgoing]".bold())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="70.481842mm"
|
||||||
|
height="70.97554mm"
|
||||||
|
viewBox="0 0 70.481842 70.97554"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
xml:space="preserve"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||||
|
id="defs1" /><g
|
||||||
|
id="layer4"
|
||||||
|
transform="translate(-483.44732,-408.61381)"><g
|
||||||
|
id="g182"><path
|
||||||
|
id="path182"
|
||||||
|
style="fill:#c29c00;fill-opacity:1;stroke:#755e00;stroke-width:1.4;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 532.22935,429.6004 a 21,21 0 0 0 -21.00017,21.00017 21,21 0 0 0 21.00017,20.9998 21,21 0 0 0 20.99981,-20.9998 21,21 0 0 0 -20.99981,-21.00017 z m 0,7.0003 a 14,14 0 0 1 13.99987,13.99987 14,14 0 0 1 -13.99987,13.99987 14,14 0 0 1 -14.00023,-13.99987 14,14 0 0 1 14.00023,-13.99987 z" /><path
|
||||||
|
id="path178"
|
||||||
|
style="fill:#daaf00;fill-opacity:1;stroke:#755e00;stroke-width:2.21395;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 492.41992,423.0918 c -5.82792,15.71457 -3.5165,47.74793 4.98242,53.65625 1.6238,1.12884 6.50214,1.73437 15.82618,1.73437 9.32404,0 14.20041,-0.60553 15.82421,-1.73437 8.49892,-5.90832 10.81035,-37.94168 4.98243,-53.65625 h -20.80664 z" /><g
|
||||||
|
id="g181"
|
||||||
|
transform="translate(64.081238,-6.2578735)"
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-opacity:1"><ellipse
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path181"
|
||||||
|
cx="449.1463"
|
||||||
|
cy="457.04498"
|
||||||
|
rx="4"
|
||||||
|
ry="20" /><ellipse
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path181-5"
|
||||||
|
cx="463.11505"
|
||||||
|
cy="457.04498"
|
||||||
|
rx="4"
|
||||||
|
ry="20" /><ellipse
|
||||||
|
style="fill:#755e00;fill-opacity:0.496844;stroke:#755e00;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
id="path181-7"
|
||||||
|
cx="435.17755"
|
||||||
|
cy="457.04498"
|
||||||
|
rx="4"
|
||||||
|
ry="20" /></g><path
|
||||||
|
d="m 520.38364,409.61388 a 10,10 0 0 0 -8.32301,4.45657 10,10 0 0 0 -5.31647,-1.53065 10,10 0 0 0 -9.14414,5.95209 10,10 0 0 0 -3.15226,-0.50953 10,10 0 0 0 -10.00043,9.9999 10,10 0 0 0 10.00043,9.99991 10,10 0 0 0 1.16427,-0.0682 10,10 0 0 0 8.99893,5.63893 10,10 0 0 0 6.87503,-2.73833 10,10 0 0 0 7.97729,3.96978 10,10 0 0 0 9.41235,-6.62234 10,10 0 0 0 2.23759,0.25322 10,10 0 0 0 9.99991,-9.99991 10,10 0 0 0 -9.99991,-9.9999 10,10 0 0 0 -0.79789,0.032 10,10 0 0 0 -9.93169,-8.83357 z"
|
||||||
|
style="fill:#ffffff;fill-opacity:0.9;stroke:#b2b2b2;stroke-width:2;stroke-opacity:0.7"
|
||||||
|
id="path180" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 7.0 KiB |
|
|
@ -17,6 +17,10 @@ $offensive_color: color.adjust($village_color, $hue: 30deg);
|
||||||
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
$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);
|
||||||
|
$traitor_color: color.adjust($village_color, $hue: 45deg);
|
||||||
|
$traitor_border: color.change($traitor_color, $alpha: 1.0);
|
||||||
|
$drunk_color: color.adjust($village_color, $hue: 150deg);
|
||||||
|
$drunk_border: color.change($drunk_color, $alpha: 1.0);
|
||||||
|
|
||||||
$wolves_border_faint: color.change($wolves_border, $alpha: 0.3);
|
$wolves_border_faint: color.change($wolves_border, $alpha: 0.3);
|
||||||
$village_border_faint: color.change($village_border, $alpha: 0.3);
|
$village_border_faint: color.change($village_border, $alpha: 0.3);
|
||||||
|
|
@ -24,6 +28,8 @@ $offensive_border_faint: color.change($offensive_border, $alpha: 0.3);
|
||||||
$defensive_border_faint: color.change($defensive_border, $alpha: 0.3);
|
$defensive_border_faint: color.change($defensive_border, $alpha: 0.3);
|
||||||
$intel_border_faint: color.change($intel_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);
|
$starts_as_villager_border_faint: color.change($starts_as_villager_border, $alpha: 0.3);
|
||||||
|
$traitor_border_faint: color.change($traitor_border, $alpha: 0.3);
|
||||||
|
$drunk_border_faint: color.change($drunk_border, $alpha: 0.3);
|
||||||
|
|
||||||
$wolves_color_faint: color.change($wolves_color, $alpha: 0.1);
|
$wolves_color_faint: color.change($wolves_color, $alpha: 0.1);
|
||||||
$village_color_faint: color.change($village_color, $alpha: 0.1);
|
$village_color_faint: color.change($village_color, $alpha: 0.1);
|
||||||
|
|
@ -31,7 +37,8 @@ $offensive_color_faint: color.change($offensive_color, $alpha: 0.1);
|
||||||
$defensive_color_faint: color.change($defensive_color, $alpha: 0.1);
|
$defensive_color_faint: color.change($defensive_color, $alpha: 0.1);
|
||||||
$intel_color_faint: color.change($intel_color, $alpha: 0.1);
|
$intel_color_faint: color.change($intel_color, $alpha: 0.1);
|
||||||
$starts_as_villager_color_faint: color.change($starts_as_villager_color, $alpha: 0.1);
|
$starts_as_villager_color_faint: color.change($starts_as_villager_color, $alpha: 0.1);
|
||||||
|
$traitor_color_faint: color.change($traitor_color, $alpha: 0.1);
|
||||||
|
$drunk_color_faint: color.change($drunk_color, $alpha: 0.1);
|
||||||
|
|
||||||
@mixin flexbox() {
|
@mixin flexbox() {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
|
|
@ -280,7 +287,6 @@ nav.host-nav {
|
||||||
align-self: center;
|
align-self: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
// background-color: hsl(283, 100%, 80%);
|
|
||||||
border: 1px solid rgba(0, 255, 0, 0.7);
|
border: 1px solid rgba(0, 255, 0, 0.7);
|
||||||
background-color: black;
|
background-color: black;
|
||||||
color: rgba(0, 255, 0, 0.7);
|
color: rgba(0, 255, 0, 0.7);
|
||||||
|
|
@ -298,6 +304,7 @@ nav.host-nav {
|
||||||
border: 1px solid rgba(255, 0, 0, 1);
|
border: 1px solid rgba(255, 0, 0, 1);
|
||||||
color: rgba(255, 0, 0, 1);
|
color: rgba(255, 0, 0, 1);
|
||||||
filter: none;
|
filter: none;
|
||||||
|
background-color: rgba(255, 0, 0, 0.1);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: rgba(255, 0, 0, 0.3);
|
background-color: rgba(255, 0, 0, 0.3);
|
||||||
|
|
@ -1110,15 +1117,20 @@ input {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
&>.submenu {
|
&>.submenu {
|
||||||
min-width: 30vw;
|
width: 30vw;
|
||||||
|
// position: absolute;
|
||||||
|
|
||||||
.assign-list {
|
.assign-list {
|
||||||
// min-width: 5cm;
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
|
||||||
& .submenu button {
|
& .submenu button {
|
||||||
width: 5cm;
|
width: 10vw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1126,6 +1138,7 @@ input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1270,6 +1283,44 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.traitor {
|
||||||
|
background-color: $traitor_color;
|
||||||
|
border: 1px solid $traitor_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $traitor_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $traitor_border_faint;
|
||||||
|
background-color: $traitor_color_faint;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $traitor_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drunk {
|
||||||
|
background-color: $drunk_color;
|
||||||
|
border: 1px solid $drunk_border;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: white;
|
||||||
|
background-color: $drunk_border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.faint {
|
||||||
|
border: 1px solid $drunk_border_faint;
|
||||||
|
background-color: $drunk_color_faint;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $drunk_border_faint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.assignments {
|
.assignments {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -1790,6 +1841,18 @@ input {
|
||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.info-icon-grow {
|
||||||
|
flex-grow: 1;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.info-player-list {
|
.info-player-list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
@ -1943,6 +2006,39 @@ input {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
height: var(--information-height);
|
height: var(--information-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.setup-aura {
|
||||||
|
&.active {
|
||||||
|
$active_color: color.change($connected_color, $alpha: 0.7);
|
||||||
|
border: $active_color 1px solid;
|
||||||
|
color: $active_color;
|
||||||
|
background-color: color.change($active_color, $alpha: 0.2);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $active_color;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.aura-title {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
gap: 3px;
|
||||||
|
// text-align: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.title {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
img,
|
||||||
|
.icon {
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
pub trait Class {
|
||||||
|
fn class(&self) -> Option<&'static str>;
|
||||||
|
}
|
||||||
|
|
@ -822,7 +822,8 @@ impl GameExt for Game {
|
||||||
fn mark_and_check(&mut self, mark: CharacterId) {
|
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||||
let prompt = self.mark(mark);
|
let prompt = self.mark(mark);
|
||||||
match prompt {
|
match prompt {
|
||||||
ActionPrompt::Insomniac { .. }
|
ActionPrompt::TraitorIntro { .. }
|
||||||
|
| ActionPrompt::Insomniac { .. }
|
||||||
| ActionPrompt::MasonsWake { .. }
|
| ActionPrompt::MasonsWake { .. }
|
||||||
| ActionPrompt::ElderReveal { .. }
|
| ActionPrompt::ElderReveal { .. }
|
||||||
| ActionPrompt::CoverOfDarkness
|
| ActionPrompt::CoverOfDarkness
|
||||||
|
|
|
||||||
|
|
@ -31,7 +31,7 @@ use crate::{
|
||||||
Button, CoverOfDarkness, Identity,
|
Button, CoverOfDarkness, Identity,
|
||||||
action::{BinaryChoice, TargetPicker, WolvesIntro},
|
action::{BinaryChoice, TargetPicker, WolvesIntro},
|
||||||
},
|
},
|
||||||
pages::MasonsWake,
|
pages::{MasonsWake, TraitorIntroPage},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
|
@ -113,6 +113,15 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let (character_id, targets, marked, role_info) = match &props.prompt {
|
let (character_id, targets, marked, role_info) = match &props.prompt {
|
||||||
|
ActionPrompt::TraitorIntro { character_id } => {
|
||||||
|
return html! {
|
||||||
|
<div class="prompt">
|
||||||
|
{identity_html(props, Some(character_id))}
|
||||||
|
<TraitorIntroPage />
|
||||||
|
{cont}
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
}
|
||||||
ActionPrompt::CoverOfDarkness => {
|
ActionPrompt::CoverOfDarkness => {
|
||||||
return html! {
|
return html! {
|
||||||
<CoverOfDarkness next={continue_callback}/>
|
<CoverOfDarkness next={continue_callback}/>
|
||||||
|
|
@ -403,24 +412,26 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
let choice = props.big_screen.not().then_some(html! {
|
||||||
|
<BinaryChoice on_chosen={on_select} />
|
||||||
|
});
|
||||||
return html! {
|
return html! {
|
||||||
<div class="role-page">
|
<div class="role-page">
|
||||||
{identity_html(props, Some(character_id))}
|
{identity_html(props, Some(character_id))}
|
||||||
<h1 class="wolves">{"SHAPESHIFTER"}</h1>
|
<h1 class="wolves">{"SHAPESHIFTER"}</h1>
|
||||||
<div class="information wolves faint">
|
<div class="information wolves faint">
|
||||||
<h3>
|
<h2>
|
||||||
{"WOULD YOU LIKE TO USE YOUR "}
|
{"WOULD YOU LIKE TO USE YOUR "}
|
||||||
<span class="yellow">{"ONCE PER GAME"}</span>
|
<span class="yellow">{"ONCE PER GAME"}</span>
|
||||||
{" SHAPESHIFT ABILITY?"}
|
{" SHAPESHIFT ABILITY?"}
|
||||||
</h3>
|
</h2>
|
||||||
<h4>
|
<h2>
|
||||||
<span class="yellow">{"YOU WILL DIE"}</span>{", AND THE "}
|
<span class="yellow">{"YOU WILL DIE"}</span>{", AND THE "}
|
||||||
{"TARGET OF THE WOLFPACK KILL"}
|
{"TARGET OF THE WOLFPACK KILL"}
|
||||||
{" SHALL INSTEAD BECOME A WOLF"}
|
{" SHALL INSTEAD BECOME A WOLF"}
|
||||||
</h4>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<BinaryChoice on_chosen={on_select}>
|
{choice}
|
||||||
</BinaryChoice>
|
|
||||||
</div>
|
</div>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,9 @@ use yew::prelude::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
components::{Button, CoverOfDarkness, Icon, IconSource, Identity},
|
components::{Button, CoverOfDarkness, Icon, IconSource, Identity},
|
||||||
pages::{
|
pages::{
|
||||||
AdjudicatorResult, ArcanistResult, BeholderSawNothing, EmpathResult, GravediggerResultPage,
|
AdjudicatorResult, ArcanistResult, BeholderSawEverything, BeholderSawNothing, DrunkPage,
|
||||||
InsomniacResult, MorticianResultPage, PowerSeerResult, RoleblockPage, SeerResult,
|
EmpathResult, GravediggerResultPage, InsomniacResult, MorticianResultPage, PowerSeerResult,
|
||||||
|
RoleblockPage, SeerResult, ShiftFailed,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -58,6 +59,15 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
||||||
.not()
|
.not()
|
||||||
.then(|| html! {<Button on_click={on_continue}>{"continue"}</Button>});
|
.then(|| html! {<Button on_click={on_continue}>{"continue"}</Button>});
|
||||||
let body = match &props.result {
|
let body = match &props.result {
|
||||||
|
ActionResult::ShiftFailed => html! {
|
||||||
|
<ShiftFailed />
|
||||||
|
},
|
||||||
|
ActionResult::Drunk => html! {
|
||||||
|
<DrunkPage />
|
||||||
|
},
|
||||||
|
ActionResult::BeholderSawEverything => html! {
|
||||||
|
<BeholderSawEverything />
|
||||||
|
},
|
||||||
ActionResult::BeholderSawNothing => html! {
|
ActionResult::BeholderSawNothing => html! {
|
||||||
<BeholderSawNothing />
|
<BeholderSawNothing />
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,28 +12,39 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use werewolves_proto::aura;
|
use werewolves_proto::aura::{self, Aura, AuraTitle};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Icon, IconType, PartialAssociatedIcon};
|
use crate::{
|
||||||
|
class::Class,
|
||||||
|
components::{Icon, IconType, PartialAssociatedIcon},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
impl Class for AuraTitle {
|
||||||
pub struct AuraProps {
|
fn class(&self) -> Option<&'static str> {
|
||||||
pub aura: aura::Aura,
|
Some(match self {
|
||||||
|
aura::AuraTitle::Traitor => "traitor",
|
||||||
|
aura::AuraTitle::Drunk => "drunk",
|
||||||
|
aura::AuraTitle::Insane => "insane",
|
||||||
|
aura::AuraTitle::Bloodlet => "wolves",
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn aura_class(aura: &aura::Aura) -> Option<&'static str> {
|
impl Class for Aura {
|
||||||
Some(match aura {
|
fn class(&self) -> Option<&'static str> {
|
||||||
aura::Aura::Traitor => "traitor",
|
self.title().class()
|
||||||
aura::Aura::Drunk => "drunk",
|
}
|
||||||
aura::Aura::Insane => "insane",
|
}
|
||||||
aura::Aura::Bloodlet { .. } => "wolves",
|
|
||||||
})
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct AuraSpanProps {
|
||||||
|
pub aura: aura::AuraTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn Aura(AuraProps { aura }: &AuraProps) -> Html {
|
pub fn AuraSpan(AuraSpanProps { aura }: &AuraSpanProps) -> Html {
|
||||||
let class = aura_class(aura);
|
let class = aura.class();
|
||||||
let icon = aura.icon().map(|icon| {
|
let icon = aura.icon().map(|icon| {
|
||||||
html! {
|
html! {
|
||||||
<div>
|
<div>
|
||||||
|
|
|
||||||
|
|
@ -151,16 +151,13 @@ pub fn SetupCategory(
|
||||||
</div>
|
</div>
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
<div class="alignment">
|
<div class="alignment">
|
||||||
// <img class="icon" src={alignment} alt={"alignment"}/>
|
|
||||||
<Icon source={alignment} icon_type={IconType::Small}/>
|
<Icon source={alignment} icon_type={IconType::Small}/>
|
||||||
</div>
|
</div>
|
||||||
<div class={classes!("killer", killer_inactive)}>
|
<div class={classes!("killer", killer_inactive)}>
|
||||||
<Icon source={IconSource::Killer} icon_type={IconType::Small}/>
|
<Icon source={IconSource::Killer} icon_type={IconType::Small}/>
|
||||||
// <img class="icon" src="/img/killer.svg" alt="killer icon"/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class={classes!("poweful", powerful_inactive)}>
|
<div class={classes!("poweful", powerful_inactive)}>
|
||||||
<Icon source={IconSource::Powerful} icon_type={IconType::Small}/>
|
<Icon source={IconSource::Powerful} icon_type={IconType::Small}/>
|
||||||
// <img class="icon" src="/img/powerful.svg" alt="powerful icon"/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
aura::Aura,
|
aura::{Aura, AuraTitle},
|
||||||
diedto::DiedToTitle,
|
diedto::DiedToTitle,
|
||||||
role::{Alignment, Killer, Powerful, RoleTitle},
|
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -79,6 +79,8 @@ decl_icon!(
|
||||||
RedX: "/img/red-x.svg",
|
RedX: "/img/red-x.svg",
|
||||||
Traitor: "/img/traitor.svg",
|
Traitor: "/img/traitor.svg",
|
||||||
Bloodlet: "/img/bloodlet.svg",
|
Bloodlet: "/img/bloodlet.svg",
|
||||||
|
Drunk: "/img/drunk.svg",
|
||||||
|
Insane: "/img/insane.svg",
|
||||||
);
|
);
|
||||||
|
|
||||||
impl IconSource {
|
impl IconSource {
|
||||||
|
|
@ -95,7 +97,6 @@ impl IconSource {
|
||||||
pub enum IconType {
|
pub enum IconType {
|
||||||
List,
|
List,
|
||||||
Small,
|
Small,
|
||||||
RoleAdd,
|
|
||||||
Fit,
|
Fit,
|
||||||
Icon15Pct,
|
Icon15Pct,
|
||||||
Informational,
|
Informational,
|
||||||
|
|
@ -110,7 +111,6 @@ impl IconType {
|
||||||
IconType::Fit => "icon-fit",
|
IconType::Fit => "icon-fit",
|
||||||
IconType::List => "icon-in-list",
|
IconType::List => "icon-in-list",
|
||||||
IconType::Small => "icon",
|
IconType::Small => "icon",
|
||||||
IconType::RoleAdd => "icon-role-add",
|
|
||||||
IconType::Informational => "icon-info",
|
IconType::Informational => "icon-info",
|
||||||
IconType::RoleCheck => "check-icon",
|
IconType::RoleCheck => "check-icon",
|
||||||
}
|
}
|
||||||
|
|
@ -227,13 +227,13 @@ impl PartialAssociatedIcon for DiedToTitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialAssociatedIcon for Aura {
|
impl PartialAssociatedIcon for AuraTitle {
|
||||||
fn icon(&self) -> Option<IconSource> {
|
fn icon(&self) -> Option<IconSource> {
|
||||||
match self {
|
match self {
|
||||||
Aura::Traitor => Some(IconSource::Traitor),
|
AuraTitle::Traitor => Some(IconSource::Traitor),
|
||||||
Aura::Drunk => todo!(),
|
AuraTitle::Drunk => Some(IconSource::Drunk),
|
||||||
Aura::Insane => todo!(),
|
AuraTitle::Insane => Some(IconSource::Insane),
|
||||||
Aura::Bloodlet { .. } => Some(IconSource::Bloodlet),
|
AuraTitle::Bloodlet => Some(IconSource::Bloodlet),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ use std::rc::Rc;
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
aura::{Aura, AuraTitle},
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
||||||
message::{Identification, PlayerState, PublicIdentity},
|
message::{Identification, PlayerState, PublicIdentity},
|
||||||
|
|
@ -24,9 +25,11 @@ use werewolves_proto::{
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{
|
use crate::{
|
||||||
Button, ClickableField, Icon, IconSource, IconType, Identity, PartialAssociatedIcon,
|
class::Class,
|
||||||
client::Signin,
|
components::{
|
||||||
|
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon, client::Signin,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Properties)]
|
#[derive(Debug, PartialEq, Properties)]
|
||||||
|
|
@ -346,6 +349,7 @@ pub fn SettingsSlot(
|
||||||
}: &SettingsSlotProps,
|
}: &SettingsSlotProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let open = use_state(|| false);
|
let open = use_state(|| false);
|
||||||
|
let aura_open = use_state(|| false);
|
||||||
let open_update = open.setter();
|
let open_update = open.setter();
|
||||||
let update = update.clone();
|
let update = update.clone();
|
||||||
let update = Callback::from(move |act| {
|
let update = Callback::from(move |act| {
|
||||||
|
|
@ -362,8 +366,14 @@ pub fn SettingsSlot(
|
||||||
open_update.set(false);
|
open_update.set(false);
|
||||||
});
|
});
|
||||||
let assign_to = assign_to_submenu(players_for_assign, slot, &update, &open.setter());
|
let assign_to = assign_to_submenu(players_for_assign, slot, &update, &open.setter());
|
||||||
let options =
|
let options = setup_options_for_slot(
|
||||||
setup_options_for_slot(slot, &update, roles_in_setup, apprentice_open, open.clone());
|
slot,
|
||||||
|
&update,
|
||||||
|
roles_in_setup,
|
||||||
|
apprentice_open,
|
||||||
|
open.clone(),
|
||||||
|
aura_open.clone(),
|
||||||
|
);
|
||||||
let assign_text = slot
|
let assign_text = slot
|
||||||
.assign_to
|
.assign_to
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
|
@ -379,7 +389,7 @@ pub fn SettingsSlot(
|
||||||
.unwrap_or_else(|| html! {{"assign"}});
|
.unwrap_or_else(|| html! {{"assign"}});
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
<Button on_click={on_kick}>
|
<Button on_click={on_kick} classes={classes!("red")}>
|
||||||
{"remove"}
|
{"remove"}
|
||||||
</Button>
|
</Button>
|
||||||
<ClickableField
|
<ClickableField
|
||||||
|
|
@ -447,7 +457,71 @@ fn setup_options_for_slot(
|
||||||
roles_in_setup: &[RoleTitle],
|
roles_in_setup: &[RoleTitle],
|
||||||
open_apprentice_assign: UseStateHandle<bool>,
|
open_apprentice_assign: UseStateHandle<bool>,
|
||||||
slot_field_open: UseStateHandle<bool>,
|
slot_field_open: UseStateHandle<bool>,
|
||||||
|
open_aura_assign: UseStateHandle<bool>,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
|
let aura_assign = {
|
||||||
|
let options = AuraTitle::ALL
|
||||||
|
.into_iter()
|
||||||
|
.filter(AuraTitle::assignable)
|
||||||
|
// .map(AuraTitle::into_aura)
|
||||||
|
.filter(|aura| slot.role.title().can_assign_aura(*aura))
|
||||||
|
.map(|aura| {
|
||||||
|
let aura_active = slot.auras.contains(&aura);
|
||||||
|
let active_class = aura_active.then_some("active");
|
||||||
|
let toggle = {
|
||||||
|
let slot = slot.clone();
|
||||||
|
let update = update.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
let mut slot = slot.clone();
|
||||||
|
if aura_active {
|
||||||
|
slot.auras.retain(|a| *a != aura);
|
||||||
|
} else {
|
||||||
|
slot.auras.push(aura);
|
||||||
|
}
|
||||||
|
update.emit(SettingSlotAction::Update(slot))
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let icon = aura
|
||||||
|
.icon()
|
||||||
|
.map(|icon| {
|
||||||
|
html! {
|
||||||
|
// <div
|
||||||
|
<Icon source={icon} icon_type={IconType::Small}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
html! {
|
||||||
|
<div class="icon inactive"/>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
let aura_class = aura.class();
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<Button
|
||||||
|
on_click={toggle}
|
||||||
|
classes={classes!(active_class, "setup-aura", aura_class, "faint")}
|
||||||
|
>
|
||||||
|
<span class="aura-title">
|
||||||
|
{icon}
|
||||||
|
<span class="title">{aura.to_string()}</span>
|
||||||
|
<div class="icon inactive"/>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
options.is_empty().not().then(|| {
|
||||||
|
let options = options.into_iter().collect::<Html>();
|
||||||
|
html! {
|
||||||
|
<ClickableField
|
||||||
|
state={open_aura_assign}
|
||||||
|
options={options}
|
||||||
|
>
|
||||||
|
{"auras"}
|
||||||
|
</ClickableField>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
let setup_options_for_role = match &slot.role {
|
let setup_options_for_role = match &slot.role {
|
||||||
SetupRole::MasonLeader { recruits_available } => {
|
SetupRole::MasonLeader { recruits_available } => {
|
||||||
let next = {
|
let next = {
|
||||||
|
|
@ -660,5 +734,10 @@ fn setup_options_for_slot(
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
setup_options_for_role.unwrap_or_default()
|
html! {
|
||||||
|
<>
|
||||||
|
{aura_assign}
|
||||||
|
{setup_options_for_role}
|
||||||
|
</>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,23 +17,20 @@ use std::{collections::HashMap, rc::Rc};
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
character::{Character, CharacterId},
|
aura::AuraTitle, character::{Character, CharacterId}, game::{
|
||||||
game::{
|
|
||||||
GameTime, SetupRole,
|
GameTime, SetupRole,
|
||||||
night::changes::NightChange,
|
night::changes::NightChange,
|
||||||
story::{
|
story::{
|
||||||
DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult,
|
DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult,
|
||||||
},
|
},
|
||||||
},
|
}, role::Alignment
|
||||||
role::Alignment,
|
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{
|
use crate::components::{
|
||||||
CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon,
|
AuraSpan, CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon, attributes::{
|
||||||
attributes::{
|
|
||||||
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
|
@ -177,11 +174,12 @@ struct StoryNightChangeProps {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
||||||
match change {
|
match change {
|
||||||
|
|
||||||
NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
||||||
<>
|
<>
|
||||||
<CharacterCard faint=true char={character.clone()}/>
|
<CharacterCard faint=true char={character.clone()}/>
|
||||||
{"lost the"}
|
{"lost the"}
|
||||||
<crate::components::Aura aura={*aura}/>
|
<AuraSpan aura={aura.title()}/>
|
||||||
{"aura"}
|
{"aura"}
|
||||||
</>
|
</>
|
||||||
}).unwrap_or_default(),
|
}).unwrap_or_default(),
|
||||||
|
|
@ -190,7 +188,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
||||||
<>
|
<>
|
||||||
<CharacterCard faint=true char={target.clone()}/>
|
<CharacterCard faint=true char={target.clone()}/>
|
||||||
{"gained the"}
|
{"gained the"}
|
||||||
<crate::components::Aura aura={*aura}/>
|
<AuraSpan aura={aura.title()}/>
|
||||||
{"aura from"}
|
{"aura from"}
|
||||||
<CharacterCard faint=true char={source.clone()}/>
|
<CharacterCard faint=true char={source.clone()}/>
|
||||||
</>
|
</>
|
||||||
|
|
@ -278,7 +276,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
||||||
})
|
})
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
|
|
||||||
NightChange::HunterTarget { .. }
|
NightChange::HunterTarget { .. }
|
||||||
| NightChange::MasonRecruit { .. }
|
| NightChange::MasonRecruit { .. }
|
||||||
| NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
| NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
||||||
}
|
}
|
||||||
|
|
@ -293,6 +291,19 @@ struct StoryNightResultProps {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
||||||
match result {
|
match result {
|
||||||
|
StoryActionResult::ShiftFailed => html!{
|
||||||
|
<span>{"but it failed"}</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::Drunk => html! {
|
||||||
|
<span>
|
||||||
|
{"but got "}
|
||||||
|
<AuraSpan aura={AuraTitle::Drunk}/>
|
||||||
|
{" instead"}
|
||||||
|
</span>
|
||||||
|
},
|
||||||
|
StoryActionResult::BeholderSawEverything => html!{
|
||||||
|
<span>{"and saw everything 👁️"}</span>
|
||||||
|
},
|
||||||
StoryActionResult::BeholderSawNothing => html!{
|
StoryActionResult::BeholderSawNothing => html!{
|
||||||
<span>{"but saw nothing"}</span>
|
<span>{"but saw nothing"}</span>
|
||||||
},
|
},
|
||||||
|
|
@ -396,7 +407,7 @@ struct StoryNightChoiceProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightChoiceProps) -> Html {
|
fn StoryNightChoice(StoryNightChoiceProps { choice, characters}: &StoryNightChoiceProps) -> Html {
|
||||||
let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
let generate = |character_id: &CharacterId, chosen: &CharacterId, action: &'static str| {
|
||||||
characters
|
characters
|
||||||
.get(character_id)
|
.get(character_id)
|
||||||
|
|
@ -544,6 +555,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
StoryActionPrompt::Shapeshifter { character_id } => {
|
StoryActionPrompt::Shapeshifter { character_id } => {
|
||||||
|
if choice.result.is_none() {
|
||||||
|
return html!{};
|
||||||
|
}
|
||||||
characters.get(character_id).map(|shifter| {
|
characters.get(character_id).map(|shifter| {
|
||||||
html! {
|
html! {
|
||||||
<>
|
<>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
mod assets;
|
mod assets;
|
||||||
|
mod class;
|
||||||
mod clients;
|
mod clients;
|
||||||
mod storage;
|
mod storage;
|
||||||
mod components {
|
mod components {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
use werewolves_proto::aura::AuraTitle;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn DrunkPage() -> Html {
|
||||||
|
let icon = AuraTitle::Drunk.icon().unwrap_or(IconSource::Roleblock);
|
||||||
|
html! {
|
||||||
|
<div class="role-page">
|
||||||
|
<h1 class="drunk">{"DRUNK"}</h1>
|
||||||
|
<div class="information drunk faint">
|
||||||
|
<h2>{"YOU GOT DRUNK INSTEAD"}</h2>
|
||||||
|
<h4 class="icons">
|
||||||
|
<Icon source={icon} icon_type={IconType::Informational}/>
|
||||||
|
</h4>
|
||||||
|
<h3 class="yellow">{"YOUR NIGHT ACTION DID NOT TAKE PLACE"}</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -31,19 +31,21 @@ pub fn RoleChangePage(RoleChangePageProps { role }: &RoleChangePageProps) -> Htm
|
||||||
let class = Into::<SetupRole>::into(*role).category().class();
|
let class = Into::<SetupRole>::into(*role).category().class();
|
||||||
let icon = role.icon().map(|icon| {
|
let icon = role.icon().map(|icon| {
|
||||||
html! {
|
html! {
|
||||||
<h4 class="icons">
|
<div class="info-icon-grow">
|
||||||
<Icon source={icon} icon_type={IconType::Informational}/>
|
<Icon source={icon}/>
|
||||||
</h4>
|
</div>
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
html! {
|
html! {
|
||||||
<div class="role-page">
|
<div class="role-page">
|
||||||
<h1 class="intel">{"ROLE CHANGE"}</h1>
|
<h1 class={classes!(class)}>{"ROLE CHANGE"}</h1>
|
||||||
<div class={classes!("information", class, "faint")}>
|
<div class={classes!("information", class, "faint")}>
|
||||||
<h2>{"YOUR ROLE HAS CHANGED"}</h2>
|
<h2>{"YOUR ROLE HAS CHANGED"}</h2>
|
||||||
{icon}
|
{icon}
|
||||||
<h3 class="yellow">{"YOUR NEW ROLE IS"}</h3>
|
<h3>
|
||||||
<h3>{role.to_string().to_case(Case::Upper)}</h3>
|
<span>{"YOUR NEW ROLE IS "}</span>
|
||||||
|
<span class="yellow">{role.to_string().to_case(Case::Upper)}</span>
|
||||||
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,24 @@ pub fn BeholderSawNothing() -> Html {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn BeholderSawEverything() -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="role-page">
|
||||||
|
<h1 class="intel">{"BEHOLDER"}</h1>
|
||||||
|
<div class="information intel faint">
|
||||||
|
<h1>{"YOUR TARGET HAS DIED"}</h1>
|
||||||
|
<div class="info-icon-grow">
|
||||||
|
<Icon source={IconSource::Beholder}/>
|
||||||
|
</div>
|
||||||
|
<h1>
|
||||||
|
{"BUT SAW "}
|
||||||
|
<em class="red">
|
||||||
|
<strong>{"EVERYTHING"}</strong>
|
||||||
|
</em>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ pub struct MapleWolfPage1Props {
|
||||||
pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) -> Html {
|
pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) -> Html {
|
||||||
let starving = starving
|
let starving = starving
|
||||||
.then_some(html! {
|
.then_some(html! {
|
||||||
<>
|
<div>
|
||||||
<h3 class="red">{"YOU ARE STARVING"}</h3>
|
<h3 class="red">{"YOU ARE STARVING"}</h3>
|
||||||
<h3>{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}</h3>
|
<h3>{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}</h3>
|
||||||
</>
|
</div>
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
html! {
|
html! {
|
||||||
|
|
@ -45,8 +45,8 @@ pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) ->
|
||||||
<h2>
|
<h2>
|
||||||
{"YOU CAN CHOOSE TO EAT A PLAYER TONIGHT"}
|
{"YOU CAN CHOOSE TO EAT A PLAYER TONIGHT"}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="icons">
|
<div class="info-icon-grow">
|
||||||
<Icon source={IconSource::MapleWolf} icon_type={IconType::Informational}/>
|
<Icon source={IconSource::MapleWolf} icon_type={IconType::Fit}/>
|
||||||
</div>
|
</div>
|
||||||
{starving}
|
{starving}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,12 +12,11 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use core::ops::Not;
|
|
||||||
|
|
||||||
use werewolves_proto::role::Powerful;
|
use werewolves_proto::role::Powerful;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
use crate::components::{Icon, IconSource, IconType};
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn PowerSeerPage1() -> Html {
|
pub fn PowerSeerPage1() -> Html {
|
||||||
|
|
@ -26,10 +25,9 @@ pub fn PowerSeerPage1() -> Html {
|
||||||
<h1 class="intel">{"POWER SEER"}</h1>
|
<h1 class="intel">{"POWER SEER"}</h1>
|
||||||
<div class="information intel faint">
|
<div class="information intel faint">
|
||||||
<h2>{"PICK A PLAYER"}</h2>
|
<h2>{"PICK A PLAYER"}</h2>
|
||||||
<h4 class="icons">
|
<div class="info-icon-grow">
|
||||||
<Icon source={IconSource::Powerful} icon_type={IconType::Informational}/>
|
<Icon source={IconSource::PowerSeer} />
|
||||||
<Icon source={IconSource::Powerful} icon_type={IconType::Informational} inactive={true}/>
|
</div>
|
||||||
</h4>
|
|
||||||
<h3 class="yellow">{"YOU WILL CHECK IF THEY ARE POWERFUL"}</h3>
|
<h3 class="yellow">{"YOU WILL CHECK IF THEY ARE POWERFUL"}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Icon, IconSource, IconType};
|
use crate::components::{Icon, IconSource};
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn ProtectorPage1() -> Html {
|
pub fn ProtectorPage1() -> Html {
|
||||||
|
|
@ -23,9 +23,9 @@ pub fn ProtectorPage1() -> Html {
|
||||||
<h1 class="defensive">{"PROTECTOR"}</h1>
|
<h1 class="defensive">{"PROTECTOR"}</h1>
|
||||||
<div class="information defensive faint">
|
<div class="information defensive faint">
|
||||||
<h2>{"PICK A PLAYER"}</h2>
|
<h2>{"PICK A PLAYER"}</h2>
|
||||||
<h4 class="icons">
|
<div class="info-icon-grow">
|
||||||
<Icon source={IconSource::Shield} icon_type={IconType::Informational}/>
|
<Icon source={IconSource::Shield} />
|
||||||
</h4>
|
</div>
|
||||||
<h3 class="yellow">{"YOU WILL PROTECT THEM FROM A DEATH TONIGHT"}</h3>
|
<h3 class="yellow">{"YOU WILL PROTECT THEM FROM A DEATH TONIGHT"}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{Icon, IconSource, IconType};
|
use crate::components::{Icon, IconSource};
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn RoleblockPage() -> Html {
|
pub fn RoleblockPage() -> Html {
|
||||||
|
|
@ -23,9 +23,9 @@ pub fn RoleblockPage() -> Html {
|
||||||
<h1 class="wolves">{"ROLE BLOCKED"}</h1>
|
<h1 class="wolves">{"ROLE BLOCKED"}</h1>
|
||||||
<div class="information wolves faint">
|
<div class="information wolves faint">
|
||||||
<h2>{"YOU WERE ROLE BLOCKED"}</h2>
|
<h2>{"YOU WERE ROLE BLOCKED"}</h2>
|
||||||
<h4 class="icons">
|
<div class="info-icon-grow">
|
||||||
<Icon source={IconSource::Roleblock} icon_type={IconType::Informational}/>
|
<Icon source={IconSource::Roleblock} />
|
||||||
</h4>
|
</div>
|
||||||
<h3 class="yellow">{"YOUR NIGHT ACTION DID NOT TAKE PLACE"}</h3>
|
<h3 class="yellow">{"YOUR NIGHT ACTION DID NOT TAKE PLACE"}</h3>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use werewolves_proto::{
|
||||||
|
game::SetupRole,
|
||||||
|
role::{Alignment, RoleTitle},
|
||||||
|
};
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{AssociatedIcon, Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn ShiftFailed() -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="role-page">
|
||||||
|
<h1 class="wolves">{"SHIFT FAILED"}</h1>
|
||||||
|
<div class={classes!("information", "wolves", "faint")}>
|
||||||
|
<h2>{"YOUR SHIFT HAS FAILED"}</h2>
|
||||||
|
<div class="info-icon-grow">
|
||||||
|
<Icon source={IconSource::RedX}/>
|
||||||
|
</div>
|
||||||
|
<h3>
|
||||||
|
{"YOU RETAIN YOUR SHAPESHIFT ABILITY"}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn TraitorIntroPage() -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="role-page">
|
||||||
|
<h1 class="traitor">{"TRAITOR"}</h1>
|
||||||
|
<div class="information traitor faint">
|
||||||
|
<h2>{"YOU ARE A TRAITOR"}</h2>
|
||||||
|
<h3>{"YOU RETAIN YOUR ROLE AND WIN IF EVIL WINS"}</h3>
|
||||||
|
<h4>{"HOWEVER"}</h4>
|
||||||
|
<h2 class="yellow inline-icons">
|
||||||
|
{"YOU CONTRIBUTE TO VILLAGE PARITY"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue