Compare commits
6 Commits
f193e4e691
...
f711ca0a55
| Author | SHA1 | Date |
|---|---|---|
|
|
f711ca0a55 | |
|
|
ad7ffaac31 | |
|
|
99a2fa31c4 | |
|
|
1c3980d700 | |
|
|
b2a118a132 | |
|
|
ac4ce81638 |
|
|
@ -306,6 +306,15 @@ dependencies = [
|
|||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "convert_case"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db05ffb6856bf0ecdf6367558a76a0e8a77b1713044eb92845c692100ed50190"
|
||||
dependencies = [
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
version = "0.8.7"
|
||||
|
|
@ -2415,7 +2424,7 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"chrono",
|
||||
"ciborium",
|
||||
"convert_case",
|
||||
"convert_case 0.8.0",
|
||||
"futures",
|
||||
"getrandom 0.3.3",
|
||||
"gloo 0.11.0",
|
||||
|
|
@ -2441,7 +2450,8 @@ dependencies = [
|
|||
name = "werewolves-macros"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"convert_case",
|
||||
"chrono",
|
||||
"convert_case 0.9.0",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.106",
|
||||
|
|
|
|||
|
|
@ -10,4 +10,5 @@ proc-macro = true
|
|||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "2", features = ["full", "extra-traits"] }
|
||||
convert_case = { version = "0.8" }
|
||||
convert_case = { version = "0.9" }
|
||||
chrono = { version = "0.4" }
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use core::error::Error;
|
|||
use std::{
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
process::Command,
|
||||
};
|
||||
|
||||
use convert_case::Casing;
|
||||
|
|
@ -574,3 +575,73 @@ pub fn ref_and_mut(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
|||
let ref_and_mut = parse_macro_input!(input as RefAndMut);
|
||||
quote! {#ref_and_mut}.into()
|
||||
}
|
||||
#[proc_macro]
|
||||
pub fn build_dirty(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_dirty doesn't take arguments");
|
||||
}
|
||||
|
||||
let git_state = Command::new("git")
|
||||
.arg("diff")
|
||||
.arg("--stat")
|
||||
.output()
|
||||
.unwrap();
|
||||
if !git_state.status.success() {
|
||||
panic!("git diff --stat failed");
|
||||
}
|
||||
let dirty = !git_state.stdout.is_empty();
|
||||
|
||||
quote! {#dirty}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn build_id(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_id doesn't take arguments");
|
||||
}
|
||||
let output = Command::new("git")
|
||||
.arg("rev-parse")
|
||||
.arg("--short")
|
||||
.arg("HEAD")
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("git rev-parse --short HEAD failed");
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
let git_ref = output.trim();
|
||||
|
||||
quote! {#git_ref}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn build_id_long(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_id doesn't take arguments");
|
||||
}
|
||||
let output = Command::new("git")
|
||||
.arg("rev-parse")
|
||||
.arg("HEAD")
|
||||
.output()
|
||||
.unwrap();
|
||||
if !output.status.success() {
|
||||
panic!("git rev-parse --short HEAD failed");
|
||||
}
|
||||
|
||||
let output = String::from_utf8(output.stdout).unwrap();
|
||||
let git_ref = output.trim();
|
||||
|
||||
quote! {#git_ref}.into()
|
||||
}
|
||||
|
||||
#[proc_macro]
|
||||
pub fn build_time(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||
if !input.is_empty() {
|
||||
panic!("build_time doesn't take arguments");
|
||||
}
|
||||
|
||||
let time = chrono::Utc::now().format("%d %b %Y %T UTC").to_string();
|
||||
|
||||
quote! {#time}.into()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
// 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 quote::{ToTokens, quote};
|
||||
use syn::{parse::Parse, spanned::Spanned};
|
||||
|
||||
use syn::parse::Parse;
|
||||
#[allow(unused)]
|
||||
pub struct RefAndMut {
|
||||
name: syn::Ident,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ log = { version = "0.4" }
|
|||
serde_json = { version = "1.0" }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
uuid = { version = "1.17", features = ["v4", "serde"] }
|
||||
rand = { version = "0.9" }
|
||||
rand = { version = "0.9", features = ["std_rng"] }
|
||||
werewolves-macros = { path = "../werewolves-macros" }
|
||||
|
||||
[dev-dependencies]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use core::fmt::Display;
|
||||
|
||||
// Copyright (C) 2025 Emilis Bliūdžius
|
||||
//
|
||||
// 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
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use core::fmt::Display;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::ChecksAs;
|
||||
use werewolves_macros::{ChecksAs, Titles};
|
||||
|
||||
use crate::{
|
||||
bag::DrunkBag,
|
||||
game::{GameTime, Village},
|
||||
role::{Alignment, Killer, Powerful},
|
||||
team::Team,
|
||||
};
|
||||
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 {
|
||||
#[checks("assignable")]
|
||||
Traitor,
|
||||
#[checks("assignable")]
|
||||
#[checks("cleansible")]
|
||||
Drunk,
|
||||
Drunk(DrunkBag),
|
||||
#[checks("assignable")]
|
||||
Insane,
|
||||
#[checks("cleansible")]
|
||||
Bloodlet {
|
||||
night: u8,
|
||||
},
|
||||
Bloodlet { night: u8 },
|
||||
}
|
||||
|
||||
impl Display for Aura {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Aura::Traitor => "Traitor",
|
||||
Aura::Drunk => "Drunk",
|
||||
Aura::Drunk(_) => "Drunk",
|
||||
Aura::Insane => "Insane",
|
||||
Aura::Bloodlet { .. } => "Bloodlet",
|
||||
})
|
||||
|
|
@ -50,7 +52,7 @@ impl Display for Aura {
|
|||
impl Aura {
|
||||
pub const fn expired(&self, village: &Village) -> bool {
|
||||
match self {
|
||||
Aura::Traitor | Aura::Drunk | Aura::Insane => false,
|
||||
Aura::Traitor | Aura::Drunk(_) | Aura::Insane => false,
|
||||
Aura::Bloodlet {
|
||||
night: applied_night,
|
||||
} => match village.time() {
|
||||
|
|
@ -88,8 +90,16 @@ impl Auras {
|
|||
&self.0
|
||||
}
|
||||
|
||||
pub fn remove_aura(&mut self, aura: Aura) {
|
||||
self.0.retain(|a| *a != aura);
|
||||
pub fn list_mut(&mut self) -> &mut [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
|
||||
|
|
@ -129,7 +139,7 @@ impl Auras {
|
|||
match aura {
|
||||
Aura::Traitor => return Some(Alignment::Traitor),
|
||||
Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
||||
Aura::Drunk | Aura::Insane => {}
|
||||
Aura::Drunk(_) | Aura::Insane => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
|
|
@ -151,3 +161,14 @@ impl Auras {
|
|||
.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,156 @@
|
|||
// 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};
|
||||
|
||||
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 crate::{
|
||||
aura::{Aura, Auras},
|
||||
aura::{Aura, AuraTitle, Auras},
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{GameTime, Village},
|
||||
|
|
@ -207,6 +207,10 @@ impl Character {
|
|||
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<()> {
|
||||
let mut role = new_role.title_to_role_excl_apprentice();
|
||||
core::mem::swap(&mut role, &mut self.role);
|
||||
|
|
@ -244,9 +248,9 @@ impl Character {
|
|||
)
|
||||
}
|
||||
|
||||
fn mason_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
||||
fn mason_prompts(&self, village: &Village) -> Result<Vec<ActionPrompt>> {
|
||||
if !self.role.wakes(village) {
|
||||
return Ok(Box::new([]));
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
let (recruits, recruits_available) = match &self.role {
|
||||
Role::MasonLeader {
|
||||
|
|
@ -304,22 +308,31 @@ impl Character {
|
|||
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);
|
||||
}
|
||||
|
||||
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
||||
let mut prompts = Vec::new();
|
||||
if self.mason_leader().is_ok() {
|
||||
return self.mason_prompts(village);
|
||||
}
|
||||
if !self.alive() || !self.role.wakes(village) {
|
||||
return Ok(Box::new([]));
|
||||
// add them here so masons wake up even with a dead leader
|
||||
prompts.append(&mut self.mason_prompts(village)?);
|
||||
}
|
||||
let night = match village.time() {
|
||||
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||
GameTime::Night { number } => number,
|
||||
};
|
||||
Ok(Box::new([match &self.role {
|
||||
if night == 0 && self.auras.list().contains(&Aura::Traitor) {
|
||||
log::info!("adding traitor prompt for {}", self.identity());
|
||||
prompts.push(ActionPrompt::TraitorIntro {
|
||||
character_id: self.identity(),
|
||||
});
|
||||
}
|
||||
if !self.alive() || !self.role.wakes(village) {
|
||||
return Ok(prompts.into_boxed_slice());
|
||||
}
|
||||
|
||||
match &self.role {
|
||||
Role::Empath { cursed: true }
|
||||
| Role::Diseased
|
||||
| Role::Weightlifter
|
||||
|
|
@ -334,11 +347,11 @@ impl Character {
|
|||
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(),
|
||||
},
|
||||
}),
|
||||
|
||||
Role::Scapegoat { redeemed: true } => {
|
||||
let mut dead = village.dead_characters();
|
||||
|
|
@ -347,99 +360,94 @@ impl Character {
|
|||
.into_iter()
|
||||
.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(),
|
||||
new_role: pr,
|
||||
}
|
||||
} else {
|
||||
return Ok(Box::new([]));
|
||||
});
|
||||
}
|
||||
}
|
||||
Role::Bloodletter => ActionPrompt::Bloodletter {
|
||||
Role::Bloodletter => prompts.push(ActionPrompt::Bloodletter {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_villagers(),
|
||||
marked: None,
|
||||
},
|
||||
Role::Seer => ActionPrompt::Seer {
|
||||
}),
|
||||
Role::Seer => prompts.push(ActionPrompt::Seer {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::Arcanist => ActionPrompt::Arcanist {
|
||||
}),
|
||||
Role::Arcanist => prompts.push(ActionPrompt::Arcanist {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: (None, None),
|
||||
},
|
||||
}),
|
||||
Role::Protector {
|
||||
last_protected: Some(last_protected),
|
||||
} => ActionPrompt::Protector {
|
||||
} => prompts.push(ActionPrompt::Protector {
|
||||
character_id: self.identity(),
|
||||
targets: village.living_players_excluding(*last_protected),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::Protector {
|
||||
last_protected: None,
|
||||
} => ActionPrompt::Protector {
|
||||
} => prompts.push(ActionPrompt::Protector {
|
||||
character_id: self.identity(),
|
||||
targets: village.living_players_excluding(self.character_id()),
|
||||
targets: village.living_players(),
|
||||
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 {
|
||||
knows_on_night,
|
||||
woken_for_reveal: false,
|
||||
..
|
||||
} => {
|
||||
let current_night = match village.time() {
|
||||
GameTime::Day { number: _ } => return Ok(Box::new([])),
|
||||
GameTime::Night { number } => number,
|
||||
};
|
||||
return Ok((current_night >= knows_on_night.get())
|
||||
.then_some({
|
||||
ActionPrompt::ElderReveal {
|
||||
} => match village.time() {
|
||||
GameTime::Day { number: _ } => {}
|
||||
GameTime::Night { number } => {
|
||||
if number >= knows_on_night.get() {
|
||||
prompts.push(ActionPrompt::ElderReveal {
|
||||
character_id: self.identity(),
|
||||
}
|
||||
})
|
||||
.into_iter()
|
||||
.collect());
|
||||
}
|
||||
Role::Militia { targeted: None } => ActionPrompt::Militia {
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
Role::Militia { targeted: None } => prompts.push(ActionPrompt::Militia {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::Werewolf => ActionPrompt::WolfPackKill {
|
||||
}),
|
||||
Role::Werewolf => prompts.push(ActionPrompt::WolfPackKill {
|
||||
living_villagers: village.living_players(),
|
||||
marked: None,
|
||||
},
|
||||
Role::AlphaWolf { killed: None } => ActionPrompt::AlphaWolf {
|
||||
}),
|
||||
Role::AlphaWolf { killed: None } => prompts.push(ActionPrompt::AlphaWolf {
|
||||
character_id: self.identity(),
|
||||
living_villagers: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::DireWolf {
|
||||
last_blocked: Some(last_blocked),
|
||||
} => ActionPrompt::DireWolf {
|
||||
} => prompts.push(ActionPrompt::DireWolf {
|
||||
character_id: self.identity(),
|
||||
living_players: village
|
||||
.living_players_excluding(self.character_id())
|
||||
|
|
@ -447,137 +455,124 @@ impl Character {
|
|||
.filter(|c| c.character_id != *last_blocked)
|
||||
.collect(),
|
||||
marked: None,
|
||||
},
|
||||
Role::DireWolf { .. } => ActionPrompt::DireWolf {
|
||||
}),
|
||||
Role::DireWolf { .. } => prompts.push(ActionPrompt::DireWolf {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::Shapeshifter { shifted_into: None } => ActionPrompt::Shapeshifter {
|
||||
}),
|
||||
Role::Shapeshifter { shifted_into: None } => prompts.push(ActionPrompt::Shapeshifter {
|
||||
character_id: self.identity(),
|
||||
},
|
||||
}),
|
||||
Role::Gravedigger => {
|
||||
let dead = village.dead_targets();
|
||||
if dead.is_empty() {
|
||||
return Ok(Box::new([]));
|
||||
}
|
||||
ActionPrompt::Gravedigger {
|
||||
character_id: self.identity(),
|
||||
dead_players: village.dead_targets(),
|
||||
marked: None,
|
||||
if !dead.is_empty() {
|
||||
prompts.push(ActionPrompt::Gravedigger {
|
||||
character_id: self.identity(),
|
||||
dead_players: village.dead_targets(),
|
||||
marked: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Role::Hunter { target } => ActionPrompt::Hunter {
|
||||
Role::Hunter { target } => prompts.push(ActionPrompt::Hunter {
|
||||
character_id: self.identity(),
|
||||
current_target: target.as_ref().and_then(|t| village.target_by_id(*t).ok()),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::MapleWolf { last_kill_on_night } => ActionPrompt::MapleWolf {
|
||||
}),
|
||||
Role::MapleWolf { last_kill_on_night } => prompts.push(ActionPrompt::MapleWolf {
|
||||
character_id: self.identity(),
|
||||
kill_or_die: last_kill_on_night + MAPLE_WOLF_ABSTAIN_LIMIT.get() == night,
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::Guardian {
|
||||
last_protected: Some(PreviousGuardianAction::Guard(prev_target)),
|
||||
} => ActionPrompt::Guardian {
|
||||
} => prompts.push(ActionPrompt::Guardian {
|
||||
character_id: self.identity(),
|
||||
previous: Some(PreviousGuardianAction::Guard(prev_target.clone())),
|
||||
living_players: village.living_players_excluding(prev_target.character_id),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::Guardian {
|
||||
last_protected: Some(PreviousGuardianAction::Protect(prev_target)),
|
||||
} => ActionPrompt::Guardian {
|
||||
} => prompts.push(ActionPrompt::Guardian {
|
||||
character_id: self.identity(),
|
||||
previous: Some(PreviousGuardianAction::Protect(prev_target.clone())),
|
||||
living_players: village.living_players(),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::Guardian {
|
||||
last_protected: None,
|
||||
} => ActionPrompt::Guardian {
|
||||
} => prompts.push(ActionPrompt::Guardian {
|
||||
character_id: self.identity(),
|
||||
previous: None,
|
||||
living_players: village.living_players(),
|
||||
marked: None,
|
||||
},
|
||||
Role::Adjudicator => ActionPrompt::Adjudicator {
|
||||
}),
|
||||
Role::Adjudicator => prompts.push(ActionPrompt::Adjudicator {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::PowerSeer => ActionPrompt::PowerSeer {
|
||||
}),
|
||||
Role::PowerSeer => prompts.push(ActionPrompt::PowerSeer {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::Mortician => ActionPrompt::Mortician {
|
||||
character_id: self.identity(),
|
||||
dead_players: {
|
||||
let dead = village.dead_targets();
|
||||
if dead.is_empty() {
|
||||
return Ok(Box::new([]));
|
||||
}
|
||||
dead
|
||||
},
|
||||
marked: None,
|
||||
},
|
||||
Role::Beholder => ActionPrompt::Beholder {
|
||||
}),
|
||||
Role::Mortician => {
|
||||
let dead = village.dead_targets();
|
||||
if !dead.is_empty() {
|
||||
prompts.push(ActionPrompt::Mortician {
|
||||
character_id: self.identity(),
|
||||
dead_players: dead,
|
||||
marked: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
Role::Beholder => prompts.push(ActionPrompt::Beholder {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::MasonLeader { .. } => {
|
||||
log::error!(
|
||||
"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(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
}),
|
||||
Role::Vindicator => {
|
||||
let last_day = match village.time() {
|
||||
GameTime::Day { .. } => {
|
||||
log::error!(
|
||||
"vindicator trying to get a prompt during the day? village state: {village:?}"
|
||||
);
|
||||
return Ok(Box::new([]));
|
||||
}
|
||||
GameTime::Night { number } => {
|
||||
if number == 0 {
|
||||
return Ok(Box::new([]));
|
||||
}
|
||||
NonZeroU8::new(number).unwrap()
|
||||
}
|
||||
};
|
||||
return Ok(village
|
||||
.executions_on_day(last_day)
|
||||
.iter()
|
||||
.any(|c| c.is_village())
|
||||
.then(|| ActionPrompt::Vindicator {
|
||||
if night != 0
|
||||
&& let Some(last_day) = NonZeroU8::new(night)
|
||||
&& village
|
||||
.executions_on_day(last_day)
|
||||
.iter()
|
||||
.any(|c| c.is_village())
|
||||
{
|
||||
prompts.push(ActionPrompt::Vindicator {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
})
|
||||
.into_iter()
|
||||
.collect());
|
||||
});
|
||||
}
|
||||
}
|
||||
Role::PyreMaster { .. } => ActionPrompt::PyreMaster {
|
||||
|
||||
Role::PyreMaster { .. } => prompts.push(ActionPrompt::PyreMaster {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
Role::LoneWolf => ActionPrompt::LoneWolfKill {
|
||||
}),
|
||||
Role::LoneWolf => prompts.push(ActionPrompt::LoneWolfKill {
|
||||
character_id: self.identity(),
|
||||
living_players: village.living_players_excluding(self.character_id()),
|
||||
marked: None,
|
||||
},
|
||||
}]))
|
||||
}),
|
||||
}
|
||||
Ok(prompts.into_boxed_slice())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
@ -830,6 +825,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 {
|
||||
self.role.initial_shown_role()
|
||||
}
|
||||
|
|
@ -872,6 +889,8 @@ decl_ref_and_mut!(
|
|||
Guardian, GuardianMut: Option<PreviousGuardianAction>;
|
||||
Direwolf, DirewolfMut: Option<CharacterId>;
|
||||
Militia, MilitiaMut: Option<CharacterId>;
|
||||
MapleWolf, MapleWolfMut: u8;
|
||||
Protector, ProtectorMut: Option<CharacterId>;
|
||||
);
|
||||
|
||||
pub struct BlackKnightKill<'a> {
|
||||
|
|
|
|||
|
|
@ -73,6 +73,8 @@ pub enum GameError {
|
|||
NightNeedsNext,
|
||||
#[error("night zero actions can only be obtained on night zero")]
|
||||
NotNightZero,
|
||||
#[error("this action cannot happen on night zero")]
|
||||
CannotHappenOnNightZero,
|
||||
#[error("wolves intro in progress")]
|
||||
WolvesIntroInProgress,
|
||||
#[error("a game is still ongoing")]
|
||||
|
|
@ -97,4 +99,10 @@ pub enum GameError {
|
|||
NoPreviousDuringDay,
|
||||
#[error("militia already spent")]
|
||||
MilitiaSpent,
|
||||
#[error("this role doesn't mark anyone")]
|
||||
RoleDoesntMark,
|
||||
#[error("cannot shapeshift on a non-shapeshifter prompt")]
|
||||
ShapeshiftingIsForShapeshifters,
|
||||
#[error("must select a target")]
|
||||
MustSelectTarget,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,14 +12,17 @@
|
|||
//
|
||||
// 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, ops::Not};
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{Village, night::changes::ChangesLookup},
|
||||
game::{
|
||||
Village,
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
},
|
||||
player::Protection,
|
||||
};
|
||||
|
||||
|
|
@ -36,7 +39,11 @@ pub enum KillOutcome {
|
|||
}
|
||||
|
||||
impl KillOutcome {
|
||||
pub fn apply_to_village(self, village: &mut Village) -> Result<()> {
|
||||
pub fn apply_to_village(
|
||||
self,
|
||||
village: &mut Village,
|
||||
recorded_changes: Option<&mut Vec<NightChange>>,
|
||||
) -> Result<()> {
|
||||
match self {
|
||||
KillOutcome::Single(character_id, died_to) => {
|
||||
village
|
||||
|
|
@ -64,15 +71,22 @@ impl KillOutcome {
|
|||
// check if guardian exists before we mutably borrow killer, which would
|
||||
// prevent us from borrowing village to check after.
|
||||
village.character_by_id(guardian)?;
|
||||
let guardian_kill = DiedTo::GuardianProtecting {
|
||||
night,
|
||||
source: guardian,
|
||||
protecting: original_target,
|
||||
protecting_from: original_killer,
|
||||
protecting_from_cause: Box::new(original_kill.clone()),
|
||||
};
|
||||
if let Some(recorded_changes) = recorded_changes {
|
||||
recorded_changes.push(NightChange::Kill {
|
||||
target: original_killer,
|
||||
died_to: guardian_kill.clone(),
|
||||
});
|
||||
}
|
||||
village
|
||||
.character_by_id_mut(original_killer)?
|
||||
.kill(DiedTo::GuardianProtecting {
|
||||
night,
|
||||
source: guardian,
|
||||
protecting: original_target,
|
||||
protecting_from: original_killer,
|
||||
protecting_from_cause: Box::new(original_kill.clone()),
|
||||
});
|
||||
.kill(guardian_kill);
|
||||
village.character_by_id_mut(guardian)?.kill(original_kill);
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -108,7 +122,7 @@ fn resolve_protection(
|
|||
}
|
||||
|
||||
pub fn resolve_kill(
|
||||
changes: &mut ChangesLookup<'_>,
|
||||
changes: &ChangesLookup<'_>,
|
||||
target: CharacterId,
|
||||
died_to: &DiedTo,
|
||||
night: u8,
|
||||
|
|
@ -119,7 +133,7 @@ pub fn resolve_kill(
|
|||
night,
|
||||
starves_if_fails: true,
|
||||
} = died_to
|
||||
&& let Some(protection) = changes.protected_take(target)
|
||||
&& let Some(protection) = changes.protected(target)
|
||||
{
|
||||
return Ok(Some(
|
||||
resolve_protection(*source, died_to, target, &protection, *night).unwrap_or(
|
||||
|
|
@ -135,7 +149,7 @@ pub fn resolve_kill(
|
|||
{
|
||||
let killing_wolf = village.character_by_id(*killing_wolf)?;
|
||||
|
||||
match changes.protected_take(target) {
|
||||
match changes.protected(target) {
|
||||
Some(protection) => {
|
||||
return Ok(resolve_protection(
|
||||
killing_wolf.character_id(),
|
||||
|
|
@ -148,7 +162,7 @@ pub fn resolve_kill(
|
|||
None => {
|
||||
// Wolf kill went through -- can kill shifter
|
||||
return Ok(Some(KillOutcome::Single(
|
||||
*ss_source,
|
||||
ss_source,
|
||||
DiedTo::Shapeshift {
|
||||
into: target,
|
||||
night: *night,
|
||||
|
|
@ -158,7 +172,7 @@ pub fn resolve_kill(
|
|||
};
|
||||
}
|
||||
|
||||
let protection = match changes.protected_take(target) {
|
||||
let protection = match changes.protected(target) {
|
||||
Some(prot) => prot,
|
||||
None => return Ok(Some(KillOutcome::Single(target, died_to.clone()))),
|
||||
};
|
||||
|
|
@ -173,7 +187,7 @@ pub fn resolve_kill(
|
|||
.ok_or(GameError::GuardianInvalidOriginalKill)?,
|
||||
original_target: target,
|
||||
original_kill: died_to.clone(),
|
||||
guardian: source,
|
||||
guardian: *source,
|
||||
night: NonZeroU8::new(night).unwrap(),
|
||||
})),
|
||||
Protection::Guardian {
|
||||
|
|
|
|||
|
|
@ -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> {
|
||||
match (&mut self.state, message) {
|
||||
(GameState::Night { night }, HostGameMessage::Night(HostNightMessage::NextPage)) => {
|
||||
|
|
@ -102,7 +111,6 @@ impl Game {
|
|||
(GameState::Day { village, marked }, HostGameMessage::Day(HostDayMessage::Execute)) => {
|
||||
let time = village.time();
|
||||
if let Some(outcome) = village.execute(marked)? {
|
||||
log::warn!("adding to history for {}", village.time());
|
||||
self.history.add(
|
||||
village.time(),
|
||||
GameActions::DayDetails(
|
||||
|
|
@ -112,7 +120,6 @@ impl Game {
|
|||
return Ok(ServerToHostMessage::GameOver(outcome));
|
||||
}
|
||||
let night = Night::new(village.clone())?;
|
||||
log::warn!("adding to history for {time}");
|
||||
self.history.add(
|
||||
time,
|
||||
GameActions::DayDetails(
|
||||
|
|
@ -162,13 +169,13 @@ impl Game {
|
|||
Ok(_) => self.process(HostGameMessage::GetState),
|
||||
Err(GameError::NightOver) => {
|
||||
let changes = night.collect_changes()?;
|
||||
let village = night.village().with_night_changes(&changes)?;
|
||||
log::warn!("adding to history for {}", night.village().time());
|
||||
let (village, recorded_changes) =
|
||||
night.village().with_night_changes(&changes)?;
|
||||
self.history.add(
|
||||
night.village().time(),
|
||||
GameActions::NightDetails(NightDetails::new(
|
||||
&night.used_actions(),
|
||||
changes,
|
||||
recorded_changes,
|
||||
)),
|
||||
)?;
|
||||
self.state = GameState::Day {
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
// 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 mod changes;
|
||||
mod next;
|
||||
mod process;
|
||||
|
||||
use core::num::NonZeroU8;
|
||||
|
|
@ -27,7 +28,7 @@ use crate::{
|
|||
error::GameError,
|
||||
game::{
|
||||
GameTime, Village,
|
||||
kill::{self},
|
||||
kill::{self, KillOutcome},
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
},
|
||||
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||
|
|
@ -58,7 +59,8 @@ impl From<ActionComplete> for ResponseOutcome {
|
|||
impl ActionPrompt {
|
||||
fn unless(&self) -> Option<Unless> {
|
||||
match &self {
|
||||
ActionPrompt::Insomniac { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
|
|
@ -532,13 +534,7 @@ impl Night {
|
|||
// role change associated with the shapeshift
|
||||
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_changes = Vec::new();
|
||||
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
|
||||
.changes_from_actions()
|
||||
.into_iter()
|
||||
|
|
@ -616,7 +612,7 @@ impl Night {
|
|||
_ => false,
|
||||
}) {
|
||||
// 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| {
|
||||
|
|
@ -667,7 +663,7 @@ impl Night {
|
|||
}
|
||||
}
|
||||
self.action_queue = new_queue;
|
||||
Ok(())
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
pub const fn page(&self) -> Option<usize> {
|
||||
|
|
@ -681,8 +677,8 @@ impl Night {
|
|||
match &mut self.night_state {
|
||||
NightState::Active { current_result, .. } => match current_result {
|
||||
CurrentResult::None => self.received_response(ActionResponse::Continue),
|
||||
CurrentResult::Result(ActionResult::Continue)
|
||||
| CurrentResult::GoBackToSleepAfterShown { .. }
|
||||
CurrentResult::GoBackToSleepAfterShown { .. }
|
||||
| CurrentResult::Result(ActionResult::Continue)
|
||||
| CurrentResult::Result(ActionResult::GoBackToSleep) => {
|
||||
Err(GameError::NightNeedsNext)
|
||||
}
|
||||
|
|
@ -697,7 +693,24 @@ 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> {
|
||||
if let ActionResponse::ContinueToResult = &resp
|
||||
&& let Some(result) = self.current_result()
|
||||
{
|
||||
return Ok(ServerAction::Result(result.clone()));
|
||||
}
|
||||
match self.received_response_with_role_blocks(resp)? {
|
||||
BlockResolvedOutcome::PromptUpdate(prompt) => match &mut self.night_state {
|
||||
NightState::Active {
|
||||
|
|
@ -715,18 +728,18 @@ impl Night {
|
|||
NightState::Complete => Err(GameError::NightOver),
|
||||
},
|
||||
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
||||
match &mut self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt: _,
|
||||
current_result,
|
||||
..
|
||||
} => *current_result = result.clone().into(),
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
};
|
||||
self.set_current_result(result.clone().into())?;
|
||||
if let NightChange::Shapeshift { source, .. } = &change {
|
||||
// needs to be resolved _now_ so that the target can be woken
|
||||
// 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(
|
||||
self.action_queue
|
||||
.iter()
|
||||
|
|
@ -766,10 +779,10 @@ impl Night {
|
|||
}
|
||||
}
|
||||
|
||||
fn received_response_consecutive_same_player_no_sleep(
|
||||
fn outcome_consecutive_same_player_no_sleep(
|
||||
&self,
|
||||
resp: ActionResponse,
|
||||
) -> Result<ResponseOutcome> {
|
||||
outcome: ResponseOutcome,
|
||||
) -> ResponseOutcome {
|
||||
let same_char = self
|
||||
.current_character_id()
|
||||
.and_then(|curr| {
|
||||
|
|
@ -780,25 +793,31 @@ impl Night {
|
|||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
match (
|
||||
self.received_response_consecutive_wolves_dont_sleep(resp)?,
|
||||
same_char,
|
||||
) {
|
||||
(ResponseOutcome::PromptUpdate(p), _) => Ok(ResponseOutcome::PromptUpdate(p)),
|
||||
match (outcome, same_char) {
|
||||
(ResponseOutcome::PromptUpdate(p), _) => ResponseOutcome::PromptUpdate(p),
|
||||
(
|
||||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change,
|
||||
}),
|
||||
true,
|
||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
) => ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Continue,
|
||||
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(
|
||||
&self,
|
||||
resp: ActionResponse,
|
||||
|
|
@ -826,7 +845,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::ActionComplete(ActionComplete {
|
||||
|
|
@ -911,6 +934,12 @@ impl Night {
|
|||
&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> {
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
|
|
@ -957,6 +986,11 @@ impl Night {
|
|||
.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 {
|
||||
matches!(self.night_state, NightState::Complete)
|
||||
}
|
||||
|
|
@ -968,99 +1002,24 @@ impl Night {
|
|||
.collect()
|
||||
}
|
||||
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Result<()> {
|
||||
match &self.night_state {
|
||||
pub fn append_change(&mut self, change: NightChange) -> Result<()> {
|
||||
match &mut self.night_state {
|
||||
NightState::Active {
|
||||
current_prompt,
|
||||
current_result: CurrentResult::Result(ActionResult::Continue),
|
||||
current_changes,
|
||||
..
|
||||
current_changes, ..
|
||||
} => {
|
||||
self.used_actions.push((
|
||||
current_prompt.clone(),
|
||||
ActionResult::Continue,
|
||||
current_changes.clone(),
|
||||
));
|
||||
current_changes.push(change);
|
||||
Ok(())
|
||||
}
|
||||
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),
|
||||
NightState::Complete => 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
|
||||
/// state of the night
|
||||
fn dies_tonight(&self, character_id: CharacterId) -> Result<bool> {
|
||||
/// state of the night and returns the [DiedTo] cause of death
|
||||
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
||||
let ch = self.current_changes();
|
||||
let mut changes = ChangesLookup::new(&ch);
|
||||
if let Some(died_to) = changes.killed(character_id)
|
||||
&& kill::resolve_kill(
|
||||
&mut changes,
|
||||
character_id,
|
||||
died_to,
|
||||
self.night,
|
||||
&self.village,
|
||||
)?
|
||||
.is_some()
|
||||
{
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
changes.died_to(character_id, self.night, &self.village)
|
||||
}
|
||||
|
||||
/// returns the matching [Character] with the current night's aura changes
|
||||
|
|
@ -1085,6 +1044,16 @@ impl Night {
|
|||
.iter()
|
||||
.flat_map(|(_, _, act)| act.iter())
|
||||
.cloned()
|
||||
.chain(
|
||||
match &self.night_state {
|
||||
NightState::Active {
|
||||
current_changes, ..
|
||||
} => Some(current_changes.iter().cloned()),
|
||||
NightState::Complete => None,
|
||||
}
|
||||
.into_iter()
|
||||
.flatten(),
|
||||
)
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -1092,6 +1061,9 @@ impl Night {
|
|||
Visits::new(
|
||||
self.used_actions
|
||||
.iter()
|
||||
.filter(|(_, result, _)| {
|
||||
!matches!(result, ActionResult::Drunk | ActionResult::RoleBlocked)
|
||||
})
|
||||
.filter_map(|(prompt, _, _)| match prompt {
|
||||
ActionPrompt::Arcanist {
|
||||
character_id,
|
||||
|
|
@ -1202,7 +1174,8 @@ impl Night {
|
|||
..
|
||||
} => (*marked == visit_char).then(|| character_id.clone()),
|
||||
|
||||
ActionPrompt::Bloodletter { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Bloodletter { .. }
|
||||
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||
| ActionPrompt::Arcanist { marked: _, .. }
|
||||
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@
|
|||
//
|
||||
// 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::ops::Not;
|
||||
use core::{num::NonZeroU8, ops::Not};
|
||||
|
||||
use super::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::Extract;
|
||||
|
||||
|
|
@ -21,6 +22,10 @@ use crate::{
|
|||
aura::Aura,
|
||||
character::CharacterId,
|
||||
diedto::DiedTo,
|
||||
game::{
|
||||
Village,
|
||||
kill::{self, KillOutcome},
|
||||
},
|
||||
player::Protection,
|
||||
role::{RoleBlock, RoleTitle},
|
||||
};
|
||||
|
|
@ -71,6 +76,29 @@ pub enum NightChange {
|
|||
},
|
||||
}
|
||||
|
||||
impl NightChange {
|
||||
pub const fn target(&self) -> Option<CharacterId> {
|
||||
match self {
|
||||
NightChange::HunterTarget { target, .. }
|
||||
| NightChange::Kill { target, .. }
|
||||
| NightChange::RoleBlock { target, .. }
|
||||
| NightChange::Shapeshift { into: target, .. }
|
||||
| NightChange::Protection { target, .. }
|
||||
| NightChange::MasonRecruit {
|
||||
recruiting: target, ..
|
||||
}
|
||||
| NightChange::EmpathFoundScapegoat {
|
||||
scapegoat: target, ..
|
||||
}
|
||||
| NightChange::ApplyAura { target, .. } => Some(*target),
|
||||
|
||||
NightChange::ElderReveal { .. }
|
||||
| NightChange::RoleChange(..)
|
||||
| NightChange::LostAura { .. } => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
||||
|
||||
impl<'a> ChangesLookup<'a> {
|
||||
|
|
@ -78,6 +106,67 @@ impl<'a> ChangesLookup<'a> {
|
|||
Self(changes, Vec::new())
|
||||
}
|
||||
|
||||
pub fn collect_remaining(&self) -> Box<[NightChange]> {
|
||||
self.0
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, c)| self.1.contains(&idx).not().then_some(c))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn kill_outcomes(&self, night: u8, village: &Village) -> Result<Box<[KillOutcome]>> {
|
||||
self.0
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
NightChange::Kill { target, died_to } => Some((*target, died_to.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.map(|(kill_target, died_to)| {
|
||||
kill::resolve_kill(self, kill_target, &died_to, night, village)
|
||||
})
|
||||
.filter_map(|result| match result {
|
||||
Ok(Some(outcome)) => Some(Ok(outcome)),
|
||||
Ok(None) => None,
|
||||
Err(err) => Some(Err(err)),
|
||||
})
|
||||
.collect::<Result<Box<[_]>>>()
|
||||
}
|
||||
|
||||
pub fn died_to(
|
||||
&self,
|
||||
character_id: CharacterId,
|
||||
night: u8,
|
||||
village: &Village,
|
||||
) -> Result<Option<DiedTo>> {
|
||||
let kill_outcomes = self.kill_outcomes(night, village)?;
|
||||
|
||||
Ok(kill_outcomes.into_iter().find_map(|outcome| match outcome {
|
||||
KillOutcome::Single(target, died_to) => (target == character_id).then_some(died_to),
|
||||
KillOutcome::Guarding {
|
||||
original_killer,
|
||||
original_target,
|
||||
original_kill,
|
||||
guardian,
|
||||
night,
|
||||
} => {
|
||||
if original_killer == character_id {
|
||||
Some(DiedTo::GuardianProtecting {
|
||||
source: guardian,
|
||||
protecting: original_target,
|
||||
protecting_from: original_killer,
|
||||
protecting_from_cause: Box::new(original_kill),
|
||||
night,
|
||||
})
|
||||
} else if guardian == character_id {
|
||||
Some(original_kill)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn killed(&self, target: CharacterId) -> Option<&'a DiedTo> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
|
|
@ -111,7 +200,7 @@ impl<'a> ChangesLookup<'a> {
|
|||
None
|
||||
}
|
||||
}
|
||||
pub fn protected(&self, target: &CharacterId) -> Option<&'a Protection> {
|
||||
pub fn protected(&self, target: CharacterId) -> Option<&'a Protection> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
|
|
@ -120,20 +209,27 @@ impl<'a> ChangesLookup<'a> {
|
|||
NightChange::Protection {
|
||||
target: t,
|
||||
protection,
|
||||
} => (t == target).then_some(protection),
|
||||
} => (*t == target).then_some(protection),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shapeshifter(&self) -> Option<&'a CharacterId> {
|
||||
pub fn shapeshifter(&self) -> Option<CharacterId> {
|
||||
self.shapeshift_change().and_then(|c| match c {
|
||||
NightChange::Shapeshift { source, .. } => Some(source),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn shapeshift_change(&self) -> Option<NightChange> {
|
||||
self.0.iter().enumerate().find_map(|(idx, c)| {
|
||||
self.1
|
||||
.contains(&idx)
|
||||
.not()
|
||||
.then_some(match c {
|
||||
NightChange::Shapeshift { source, .. } => Some(source),
|
||||
NightChange::Shapeshift { .. } => Some(c.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.flatten()
|
||||
|
|
|
|||
|
|
@ -0,0 +1,212 @@
|
|||
// 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::{
|
||||
character::CharacterId,
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::night::{CurrentResult, Night, NightState, changes::NightChange},
|
||||
message::night::{ActionPrompt, ActionResult},
|
||||
role::{RoleBlock, RoleTitle},
|
||||
};
|
||||
|
||||
use super::Result;
|
||||
impl Night {
|
||||
#[allow(clippy::should_implement_trait)]
|
||||
pub fn next(&mut self) -> Result<()> {
|
||||
self.retroactive_role_blocks()?;
|
||||
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.pull_next_prompt_with_dead_ignore()? {
|
||||
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(())
|
||||
}
|
||||
|
||||
fn retroactive_role_blocks(&mut self) -> Result<()> {
|
||||
let blocks = match &self.night_state {
|
||||
NightState::Active {
|
||||
current_changes, ..
|
||||
} => current_changes
|
||||
.iter()
|
||||
.filter_map(|c| match c {
|
||||
NightChange::RoleBlock {
|
||||
target, block_type, ..
|
||||
} => Some((*target, *block_type)),
|
||||
_ => None,
|
||||
})
|
||||
.collect::<Box<[_]>>(),
|
||||
NightState::Complete => return Err(GameError::NightOver),
|
||||
};
|
||||
for (target, block_type) in blocks {
|
||||
match block_type {
|
||||
RoleBlock::Direwolf => self.apply_direwolf_block_retroactively(target),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn apply_direwolf_block_retroactively(&mut self, target: CharacterId) {
|
||||
self.used_actions
|
||||
.iter_mut()
|
||||
.filter_map(|(prompt, res, changes)| match prompt.marked() {
|
||||
Some((marked, None)) => (marked == target).then_some((res, changes)),
|
||||
Some((marked1, Some(marked2))) => {
|
||||
(marked1 == target || marked2 == target).then_some((res, changes))
|
||||
}
|
||||
None => None,
|
||||
})
|
||||
.for_each(|(result, changes)| {
|
||||
changes.clear();
|
||||
*result = ActionResult::RoleBlocked;
|
||||
});
|
||||
}
|
||||
|
||||
fn pull_next_prompt_with_dead_ignore(&mut self) -> Result<Option<ActionPrompt>> {
|
||||
let has_living_beholder = self
|
||||
.village
|
||||
.characters()
|
||||
.into_iter()
|
||||
.any(|c| matches!(c.role_title(), RoleTitle::Beholder));
|
||||
while let Some(prompt) = self.action_queue.pop_front() {
|
||||
let Some(char_id) = prompt.character_id() else {
|
||||
return Ok(Some(prompt));
|
||||
};
|
||||
match (self.died_to_tonight(char_id)?, has_living_beholder) {
|
||||
(Some(_), false) => {}
|
||||
(Some(DiedTo::Shapeshift { .. }), _) | (Some(_), true) | (None, _) => {
|
||||
return Ok(Some(prompt));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
|
@ -16,6 +16,7 @@ use core::num::NonZeroU8;
|
|||
|
||||
use crate::{
|
||||
aura::Aura,
|
||||
bag::DrunkRoll,
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::night::{
|
||||
|
|
@ -79,7 +80,7 @@ impl Night {
|
|||
.ok_or(GameError::InvalidTarget)?,
|
||||
}),
|
||||
})),
|
||||
_ => Err(GameError::InvalidMessageForGameState),
|
||||
_ => Err(GameError::ShapeshiftingIsForShapeshifters),
|
||||
};
|
||||
}
|
||||
ActionResponse::Continue => {
|
||||
|
|
@ -106,13 +107,19 @@ impl Night {
|
|||
}));
|
||||
}
|
||||
}
|
||||
ActionResponse::ContinueToResult => return self.process(ActionResponse::Continue),
|
||||
};
|
||||
|
||||
match current_prompt {
|
||||
ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: None,
|
||||
}
|
||||
.into()),
|
||||
ActionPrompt::Bloodletter {
|
||||
character_id,
|
||||
living_players,
|
||||
marked: Some(marked),
|
||||
..
|
||||
} => Ok(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::ApplyAura {
|
||||
|
|
@ -222,7 +229,7 @@ impl Night {
|
|||
died_to: DiedTo::Militia {
|
||||
killer: character_id.character_id,
|
||||
night: NonZeroU8::new(self.night)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
|
|
@ -236,12 +243,11 @@ impl Night {
|
|||
..
|
||||
} => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::GoBackToSleep,
|
||||
change: Some(NightChange::Kill {
|
||||
change: NonZeroU8::new(self.night).map(|night| NightChange::Kill {
|
||||
target: *marked,
|
||||
died_to: DiedTo::MapleWolf {
|
||||
night,
|
||||
source: character_id.character_id,
|
||||
night: NonZeroU8::new(self.night)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
starves_if_fails: *kill_or_die,
|
||||
},
|
||||
}),
|
||||
|
|
@ -313,7 +319,7 @@ impl Night {
|
|||
.ok_or(GameError::NoWolves)?
|
||||
.character_id(),
|
||||
night: NonZeroU8::new(self.night)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
|
|
@ -336,7 +342,7 @@ impl Night {
|
|||
})
|
||||
.ok_or(GameError::InvalidTarget)?,
|
||||
}),
|
||||
_ => return Err(GameError::InvalidMessageForGameState),
|
||||
_ => return Err(GameError::ShapeshiftingIsForShapeshifters),
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
|
@ -351,7 +357,7 @@ impl Night {
|
|||
died_to: DiedTo::AlphaWolf {
|
||||
killer: character_id.character_id,
|
||||
night: NonZeroU8::new(self.night)
|
||||
.ok_or(GameError::InvalidMessageForGameState)?,
|
||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||
},
|
||||
}),
|
||||
})),
|
||||
|
|
@ -410,10 +416,11 @@ impl Night {
|
|||
} => {
|
||||
if let Some(result) = self.used_actions.iter().find_map(|(prompt, result, _)| {
|
||||
prompt.matches_beholding(*marked).then_some(result)
|
||||
}) && self.dies_tonight(*marked)?
|
||||
}) && self.died_to_tonight(*marked)?.is_some()
|
||||
{
|
||||
Ok(ActionComplete {
|
||||
result: if matches!(result, ActionResult::RoleBlocked) {
|
||||
result: if matches!(result, ActionResult::RoleBlocked | ActionResult::Drunk)
|
||||
{
|
||||
ActionResult::BeholderSawNothing
|
||||
} else {
|
||||
result.clone()
|
||||
|
|
@ -531,7 +538,57 @@ impl Night {
|
|||
| ActionPrompt::Guardian { marked: None, .. }
|
||||
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||
| ActionPrompt::DireWolf { marked: None, .. }
|
||||
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::InvalidMessageForGameState),
|
||||
| ActionPrompt::Seer { marked: None, .. } => Err(GameError::MustSelectTarget),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn received_response_with_auras(
|
||||
&self,
|
||||
resp: ActionResponse,
|
||||
) -> Result<ResponseOutcome> {
|
||||
let outcome = self.process(resp)?;
|
||||
if matches!(
|
||||
self.current_prompt(),
|
||||
Some((ActionPrompt::TraitorIntro { .. }, _))
|
||||
| Some((ActionPrompt::RoleChange { .. }, _))
|
||||
| Some((ActionPrompt::ElderReveal { .. }, _))
|
||||
) {
|
||||
return Ok(outcome);
|
||||
}
|
||||
let mut act = match outcome {
|
||||
ResponseOutcome::PromptUpdate(prompt) => {
|
||||
return Ok(ResponseOutcome::PromptUpdate(prompt));
|
||||
}
|
||||
ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::Drunk,
|
||||
..
|
||||
})
|
||||
| ResponseOutcome::ActionComplete(ActionComplete {
|
||||
result: ActionResult::RoleBlocked,
|
||||
..
|
||||
}) => return Ok(outcome),
|
||||
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 crate::{
|
||||
aura::Aura,
|
||||
aura::AuraTitle,
|
||||
character::Character,
|
||||
error::GameError,
|
||||
message::Identification,
|
||||
|
|
@ -157,6 +157,46 @@ pub enum SetupRole {
|
|||
}
|
||||
|
||||
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
|
||||
| Self::Insomniac
|
||||
| Self::Mortician
|
||||
| Self::Gravedigger
|
||||
)
|
||||
}
|
||||
AuraTitle::Bloodlet => false,
|
||||
}
|
||||
}
|
||||
pub fn into_role(self) -> Role {
|
||||
match self {
|
||||
SetupRoleTitle::Bloodletter => Role::Bloodletter,
|
||||
|
|
@ -379,7 +419,7 @@ impl SlotId {
|
|||
pub struct SetupSlot {
|
||||
pub slot_id: SlotId,
|
||||
pub role: SetupRole,
|
||||
pub auras: Vec<Aura>,
|
||||
pub auras: Vec<AuraTitle>,
|
||||
pub assign_to: Option<PlayerId>,
|
||||
pub created_order: u32,
|
||||
}
|
||||
|
|
@ -403,7 +443,10 @@ impl SetupSlot {
|
|||
Character::new(
|
||||
ident.clone(),
|
||||
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()))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -84,12 +84,18 @@ pub enum StoryActionResult {
|
|||
Insomniac { visits: Box<[CharacterId]> },
|
||||
Empath { scapegoat: bool },
|
||||
BeholderSawNothing,
|
||||
BeholderSawEverything,
|
||||
Drunk,
|
||||
ShiftFailed,
|
||||
}
|
||||
|
||||
impl StoryActionResult {
|
||||
pub fn new(result: ActionResult) -> Option<Self> {
|
||||
Some(match result {
|
||||
ActionResult::ShiftFailed => Self::ShiftFailed,
|
||||
ActionResult::BeholderSawNothing => Self::BeholderSawNothing,
|
||||
ActionResult::BeholderSawEverything => Self::BeholderSawEverything,
|
||||
ActionResult::Drunk => Self::Drunk,
|
||||
ActionResult::RoleBlocked => Self::RoleBlocked,
|
||||
ActionResult::Seer(alignment) => Self::Seer(alignment),
|
||||
ActionResult::PowerSeer { powerful } => Self::PowerSeer { powerful },
|
||||
|
|
@ -390,7 +396,8 @@ impl StoryActionPrompt {
|
|||
character_id: character_id.character_id,
|
||||
},
|
||||
|
||||
ActionPrompt::Bloodletter { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Bloodletter { .. }
|
||||
| ActionPrompt::Protector { .. }
|
||||
| ActionPrompt::Gravedigger { .. }
|
||||
| ActionPrompt::Hunter { .. }
|
||||
|
|
@ -448,7 +455,7 @@ impl GameStory {
|
|||
village = match actions {
|
||||
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
||||
GameActions::NightDetails(night_details) => {
|
||||
village.with_night_changes(&night_details.changes)?
|
||||
village.with_night_changes(&night_details.changes)?.0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
@ -461,7 +468,7 @@ impl GameStory {
|
|||
village = match actions {
|
||||
GameActions::DayDetails(day_details) => village.with_day_changes(day_details)?,
|
||||
GameActions::NightDetails(night_details) => {
|
||||
village.with_night_changes(&night_details.changes)?
|
||||
village.with_night_changes(&night_details.changes)?.0
|
||||
}
|
||||
};
|
||||
if time == at_time {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ use serde::{Deserialize, Serialize};
|
|||
|
||||
use super::Result;
|
||||
use crate::{
|
||||
aura::Aura,
|
||||
character::{Character, CharacterId},
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
|
|
|
|||
|
|
@ -12,14 +12,15 @@
|
|||
//
|
||||
// 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 core::{num::NonZeroU8, ops::Not};
|
||||
|
||||
use crate::{
|
||||
aura::Aura,
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
game::{
|
||||
GameTime, Village, kill,
|
||||
GameTime, Village,
|
||||
kill::{self, KillOutcome},
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
story::DayDetail,
|
||||
},
|
||||
|
|
@ -43,19 +44,41 @@ impl Village {
|
|||
|
||||
Ok(new_village)
|
||||
}
|
||||
pub fn with_night_changes(&self, all_changes: &[NightChange]) -> Result<Self> {
|
||||
pub fn with_night_changes(
|
||||
&self,
|
||||
all_changes: &[NightChange],
|
||||
) -> Result<(Self, Box<[NightChange]>)> {
|
||||
let night = match self.time {
|
||||
GameTime::Day { .. } => return Err(GameError::NotNight),
|
||||
GameTime::Night { number } => number,
|
||||
};
|
||||
let mut changes = ChangesLookup::new(all_changes);
|
||||
let changes = ChangesLookup::new(all_changes);
|
||||
|
||||
let mut new_village = self.clone();
|
||||
|
||||
// recorded changes: changes sans failed kills, actions failed due to blocks, etc
|
||||
let mut recorded_changes = all_changes.to_vec();
|
||||
|
||||
// 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 {
|
||||
match change {
|
||||
NightChange::ApplyAura { target, aura, .. } => {
|
||||
let target = new_village.character_by_id_mut(*target)?;
|
||||
target.apply_aura(*aura);
|
||||
target.apply_aura(aura.clone());
|
||||
}
|
||||
NightChange::ElderReveal { elder } => {
|
||||
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
||||
|
|
@ -66,29 +89,52 @@ impl Village {
|
|||
NightChange::HunterTarget { source, target } => {
|
||||
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
||||
hunter_character.hunter_mut()?.replace(*target);
|
||||
if changes.killed(*source).is_some()
|
||||
&& changes.protected(source).is_none()
|
||||
&& changes.protected(target).is_none()
|
||||
{
|
||||
new_village
|
||||
.character_by_id_mut(*target)
|
||||
.unwrap()
|
||||
.kill(DiedTo::Hunter {
|
||||
if changes
|
||||
.died_to(hunter_character.character_id(), night, self)?
|
||||
.is_some()
|
||||
&& let Some(kill) = kill::resolve_kill(
|
||||
&changes,
|
||||
*target,
|
||||
&DiedTo::Hunter {
|
||||
killer: *source,
|
||||
night: NonZeroU8::new(night).unwrap(),
|
||||
})
|
||||
night: NonZeroU8::new(night)
|
||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||
},
|
||||
night,
|
||||
&new_village,
|
||||
)?
|
||||
{
|
||||
kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?;
|
||||
}
|
||||
}
|
||||
NightChange::Kill { target, died_to } => {
|
||||
if let Some(kill) =
|
||||
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
||||
if let Some(kill) = kill::resolve_kill(&changes, *target, died_to, night, self)?
|
||||
{
|
||||
kill.apply_to_village(&mut new_village)?;
|
||||
if let KillOutcome::Guarding {
|
||||
guardian,
|
||||
original_kill,
|
||||
..
|
||||
} = &kill
|
||||
{
|
||||
recorded_changes.retain(|c| c != change);
|
||||
recorded_changes.push(NightChange::Kill {
|
||||
target: *guardian,
|
||||
died_to: original_kill.clone(),
|
||||
});
|
||||
}
|
||||
kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?;
|
||||
if let DiedTo::MapleWolf { source, .. } = died_to
|
||||
&& let Ok(maple) = new_village.character_by_id_mut(*source)
|
||||
{
|
||||
*maple.maple_wolf_mut()? = night;
|
||||
}
|
||||
} else {
|
||||
recorded_changes.retain(|c| c != change);
|
||||
}
|
||||
}
|
||||
NightChange::Shapeshift { source, into } => {
|
||||
if let Some(target) = changes.wolf_pack_kill_target()
|
||||
&& changes.protected(target).is_none()
|
||||
&& changes.protected(*target).is_none()
|
||||
{
|
||||
if *target != *into {
|
||||
log::error!("shapeshift into({into}) != target({target})");
|
||||
|
|
@ -101,6 +147,8 @@ impl Village {
|
|||
night: NonZeroU8::new(night).unwrap(),
|
||||
});
|
||||
// role change pushed in [apply_shapeshift]
|
||||
} else {
|
||||
recorded_changes.retain(|c| c != change);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -128,6 +176,11 @@ impl Village {
|
|||
.character_by_id_mut(*source)?
|
||||
.direwolf_mut()?
|
||||
.replace(*target);
|
||||
|
||||
recorded_changes.retain(|c| {
|
||||
matches!(c, NightChange::RoleBlock { .. })
|
||||
|| c.target().map(|t| t == *target).unwrap_or_default().not()
|
||||
});
|
||||
}
|
||||
|
||||
NightChange::MasonRecruit {
|
||||
|
|
@ -141,6 +194,7 @@ impl Village {
|
|||
tried_recruiting: *recruiting,
|
||||
},
|
||||
);
|
||||
recorded_changes.retain(|c| c != change);
|
||||
} else {
|
||||
new_village
|
||||
.character_by_id_mut(*mason_leader)?
|
||||
|
|
@ -157,9 +211,38 @@ impl Village {
|
|||
NightChange::LostAura { character, aura } => {
|
||||
new_village
|
||||
.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
|
||||
|
|
@ -203,6 +286,6 @@ impl Village {
|
|||
if new_village.is_game_over().is_none() {
|
||||
new_village.to_day()?;
|
||||
}
|
||||
Ok(new_village)
|
||||
Ok((new_village, recorded_changes.into_boxed_slice()))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,311 @@
|
|||
// 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;
|
||||
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{
|
||||
Game, GameSettings, SetupRole,
|
||||
night::changes::{ChangesLookup, NightChange},
|
||||
},
|
||||
game_test::{GameExt, SettingsExt, gen_players, init_log},
|
||||
player::Protection,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn shapeshift() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let shapeshifter = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Shapeshifter, shapeshifter);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Shapeshift {
|
||||
source: game.character_by_player_id(shapeshifter).character_id(),
|
||||
into: game.character_by_player_id(target).character_id(),
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(shapeshifter).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::Shapeshift {
|
||||
into: game.character_by_player_id(target).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_protect() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_guard() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(wolf).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::GuardianProtecting {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
protecting: game.character_by_player_id(target).character_id(),
|
||||
protecting_from: game.character_by_player_id(wolf).character_id(),
|
||||
protecting_from_cause: Box::new(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
}),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_protect_from_multiple_attackers() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let militia = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Villager, target);
|
||||
settings.add_and_assign(SetupRole::Villager, militia);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Militia {
|
||||
killer: game.character_by_player_id(militia).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn guardian_protect_someone_else_unprotected_dies() {
|
||||
init_log();
|
||||
let players = gen_players(1..10);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let target = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Seer, target);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||
settings.fill_remaining_slots_with_villagers(9);
|
||||
let game = Game::new(&players, settings).unwrap();
|
||||
|
||||
let all_changes = [
|
||||
NightChange::Kill {
|
||||
target: game.character_by_player_id(target).character_id(),
|
||||
died_to: DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
},
|
||||
NightChange::Protection {
|
||||
target: game.living_villager().character_id(),
|
||||
protection: Protection::Guardian {
|
||||
source: game.character_by_player_id(guardian).character_id(),
|
||||
guarding: false,
|
||||
},
|
||||
},
|
||||
];
|
||||
let changes = ChangesLookup::new(&all_changes);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(target).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(Some(DiedTo::Wolfpack {
|
||||
killing_wolf: game.character_by_player_id(wolf).character_id(),
|
||||
night: NonZeroU8::new(1).unwrap()
|
||||
}))
|
||||
);
|
||||
assert_eq!(
|
||||
changes.died_to(
|
||||
game.character_by_player_id(guardian).character_id(),
|
||||
1,
|
||||
game.village()
|
||||
),
|
||||
Ok(None)
|
||||
);
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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/>.
|
||||
mod changes;
|
||||
mod night_order;
|
||||
mod previous;
|
||||
mod revert;
|
||||
|
|
@ -186,9 +187,17 @@ pub trait ActionResultExt {
|
|||
fn adjudicator(&self) -> Killer;
|
||||
fn mortician(&self) -> DiedToTitle;
|
||||
fn empath(&self) -> bool;
|
||||
fn drunk(&self);
|
||||
fn shapeshift_failed(&self);
|
||||
}
|
||||
|
||||
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 {
|
||||
match self {
|
||||
Self::Empath { scapegoat } => *scapegoat,
|
||||
|
|
@ -251,7 +260,7 @@ impl ActionResultExt for ActionResult {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait AlignmentExt {
|
||||
fn village(&self);
|
||||
fn wolves(&self);
|
||||
|
|
@ -267,6 +276,7 @@ impl AlignmentExt for Alignment {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait ServerToHostMessageExt {
|
||||
fn prompt(self) -> ActionPrompt;
|
||||
fn result(self) -> ActionResult;
|
||||
|
|
@ -322,6 +332,7 @@ pub trait GameExt {
|
|||
fn get_state(&mut self) -> ServerToHostMessage;
|
||||
fn next_expect_game_over(&mut self) -> GameOver;
|
||||
fn prev(&mut self) -> ServerToHostMessage;
|
||||
fn mark_villager(&mut self) -> ActionPrompt;
|
||||
}
|
||||
|
||||
impl GameExt for Game {
|
||||
|
|
@ -394,10 +405,15 @@ impl GameExt for Game {
|
|||
.prompt()
|
||||
}
|
||||
|
||||
fn mark_villager(&mut self) -> ActionPrompt {
|
||||
self.mark(self.living_villager().character_id())
|
||||
}
|
||||
|
||||
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||
let prompt = self.mark(mark);
|
||||
match prompt {
|
||||
ActionPrompt::Insomniac { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::CoverOfDarkness
|
||||
|
|
@ -839,7 +855,7 @@ fn big_game_test_based_on_story_test() {
|
|||
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||
settings.add_and_assign(hunter.0, hunter.1);
|
||||
settings.fill_remaining_slots_with_villagers(players.len());
|
||||
|
||||
#[allow(unused)]
|
||||
let (
|
||||
werewolf,
|
||||
dire_wolf,
|
||||
|
|
@ -923,7 +939,9 @@ fn big_game_test_based_on_story_test() {
|
|||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().shapeshifter();
|
||||
game.response(ActionResponse::Shapeshift).sleep();
|
||||
game.response(ActionResponse::Shapeshift)
|
||||
.shapeshift_failed();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().seer();
|
||||
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||
|
|
|
|||
|
|
@ -227,7 +227,7 @@ fn previous_prompt() {
|
|||
settings.add_and_assign(scapegoat.0, scapegoat.1);
|
||||
settings.add_and_assign(hunter.0, hunter.1);
|
||||
settings.fill_remaining_slots_with_villagers(players.len());
|
||||
|
||||
#[allow(unused)]
|
||||
let (
|
||||
werewolf,
|
||||
dire_wolf,
|
||||
|
|
@ -353,7 +353,9 @@ fn previous_prompt() {
|
|||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().shapeshifter();
|
||||
game.response(ActionResponse::Shapeshift).sleep();
|
||||
game.response(ActionResponse::Shapeshift)
|
||||
.shapeshift_failed();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().seer();
|
||||
game.mark(game.character_by_player_id(werewolf).character_id());
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ use crate::{
|
|||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||
},
|
||||
message::{CharacterIdentity, night::ActionPrompt},
|
||||
message::night::ActionPrompt,
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -12,22 +12,14 @@
|
|||
//
|
||||
// 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;
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
||||
SettingsExt, gen_players,
|
||||
},
|
||||
message::{
|
||||
host::{HostDayMessage, HostGameMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
||||
},
|
||||
role::{Role, RoleTitle},
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::{ActionPrompt, ActionPromptTitle},
|
||||
role::RoleTitle,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -18,16 +18,9 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
|||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
||||
SettingsExt, gen_players,
|
||||
},
|
||||
message::{
|
||||
host::{HostDayMessage, HostGameMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
||||
},
|
||||
role::Role,
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::ActionPromptTitle,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,123 @@
|
|||
// 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 crate::{
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||
},
|
||||
};
|
||||
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
#[test]
|
||||
fn block_on_wolf_kill_target_prevents_kill() {
|
||||
init_log();
|
||||
let players = gen_players(1..21);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let scapegoat = player_ids.next().unwrap();
|
||||
let direwolf = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(
|
||||
SetupRole::Scapegoat {
|
||||
redeemed: OrRandom::Determined(false),
|
||||
},
|
||||
scapegoat,
|
||||
);
|
||||
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||
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();
|
||||
game.next().title().wolves_intro();
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().direwolf();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
game.execute().title().wolf_pack_kill();
|
||||
let scapegoat_char_id = game.character_by_player_id(scapegoat).character_id();
|
||||
game.mark(scapegoat_char_id);
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().direwolf();
|
||||
game.mark(scapegoat_char_id);
|
||||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
game.character_by_player_id(scapegoat).died_to().cloned(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_on_guardian_target_prevents_the_visit() {
|
||||
init_log();
|
||||
let players = gen_players(1..21);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let scapegoat = player_ids.next().unwrap();
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let direwolf = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(
|
||||
SetupRole::Scapegoat {
|
||||
redeemed: OrRandom::Determined(false),
|
||||
},
|
||||
scapegoat,
|
||||
);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
settings.add_and_assign(SetupRole::DireWolf, direwolf);
|
||||
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();
|
||||
game.next().title().wolves_intro();
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().direwolf();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
game.execute().title().guardian();
|
||||
let scapegoat_char_id = game.character_by_player_id(scapegoat).character_id();
|
||||
game.mark(scapegoat_char_id);
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.mark_villager();
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().direwolf();
|
||||
game.mark(scapegoat_char_id);
|
||||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
game.character_by_player_id(guardian)
|
||||
.guardian()
|
||||
.unwrap()
|
||||
.clone(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
|
@ -18,16 +18,15 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
|||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
||||
SettingsExt, gen_players,
|
||||
ActionPromptTitleExt, ActionResultExt, GameExt, ServerToHostMessageExt, SettingsExt,
|
||||
gen_players,
|
||||
},
|
||||
message::{
|
||||
host::{HostDayMessage, HostGameMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
||||
night::{ActionPrompt, ActionPromptTitle},
|
||||
},
|
||||
role::Role,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ use crate::{
|
|||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::{ActionPrompt, ActionPromptTitle},
|
||||
message::night::ActionPromptTitle,
|
||||
role::{Alignment, Role},
|
||||
};
|
||||
#[allow(unused)]
|
||||
|
|
|
|||
|
|
@ -12,16 +12,13 @@
|
|||
//
|
||||
// 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;
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
||||
},
|
||||
message::night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::{ActionPromptTitle, ActionResult},
|
||||
role::Role,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,9 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use core::num::NonZeroU8;
|
||||
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
error::GameError,
|
||||
|
|
@ -25,7 +28,7 @@ use crate::{
|
|||
host::{HostGameMessage, HostNightMessage},
|
||||
night::{ActionPromptTitle, ActionResponse},
|
||||
},
|
||||
role::{PreviousGuardianAction, Role},
|
||||
role::{PreviousGuardianAction, Role, RoleTitle},
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
@ -215,3 +218,54 @@ fn cannot_visit_previous_nights_guard_target() {
|
|||
Err(GameError::InvalidTarget)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protects_from_militia() {
|
||||
init_log();
|
||||
let players = gen_players(1..21);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let guardian = player_ids.next().unwrap();
|
||||
let militia = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(SetupRole::Militia, militia);
|
||||
settings.add_and_assign(SetupRole::Guardian, guardian);
|
||||
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();
|
||||
game.next().title().wolves_intro();
|
||||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
game.execute().title().guardian();
|
||||
let mut villagers = game
|
||||
.village()
|
||||
.characters()
|
||||
.into_iter()
|
||||
.filter(|c| c.alive() && matches!(c.role().title(), RoleTitle::Villager));
|
||||
let protected = villagers.next().unwrap();
|
||||
game.mark(protected.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().wolf_pack_kill();
|
||||
game.mark(villagers.next().unwrap().character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().militia();
|
||||
game.mark(protected.character_id());
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
game.character_by_player_id(protected.player_id())
|
||||
.died_to()
|
||||
.cloned(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
game.character_by_player_id(militia).role().clone(),
|
||||
Role::Militia { targeted: None }
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,20 +12,13 @@
|
|||
//
|
||||
// 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::{Game, GameSettings, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||
},
|
||||
message::{
|
||||
host::{HostGameMessage, HostNightMessage},
|
||||
night::{ActionPromptTitle, ActionResponse},
|
||||
},
|
||||
role::{PreviousGuardianAction, Role},
|
||||
message::night::ActionPromptTitle,
|
||||
role::Role,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -16,11 +16,8 @@
|
|||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, SettingsExt, gen_players,
|
||||
},
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::{ActionPromptTitle, Visits},
|
||||
role::{Role, RoleTitle},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,333 @@
|
|||
// 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;
|
||||
|
||||
#[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,
|
||||
};
|
||||
|
||||
#[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()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
|
@ -18,7 +18,7 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
|||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::{ActionPrompt, ActionPromptTitle},
|
||||
};
|
||||
|
|
@ -243,3 +243,63 @@ fn masons_wake_even_if_leader_died() {
|
|||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn masons_get_go_back_to_sleep() {
|
||||
let players = gen_players(1..21);
|
||||
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||
let mason = player_ids.next().unwrap();
|
||||
let scapegoat = player_ids.next().unwrap();
|
||||
let beholder = player_ids.next().unwrap();
|
||||
let wolf = player_ids.next().unwrap();
|
||||
let mut settings = GameSettings::empty();
|
||||
settings.add_and_assign(
|
||||
SetupRole::MasonLeader {
|
||||
recruits_available: NonZeroU8::new(1).unwrap(),
|
||||
},
|
||||
mason,
|
||||
);
|
||||
settings.add_and_assign(
|
||||
SetupRole::Scapegoat {
|
||||
redeemed: OrRandom::Determined(false),
|
||||
},
|
||||
scapegoat,
|
||||
);
|
||||
settings.add_and_assign(SetupRole::Beholder, beholder);
|
||||
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();
|
||||
game.next().title().wolves_intro();
|
||||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(game.execute().title(), ActionPromptTitle::WolfPackKill);
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_leader_recruit();
|
||||
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||
game.r#continue().r#continue();
|
||||
|
||||
game.next().title().masons_wake();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().beholder();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
game.execute().title().wolf_pack_kill();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().masons_wake();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next().title().beholder();
|
||||
game.mark_villager();
|
||||
game.r#continue().sleep();
|
||||
|
||||
game.next_expect_day();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ mod apprentice;
|
|||
mod beholder;
|
||||
mod black_knight;
|
||||
mod bloodletter;
|
||||
mod direwolf;
|
||||
mod diseased;
|
||||
mod elder;
|
||||
mod empath;
|
||||
|
|
@ -23,9 +24,11 @@ mod guardian;
|
|||
mod hunter;
|
||||
mod insomniac;
|
||||
mod lone_wolf;
|
||||
mod maple_wolf;
|
||||
mod mason;
|
||||
mod militia;
|
||||
mod mortician;
|
||||
mod protector;
|
||||
mod pyremaster;
|
||||
mod scapegoat;
|
||||
mod shapeshifter;
|
||||
|
|
|
|||
|
|
@ -12,15 +12,13 @@
|
|||
//
|
||||
// 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;
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameSettings, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::{ActionPrompt, ActionPromptTitle},
|
||||
game_test::{ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::ActionPromptTitle,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
|
|
@ -100,10 +100,6 @@ fn redeemed_scapegoat_role_changes() {
|
|||
game.mark_and_check(seer);
|
||||
game.r#continue().sleep();
|
||||
|
||||
assert_eq!(game.next().title(), ActionPromptTitle::Seer);
|
||||
game.mark_and_check(wolf_char_id);
|
||||
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||
game.r#continue().sleep();
|
||||
game.next_expect_day();
|
||||
|
||||
assert_eq!(
|
||||
|
|
|
|||
|
|
@ -130,7 +130,9 @@ fn protect_stops_shapeshift() {
|
|||
|
||||
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();
|
||||
|
||||
|
|
@ -218,3 +220,60 @@ fn i_would_simply_refuse() {
|
|||
|
||||
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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,22 +12,13 @@
|
|||
//
|
||||
// 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;
|
||||
#[allow(unused)]
|
||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||
|
||||
use crate::{
|
||||
diedto::DiedTo,
|
||||
game::{Game, GameOver, GameSettings, OrRandom, SetupRole},
|
||||
game_test::{
|
||||
ActionPromptTitleExt, ActionResultExt, AlignmentExt, GameExt, ServerToHostMessageExt,
|
||||
SettingsExt, gen_players,
|
||||
},
|
||||
message::{
|
||||
host::{HostDayMessage, HostGameMessage, ServerToHostMessage},
|
||||
night::{ActionPrompt, ActionPromptTitle, ActionResult},
|
||||
},
|
||||
role::Role,
|
||||
game::{Game, GameOver, GameSettings, SetupRole},
|
||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||
message::night::ActionPromptTitle,
|
||||
};
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
// 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(clippy::new_without_default)]
|
||||
|
||||
pub mod aura;
|
||||
pub mod bag;
|
||||
pub mod character;
|
||||
pub mod diedto;
|
||||
pub mod error;
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use core::{num::NonZeroU8, ops::Deref};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use werewolves_macros::{ChecksAs, Titles};
|
||||
use werewolves_macros::{ChecksAs, Extract, Titles};
|
||||
|
||||
use crate::{
|
||||
character::CharacterId,
|
||||
|
|
@ -32,6 +32,7 @@ pub enum ActionType {
|
|||
Cover,
|
||||
#[checks("is_wolfy")]
|
||||
WolvesIntro,
|
||||
TraitorIntro,
|
||||
RoleChange,
|
||||
Protect,
|
||||
#[checks("is_wolfy")]
|
||||
|
|
@ -55,7 +56,7 @@ pub enum ActionType {
|
|||
Beholder,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, ChecksAs, Titles, Extract)]
|
||||
pub enum ActionPrompt {
|
||||
#[checks(ActionType::Cover)]
|
||||
CoverOfDarkness,
|
||||
|
|
@ -209,12 +210,67 @@ pub enum ActionPrompt {
|
|||
living_players: Box<[CharacterIdentity]>,
|
||||
marked: Option<CharacterId>,
|
||||
},
|
||||
#[checks(ActionType::TraitorIntro)]
|
||||
TraitorIntro { character_id: CharacterIdentity },
|
||||
}
|
||||
|
||||
impl ActionPrompt {
|
||||
pub(crate) const fn marked(&self) -> Option<(CharacterId, Option<CharacterId>)> {
|
||||
match self {
|
||||
ActionPrompt::Seer { marked, .. }
|
||||
| ActionPrompt::Protector { marked, .. }
|
||||
| ActionPrompt::Gravedigger { marked, .. }
|
||||
| ActionPrompt::Hunter { marked, .. }
|
||||
| ActionPrompt::Militia { marked, .. }
|
||||
| ActionPrompt::MapleWolf { marked, .. }
|
||||
| ActionPrompt::Guardian { marked, .. }
|
||||
| ActionPrompt::Adjudicator { marked, .. }
|
||||
| ActionPrompt::PowerSeer { marked, .. }
|
||||
| ActionPrompt::Mortician { marked, .. }
|
||||
| ActionPrompt::Beholder { marked, .. }
|
||||
| ActionPrompt::MasonLeaderRecruit { marked, .. }
|
||||
| ActionPrompt::Empath { marked, .. }
|
||||
| ActionPrompt::Vindicator { marked, .. }
|
||||
| ActionPrompt::PyreMaster { marked, .. }
|
||||
| ActionPrompt::WolfPackKill { marked, .. }
|
||||
| ActionPrompt::AlphaWolf { marked, .. }
|
||||
| ActionPrompt::DireWolf { marked, .. }
|
||||
| ActionPrompt::LoneWolfKill { marked, .. }
|
||||
| ActionPrompt::Bloodletter { marked, .. } => match *marked {
|
||||
Some(marked) => Some((marked, None)),
|
||||
None => None,
|
||||
},
|
||||
ActionPrompt::Arcanist {
|
||||
marked: (None, Some(marked)),
|
||||
..
|
||||
}
|
||||
| ActionPrompt::Arcanist {
|
||||
marked: (Some(marked), None),
|
||||
..
|
||||
} => Some((*marked, None)),
|
||||
ActionPrompt::Arcanist {
|
||||
marked: (Some(marked1), Some(marked2)),
|
||||
..
|
||||
} => Some((*marked1, Some(*marked2))),
|
||||
|
||||
ActionPrompt::Arcanist {
|
||||
marked: (None, None),
|
||||
..
|
||||
}
|
||||
| ActionPrompt::CoverOfDarkness
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::TraitorIntro { .. } => None,
|
||||
}
|
||||
}
|
||||
pub(crate) const fn character_id(&self) -> Option<CharacterId> {
|
||||
match self {
|
||||
ActionPrompt::Insomniac { character_id, .. }
|
||||
ActionPrompt::TraitorIntro { character_id }
|
||||
| ActionPrompt::Insomniac { character_id, .. }
|
||||
| ActionPrompt::LoneWolfKill { character_id, .. }
|
||||
| ActionPrompt::ElderReveal { character_id }
|
||||
| ActionPrompt::RoleChange { character_id, .. }
|
||||
|
|
@ -257,7 +313,8 @@ impl ActionPrompt {
|
|||
| ActionPrompt::Mortician { character_id, .. }
|
||||
| ActionPrompt::Vindicator { character_id, .. } => character_id.character_id == target,
|
||||
|
||||
ActionPrompt::Beholder { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Beholder { .. }
|
||||
| ActionPrompt::CoverOfDarkness
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
|
|
@ -278,16 +335,28 @@ impl ActionPrompt {
|
|||
| ActionPrompt::LoneWolfKill { .. } => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn interactive(&self) -> bool {
|
||||
match self {
|
||||
Self::Shapeshifter { .. } => true,
|
||||
_ => !matches!(
|
||||
self.with_mark(CharacterId::new()),
|
||||
Err(GameError::RoleDoesntMark)
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_mark(&self, mark: CharacterId) -> Result<ActionPrompt> {
|
||||
let mut prompt = self.clone();
|
||||
match &mut prompt {
|
||||
ActionPrompt::Insomniac { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::WolvesIntro { .. }
|
||||
| ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::Shapeshifter { .. }
|
||||
| ActionPrompt::CoverOfDarkness => Err(GameError::InvalidMessageForGameState),
|
||||
| ActionPrompt::CoverOfDarkness => Err(GameError::RoleDoesntMark),
|
||||
|
||||
ActionPrompt::Guardian {
|
||||
previous,
|
||||
|
|
@ -484,11 +553,13 @@ pub enum ActionResponse {
|
|||
MarkTarget(CharacterId),
|
||||
Shapeshift,
|
||||
Continue,
|
||||
ContinueToResult,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum ActionResult {
|
||||
RoleBlocked,
|
||||
Drunk,
|
||||
Seer(Alignment),
|
||||
PowerSeer { powerful: Powerful },
|
||||
Adjudicator { killer: Killer },
|
||||
|
|
@ -498,10 +569,42 @@ pub enum ActionResult {
|
|||
Insomniac(Visits),
|
||||
Empath { scapegoat: bool },
|
||||
BeholderSawNothing,
|
||||
BeholderSawEverything,
|
||||
GoBackToSleep,
|
||||
ShiftFailed,
|
||||
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)]
|
||||
pub struct Visits(Box<[CharacterIdentity]>);
|
||||
|
||||
|
|
|
|||
|
|
@ -131,31 +131,37 @@ pub enum Role {
|
|||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Seer,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Arcanist,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Adjudicator,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
PowerSeer,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Mortician,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Beholder,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
|
|
@ -197,6 +203,7 @@ pub enum Role {
|
|||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("is_mentor")]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Gravedigger,
|
||||
#[checks(Alignment::Village)]
|
||||
#[checks(Killer::Killer)]
|
||||
|
|
@ -241,6 +248,7 @@ pub enum Role {
|
|||
#[checks(Alignment::Village)]
|
||||
#[checks(Powerful::Powerful)]
|
||||
#[checks(Killer::NotKiller)]
|
||||
#[checks("doesnt_wake_if_died_tonight")]
|
||||
Insomniac,
|
||||
|
||||
#[checks(Alignment::Wolves)]
|
||||
|
|
@ -318,7 +326,7 @@ impl Role {
|
|||
|
||||
Role::Werewolf => KillingWolfOrder::Werewolf,
|
||||
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
||||
Role::Bloodletter { .. } => KillingWolfOrder::Bloodletter,
|
||||
Role::Bloodletter => KillingWolfOrder::Bloodletter,
|
||||
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
||||
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
||||
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
||||
|
|
@ -469,7 +477,7 @@ impl Display for Alignment {
|
|||
match self {
|
||||
Alignment::Village => f.write_str("Village"),
|
||||
Alignment::Wolves => f.write_str("Wolves"),
|
||||
Alignment::Traitor => f.write_str("Damned"),
|
||||
Alignment::Traitor => f.write_str("Traitor"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -485,7 +493,7 @@ pub enum ArcanistCheck {
|
|||
pub const MAPLE_WOLF_ABSTAIN_LIMIT: NonZeroU8 = NonZeroU8::new(3).unwrap();
|
||||
pub const PYREMASTER_VILLAGER_KILLS_TO_DIE: NonZeroU8 = NonZeroU8::new(2).unwrap();
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
||||
pub enum RoleBlock {
|
||||
Direwolf,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use core::{net::SocketAddr, time::Duration};
|
||||
|
||||
use crate::{
|
||||
AppState, LogError, XForwardedFor,
|
||||
AppState, XForwardedFor,
|
||||
connection::{ConnectionId, JoinedPlayer},
|
||||
runner::IdentifiedClientMessage,
|
||||
};
|
||||
|
|
@ -26,7 +26,7 @@ use axum::{
|
|||
},
|
||||
response::IntoResponse,
|
||||
};
|
||||
use axum_extra::{TypedHeader, headers};
|
||||
use axum_extra::TypedHeader;
|
||||
use chrono::Utc;
|
||||
use colored::Colorize;
|
||||
use tokio::sync::broadcast::{Receiver, Sender};
|
||||
|
|
@ -34,7 +34,6 @@ use werewolves_proto::message::{ClientMessage, Identification, ServerMessage, Up
|
|||
|
||||
pub async fn handler(
|
||||
ws: WebSocketUpgrade,
|
||||
user_agent: Option<TypedHeader<headers::UserAgent>>,
|
||||
x_forwarded_for: Option<TypedHeader<XForwardedFor>>,
|
||||
ConnectInfo(addr): ConnectInfo<SocketAddr>,
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -43,12 +42,7 @@ pub async fn handler(
|
|||
.map(|x| x.to_string())
|
||||
.unwrap_or_else(|| addr.to_string())
|
||||
.italic();
|
||||
// log::debug!(
|
||||
// "{who}{} connected.",
|
||||
// user_agent
|
||||
// .map(|agent| format!(" (User-Agent: {})", agent.as_str()))
|
||||
// .unwrap_or_default(),
|
||||
// );
|
||||
|
||||
let player_list = state.joined_players;
|
||||
|
||||
// finalize the upgrade process by returning upgrade callback.
|
||||
|
|
@ -208,7 +202,7 @@ impl Client {
|
|||
|
||||
return Ok(());
|
||||
}
|
||||
Message::Close(Some(close_frame)) => {
|
||||
Message::Close(Some(_)) => {
|
||||
// log::debug!("sent close frame: {close_frame:?}");
|
||||
return Ok(());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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 colored::Colorize;
|
||||
use tokio::sync::{broadcast::Sender, mpsc::Receiver};
|
||||
use werewolves_proto::{
|
||||
error::GameError,
|
||||
|
|
@ -45,6 +46,10 @@ impl HostComms {
|
|||
}
|
||||
|
||||
pub fn send(&mut self, message: ServerToHostMessage) -> Result<(), GameError> {
|
||||
log::debug!(
|
||||
"sending message to host: {}",
|
||||
format!("{message:?}").dimmed()
|
||||
);
|
||||
self.send
|
||||
.send(message)
|
||||
.map_err(|err| GameError::GenericError(err.to_string()))?;
|
||||
|
|
|
|||
|
|
@ -296,7 +296,7 @@ impl GameRunner {
|
|||
HostMessage::GetState => self.game.process(HostGameMessage::GetState),
|
||||
HostMessage::InGame(msg) => self.game.process(msg),
|
||||
HostMessage::Lobby(_) | HostMessage::PostGame(_) | HostMessage::ForceRoleAckFor(_) => {
|
||||
Err(GameError::InvalidMessageForGameState)
|
||||
Err(GameError::GameOngoing)
|
||||
}
|
||||
HostMessage::Echo(echo) => Ok(echo),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,6 @@ impl Host {
|
|||
msg = self.server_recv.recv() => {
|
||||
match msg {
|
||||
Ok(msg) => {
|
||||
log::debug!("sending message to host: {}", format!("{msg:?}").dimmed());
|
||||
if let Err(err) = self.send_message(&msg).await {
|
||||
log::error!("{} {err}", "[host::outgoing]".bold())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,9 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="45.363396mm"
|
||||
height="45.36343mm"
|
||||
viewBox="0 0 45.363396 45.36343"
|
||||
width="58.49649mm"
|
||||
height="55.059494mm"
|
||||
viewBox="0 0 58.49649 55.059494"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
|
|
@ -24,9 +24,9 @@
|
|||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="1"
|
||||
inkscape:cx="1108.5"
|
||||
inkscape:cy="797.49998"
|
||||
inkscape:zoom="2.8284271"
|
||||
inkscape:cx="470.75634"
|
||||
inkscape:cy="2967.1969"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1042"
|
||||
inkscape:window-x="0"
|
||||
|
|
@ -35,8 +35,8 @@
|
|||
inkscape:current-layer="layer4"><inkscape:grid
|
||||
id="grid1"
|
||||
units="mm"
|
||||
originx="-266.17087"
|
||||
originy="-217.22292"
|
||||
originx="-29.360038"
|
||||
originy="-825.92692"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458334"
|
||||
empcolor="#0099e5"
|
||||
|
|
@ -48,46 +48,232 @@
|
|||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="45.363396"
|
||||
height="45.36343"
|
||||
width="58.49649"
|
||||
height="55.059494"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
id="defs1" /><g
|
||||
id="defs1"><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-5"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-8"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-5-67"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-8-3"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /></defs><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
transform="translate(-266.17087,-217.22291)"><g
|
||||
id="g61"
|
||||
transform="translate(-56.091665,-3.7041666)"><path
|
||||
d="m 347.07926,221.66934 c -0.45815,-0.0281 -0.90857,0.004 -1.35031,0.0992 -1.05487,0.22775 -2.05439,0.76275 -2.99671,1.56425 -1.13002,-0.2664 -2.18934,-0.42422 -3.16415,-0.46147 -0.97482,-0.0373 -1.86513,0.0461 -2.65772,0.26096 -0.38599,0.10465 -0.74872,0.24076 -1.08675,0.40928 -0.41079,0.20479 -0.78496,0.45722 -1.11983,0.76068 -0.79968,0.72467 -1.398,1.68777 -1.81333,2.85306 -2.22365,0.66859 -3.98388,1.57251 -5.1418,2.7373 -0.28196,0.28362 -0.52842,0.58265 -0.73691,0.89762 -0.25336,0.38274 -0.45083,0.7888 -0.58911,1.21904 -0.33021,1.02743 -0.36713,2.16074 -0.14418,3.37757 -1.59144,1.69085 -2.66416,3.35361 -3.08456,4.9413 -0.10237,0.38661 -0.16607,0.76866 -0.18914,1.14567 -0.028,0.45815 0.004,0.90857 0.0992,1.35031 0.22775,1.05487 0.76275,2.05439 1.56424,2.99671 -0.5328,2.26004 -0.63027,4.23669 -0.2005,5.82187 0.10465,0.38599 0.24024,0.74872 0.40876,1.08675 0.20479,0.41079 0.45773,0.78496 0.76119,1.11983 0.72468,0.79968 1.68778,1.398 2.85306,1.81333 0.6686,2.22365 1.57252,3.9844 2.7373,5.14232 0.28363,0.28196 0.58266,0.5279 0.89762,0.73639 0.38275,0.25336 0.7888,0.45083 1.21905,0.58911 1.02742,0.33021 2.16074,0.36713 3.37757,0.14418 1.69085,1.59144 3.35361,2.66417 4.9413,3.08456 0.3866,0.10237 0.76866,0.16607 1.14567,0.18914 0.45814,0.028 0.90856,-0.004 1.3503,-0.0992 1.05488,-0.22774 2.05439,-0.76274 2.99672,-1.56424 2.26004,0.5328 4.23668,0.63027 5.82186,0.2005 0.386,-0.10465 0.74873,-0.24024 1.08676,-0.40876 0.41079,-0.20479 0.78496,-0.45773 1.11983,-0.76119 0.79967,-0.72468 1.398,-1.68778 1.81332,-2.85306 2.22366,-0.6686 3.9844,-1.57252 5.14233,-2.7373 0.28195,-0.28363 0.5279,-0.58266 0.73639,-0.89762 0.25335,-0.38275 0.45083,-0.7888 0.58911,-1.21905 0.3302,-1.02742 0.36713,-2.16074 0.14417,-3.37757 1.59144,-1.69085 2.66417,-3.35361 3.08457,-4.9413 0.10237,-0.3866 0.16606,-0.76866 0.18914,-1.14567 0.028,-0.45814 -0.004,-0.90856 -0.0992,-1.3503 -0.22775,-1.05488 -0.76275,-2.05439 -1.56425,-2.99672 0.5328,-2.26003 0.63028,-4.23668 0.2005,-5.82186 -0.10465,-0.386 -0.24024,-0.74873 -0.40876,-1.08676 -0.20479,-0.41079 -0.45773,-0.78495 -0.76119,-1.11983 -0.72467,-0.79967 -1.68777,-1.39799 -2.85306,-1.81332 -0.66859,-2.22366 -1.57251,-3.98389 -2.7373,-5.14181 -0.28362,-0.28195 -0.58266,-0.52842 -0.89762,-0.7369 -0.38274,-0.25336 -0.7888,-0.45084 -1.21904,-0.58912 -1.02743,-0.3302 -2.16075,-0.36713 -3.37757,-0.14417 -1.69085,-1.59144 -3.35361,-2.66417 -4.94131,-3.08457 -0.3866,-0.10237 -0.76866,-0.16606 -1.14566,-0.18913 z m -2.62206,11.10371 c 1.90227,0.35928 3.73774,0.76228 5.48338,1.2082 1.46778,1.26227 2.85564,2.5294 4.14445,3.78839 0.63999,1.82705 1.20834,3.61793 1.69499,5.35265 -0.35928,1.90227 -0.76228,3.73723 -1.2082,5.48287 -1.26227,1.46777 -2.52889,2.85563 -3.78788,4.14445 -1.82705,0.63999 -3.61792,1.20833 -5.35264,1.69499 -1.90227,-0.35928 -3.73775,-0.76229 -5.48339,-1.2082 -1.46777,-1.26228 -2.85563,-2.52889 -4.14445,-3.78788 -0.63999,-1.82705 -1.20833,-3.61792 -1.69499,-5.35265 0.35928,-1.90226 0.76229,-3.73774 1.2082,-5.48338 1.26228,-1.46778 2.52889,-2.85564 3.78788,-4.14445 1.82705,-0.63999 3.61792,-1.20834 5.35265,-1.69499 z"
|
||||
style="fill:#d5ffc9;fill-opacity:1;stroke:#5fff32;stroke-width:1.465;stroke-opacity:1"
|
||||
id="path40-7" /><path
|
||||
sodipodi:type="spiral"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#86ff61;stroke-width:0.565;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path61"
|
||||
sodipodi:cx="342.65289"
|
||||
sodipodi:cy="241.25082"
|
||||
sodipodi:expansion="1"
|
||||
sodipodi:revolution="9.8757124"
|
||||
sodipodi:radius="19.917585"
|
||||
sodipodi:argument="-17.350523"
|
||||
sodipodi:t0="0.0044709877"
|
||||
d="m 342.63471,241.338 c -0.14905,0.28891 -0.50558,-0.0176 -0.56257,-0.20833 -0.14974,-0.50126 0.34837,-0.9171 0.80488,-0.95318 0.77741,-0.0615 1.34953,0.67239 1.34379,1.40143 -0.008,1.04713 -0.99858,1.78883 -1.99798,1.7344 -1.31593,-0.0717 -2.23097,-1.32542 -2.12501,-2.59452 0.1323,-1.58458 1.65253,-2.6746 3.19107,-2.51563 1.85321,0.19148 3.11907,1.9798 2.90624,3.78762 -0.24981,2.12187 -2.30716,3.56409 -4.38417,3.29685 -2.39056,-0.30758 -4.00948,-2.63459 -3.68746,-4.98072 0.36499,-2.65927 2.96206,-4.45512 5.57726,-4.07807 2.92802,0.42215 4.90096,3.28957 4.46869,6.17381 -0.47911,3.19677 -3.61711,5.34694 -6.77036,4.8593 -3.46555,-0.53593 -5.79303,-3.94466 -5.24992,-7.36691 0.59265,-3.73433 4.27224,-6.2392 7.96347,-5.64052 4.00312,0.64926 6.68544,4.59982 6.03113,8.56001 -0.70581,4.27192 -4.92742,7.13174 -9.15656,6.42174 -4.54073,-0.76231 -7.57809,-5.25502 -6.81236,-9.7531 0.81876,-4.80955 5.58264,-8.02449 10.34966,-7.20298 5.07837,0.87517 8.4709,5.91026 7.59358,10.94621 -0.93154,5.3472 -6.23788,8.91735 -11.54275,7.98419 -5.61603,-0.98788 -9.36382,-6.56551 -8.37481,-12.1393 1.04421,-5.88486 6.89315,-9.81031 12.73585,-8.76542 6.1537,1.10051 10.25683,7.22079 9.15603,13.3324 -1.15679,6.42254 -7.54842,10.70335 -13.92894,9.54665 -6.69138,-1.21307 -11.14989,-7.87608 -9.93726,-14.5255 1.26932,-6.96022 8.20372,-11.59645 15.12205,-10.32787 7.22906,1.32556 12.043,8.53137 10.71848,15.7186 -1.38179,7.49791 -8.85902,12.48958 -16.31515,11.10909 -7.76676,-1.43801 -12.93616,-9.18668 -11.4997,-16.91169 1.49422,-8.03561 9.51433,-13.38276 17.50824,-11.89032 8.30446,1.55042 13.82935,9.84199 12.28093,18.10479 -1.60662,8.57331 -10.16965,14.27596 -18.70134,12.67154 -8.84216,-1.66281 -14.72257,-10.4973 -13.06215,-19.29789 1.71899,-9.11102 10.82496,-15.16918 19.89443,-13.45276 9.37988,1.77516 15.61581,11.15262 13.84338,20.49098 -1.83134,9.64873 -11.48029,16.06243 -21.08753,14.23399 -9.91759,-1.88751 -16.50906,-11.80795 -14.6246,-21.68408 1.94367,-10.18644 12.13561,-16.95569 22.28063,-15.01521 10.4553,1.99983 17.40232,12.46328 15.40582,22.87718 -0.63903,3.33322 -2.14652,6.47994 -4.33787,9.07108"
|
||||
transform="translate(1.7875357,2.8617786)" /><g
|
||||
id="g59"><path
|
||||
d="m 344.4572,232.77302 c -1.73473,0.48665 -3.5256,1.055 -5.35265,1.69499 -1.25899,1.28881 -2.5256,2.67667 -3.78788,4.14445 -0.44591,1.74564 -0.84891,3.58112 -1.20819,5.48338 0.48665,1.73473 1.055,3.5256 1.69498,5.35265 1.28882,1.25899 2.67668,2.5256 4.14445,3.78788 1.74564,0.44591 3.58112,0.84892 5.48339,1.2082 1.73472,-0.48666 3.52559,-1.055 5.35264,-1.69499 1.25899,-1.28882 2.52561,-2.67668 3.78789,-4.14445 0.44591,-1.74564 0.84891,-3.5806 1.20819,-5.48287 -0.48665,-1.73472 -1.055,-3.52559 -1.69499,-5.35264 -1.28881,-1.25899 -2.67667,-2.52613 -4.14445,-3.7884 -1.74564,-0.44592 -3.58111,-0.84892 -5.48338,-1.2082 z"
|
||||
style="fill:#26d734;fill-opacity:1;stroke:#9a9700;stroke-width:1.465;stroke-opacity:0.600002"
|
||||
id="path42-5" /><path
|
||||
sodipodi:type="spiral"
|
||||
style="fill:none;fill-rule:evenodd;stroke:#fffa32;stroke-width:1.065;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path59"
|
||||
sodipodi:cx="335.75626"
|
||||
sodipodi:cy="233.09792"
|
||||
sodipodi:expansion="1"
|
||||
sodipodi:revolution="3"
|
||||
sodipodi:radius="10.089354"
|
||||
sodipodi:argument="-19.333729"
|
||||
sodipodi:t0="0"
|
||||
d="m 335.75626,233.09792 c 0.44772,-0.23547 0.53829,0.48734 0.39136,0.74414 -0.39818,0.69589 -1.39291,0.51861 -1.87964,0.0386 -0.87067,-0.85865 -0.55945,-2.28928 0.31419,-3.01515 1.28209,-1.06524 3.19926,-0.60551 4.15065,0.66697 1.26805,1.69602 0.65405,4.11428 -1.01975,5.28616 -2.1063,1.47469 -5.03172,0.70392 -6.42166,-1.37253 -1.68339,-2.51485 -0.75456,-5.95055 1.7253,-7.55716 2.92244,-1.89333 6.87026,-0.8057 8.69267,2.07808 2.10404,3.32944 0.85716,7.79053 -2.43086,9.82817 -3.73605,2.31529 -8.71121,0.90885 -10.96367,-2.78363 -2.52692,-4.14241 -0.9607,-9.63219 3.13641,-12.09918 4.54858,-2.73883 10.55339,-1.01268 13.23468,3.48919"
|
||||
transform="matrix(0.8,0,0,0.8,76.234013,57.800034)" /></g></g></g></svg>
|
||||
transform="translate(-29.360037,-825.92694)"><g
|
||||
id="g43"
|
||||
transform="translate(0.07389574)"><g
|
||||
id="g42-4"
|
||||
transform="matrix(0.70710678,0.70711411,-0.70710678,0.70711411,584.35218,19.075908)"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0"
|
||||
cx="640.62866"
|
||||
cy="431.71231"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264582px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||
id="path45-1"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-4"
|
||||
inkscape:path-effect="#path-effect41-5"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-4"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-3"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-0"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-7"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(7.5790061e-6)" /></g><g
|
||||
id="g42-1"
|
||||
transform="rotate(-45,70.430131,928.10363)"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0-9"
|
||||
cx="-213.94203"
|
||||
cy="831.42212"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||
id="path45-1-7"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-9"
|
||||
inkscape:path-effect="#path-effect41-8"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-2"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-0"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-6"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-9"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-4"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(-9.3315134e-6)" /></g><g
|
||||
id="g63"><path
|
||||
id="rect33-6"
|
||||
style="fill:#005c01;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 57.136103,850.95756 -1.50032,-0.57183 -1.84004,1.853 3.34036,2.084 1.2e-4,13.42718 c 0,0 0.4451,1.28623 1.346,1.28623 0.83175,0 1.346,-1.28623 1.346,-1.28623 l -2.9e-4,-13.42718 2.71198,-2.084 -1.37887,-1.853 -1.33311,0.57183 z"
|
||||
sodipodi:nodetypes="cccccscccccc" /><path
|
||||
d="m 60.527143,867.65428 c 0,0 -1.28908,1.47478 -2.12083,1.47478 -0.9009,0 -1.99075,-1.42264 -1.99075,-1.42264 -4.65194,0.59664 -5.92716,3.58427 -8.28253,4.70926 -2.52584,1.20643 -11.53768,1.98812 -11.53779,3.7052 10e-6,2.41107 9.83202,4.36562 21.96041,4.36562 12.12839,0 21.96041,-1.95455 21.96042,-4.36562 10e-6,-1.68893 -9.37115,-3.14116 -11.61634,-3.7052 -2.57054,-0.64577 -3.5869,-4.17719 -8.37259,-4.7614 z"
|
||||
style="fill:#8f4c00;stroke:#5c3100;stroke-linecap:round"
|
||||
id="path57-3"
|
||||
sodipodi:nodetypes="cscscsssc" /><g
|
||||
id="g42-4-4"
|
||||
transform="matrix(0.46393276,0.46393757,-0.46393276,0.46393757,360.22685,355.15504)"
|
||||
style="stroke-width:1.52415;stroke-dasharray:none"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0-8"
|
||||
cx="640.62866"
|
||||
cy="431.71231"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52415;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||
id="path45-1-1"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-4-2"
|
||||
inkscape:path-effect="#path-effect41-5-67"
|
||||
style="stroke-width:1.52415;stroke-dasharray:none"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-4-9"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-3-3"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-0-9"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-7-0"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-8-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52415;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-8"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(7.5790061e-6)" /></g><g
|
||||
id="g42-1-5"
|
||||
transform="matrix(0.46393276,-0.46393276,0.46393276,0.46393276,-441.6633,553.4829)"
|
||||
style="stroke-width:1.52416;stroke-dasharray:none"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0-9-0"
|
||||
cx="-213.94203"
|
||||
cy="831.42212"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52416;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||
id="path45-1-7-9"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-9-6"
|
||||
inkscape:path-effect="#path-effect41-8-3"
|
||||
style="stroke-width:1.52416;stroke-dasharray:none"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-2-3"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-0-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-6-5"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-8-6"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-9-1"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-4-1"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(-9.3315134e-6)" /></g></g></g></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -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 |
|
|
@ -12,15 +12,16 @@
|
|||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer4"
|
||||
transform="translate(-50.121599,-612.75354)"><g
|
||||
id="g9"><rect
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="translate(-50.121599,-612.75355)"><g
|
||||
id="g9"
|
||||
style="stroke:#000000;stroke-opacity:1"><rect
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6"
|
||||
width="80"
|
||||
height="20"
|
||||
x="50.902103"
|
||||
y="613.53406" /><rect
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6-1"
|
||||
width="80"
|
||||
height="20"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 932 B After Width: | Height: | Size: 979 B |
|
|
@ -2,158 +2,29 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="24.399992mm"
|
||||
height="24.399998mm"
|
||||
viewBox="0 0 24.399992 24.399998"
|
||||
width="29.583593mm"
|
||||
height="29.755226mm"
|
||||
viewBox="0 0 29.583593 29.755226"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="icons.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8284272"
|
||||
inkscape:cx="35.885668"
|
||||
inkscape:cy="411.71291"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1042"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="17"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer4"><inkscape:grid
|
||||
id="grid1"
|
||||
units="mm"
|
||||
originx="-1.6153444"
|
||||
originy="-96.526486"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458334"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="24.399992"
|
||||
height="24.399998"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
id="defs1"><inkscape:path-effect
|
||||
effect="mirror_symmetry"
|
||||
start_point="50.372741,180.48552"
|
||||
end_point="50.372741,189.138"
|
||||
center_point="50.372741,184.81176"
|
||||
id="path-effect166-2"
|
||||
is_visible="true"
|
||||
lpeversion="1.2"
|
||||
lpesatellites=""
|
||||
mode="free"
|
||||
discard_orig_path="false"
|
||||
fuse_paths="true"
|
||||
oposite_fuse="false"
|
||||
split_items="false"
|
||||
split_open="false"
|
||||
link_styles="false" /></defs><g
|
||||
inkscape:groupmode="layer"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
transform="translate(-1.6153444,-96.526492)"><g
|
||||
id="g166-2"
|
||||
inkscape:path-effect="#path-effect166-2"
|
||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-dasharray:none;stroke-opacity:1"
|
||||
transform="matrix(1.728,0,0,1.728,-72.928813,-210.62831)"
|
||||
inkscape:export-filename="hunter.svg"
|
||||
inkscape:export-xdpi="900.08"
|
||||
inkscape:export-ydpi="900.08"><path
|
||||
id="path164-8"
|
||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="m 50.373047,183.43359 c -1.534096,0.26874 -1.029204,1.24619 -1.832031,2.63672 -0.65564,1.13559 -1.854319,1.39291 -1.029297,2.41211 0.94012,1.1614 1.512735,0.45231 2.861328,0.33008 1.348593,0.12223 1.921208,0.83132 2.861328,-0.33008 0.825022,-1.0192 -0.373657,-1.27652 -1.029297,-2.41211 -0.802827,-1.39053 -0.297935,-2.36798 -1.832031,-2.63672 z"
|
||||
inkscape:original-d="m 50.372741,183.43371 c -1.534096,0.26874 -1.028586,1.24549 -1.831413,2.63602 -0.65564,1.13559 -1.854933,1.39253 -1.029911,2.41173 0.94012,1.1614 1.512731,0.45348 2.861324,0.33125 z" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path165-3"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="2"
|
||||
sodipodi:cx="51.634216"
|
||||
sodipodi:cy="182.45293"
|
||||
sodipodi:r1="1.3725731"
|
||||
sodipodi:r2="0.82291079"
|
||||
sodipodi:arg1="1.1071487"
|
||||
sodipodi:arg2="2.677945"
|
||||
inkscape:rounded="0.5"
|
||||
inkscape:randomized="0"
|
||||
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 5.18186,0 c 0.7157,0.35785 0.992016,-0.14395 1.349867,-0.85965 0.35785,-0.7157 0.5935,-1.23783 -0.122201,-1.59568 -0.715701,-0.35785 -0.992016,0.14395 -1.349867,0.85965 -0.35785,0.7157 -0.5935,1.23783 0.122201,1.59568 z"
|
||||
inkscape:transform-center-x="0.12681959"
|
||||
inkscape:transform-center-y="0.079724714"
|
||||
transform="translate(-4.4662386,1.8414355)" /><path
|
||||
sodipodi:type="star"
|
||||
style="fill:#ff0707;fill-opacity:0.697154;stroke:#c10000;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="path165-6-8"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="2"
|
||||
sodipodi:cx="51.634216"
|
||||
sodipodi:cy="182.45293"
|
||||
sodipodi:r1="1.3725731"
|
||||
sodipodi:r2="0.82291079"
|
||||
sodipodi:arg1="1.1071487"
|
||||
sodipodi:arg2="2.677945"
|
||||
inkscape:rounded="0.5"
|
||||
inkscape:randomized="0"
|
||||
d="m 52.24805,183.68059 c -0.715701,0.35785 -0.992017,-0.14395 -1.349867,-0.85965 -0.357851,-0.7157 -0.593501,-1.23783 0.1222,-1.59568 0.715701,-0.35785 0.992017,0.14395 1.349867,0.85965 0.357851,0.7157 0.593501,1.23783 -0.1222,1.59568 z m 2.374602,-1.10775 c 0.734072,-0.31847 0.527114,-0.85263 0.208644,-1.5867 -0.31847,-0.73407 -0.567138,-1.25013 -1.30121,-0.93166 -0.734072,0.31847 -0.527114,0.85263 -0.208644,1.5867 0.31847,0.73407 0.567138,1.25013 1.30121,0.93166 z"
|
||||
inkscape:transform-center-x="0.14863452"
|
||||
inkscape:transform-center-y="0.01863483"
|
||||
transform="rotate(25.009099,51.670619,176.27381)" /></g><g
|
||||
id="g6"
|
||||
inkscape:export-filename="../../src/werewolves/werewolves/img/hunter.svg"
|
||||
inkscape:export-xdpi="900.08"
|
||||
inkscape:export-ydpi="900.08"><path
|
||||
id="path166-0"
|
||||
style="fill:#0f07ff;fill-opacity:0.496669;stroke:#05009e;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
d="m 13.815426,98.72668 c -5.522776,5e-5 -10.000374,4.47713 -10.000423,9.99991 4.9e-5,5.52277 4.477647,9.99985 10.000423,9.9999 5.522777,-5e-5 9.999857,-4.47713 9.999907,-9.9999 -5e-5,-5.52278 -4.47713,-9.99986 -9.999907,-9.99991 z m 0,1.99988 c 4.418297,-2e-5 8.000047,3.58172 8.000027,8.00003 2e-5,4.4183 -3.58173,8.00004 -8.000027,8.00002 -4.418301,2e-5 -8.000045,-3.58172 -8.000029,-8.00002 -1.6e-5,-4.41831 3.581728,-8.00005 8.000029,-8.00003 z" /><rect
|
||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect167-8-0-4"
|
||||
width="1.5"
|
||||
height="6"
|
||||
x="-109.47657"
|
||||
y="19.815336"
|
||||
transform="rotate(-90)"
|
||||
ry="0" /><rect
|
||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect167-8-0-9-2"
|
||||
width="1.5"
|
||||
height="6"
|
||||
x="-109.47657"
|
||||
y="1.8153445"
|
||||
transform="rotate(-90)"
|
||||
ry="0" /><rect
|
||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect167-8-0-1-2"
|
||||
width="1.5"
|
||||
height="6"
|
||||
x="-14.56517"
|
||||
y="-120.72649"
|
||||
transform="scale(-1)"
|
||||
ry="0" /><rect
|
||||
style="fill:#0f07ff;fill-opacity:1;stroke:#05009e;stroke-width:0.4;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
|
||||
id="rect167-8-0-1-7-2"
|
||||
width="1.5"
|
||||
height="6"
|
||||
x="-14.56517"
|
||||
y="-102.72649"
|
||||
transform="scale(-1)"
|
||||
ry="0" /></g></g></svg>
|
||||
transform="translate(-151.13398,-751.15207)"><path
|
||||
d="m 165.17026,751.95407 c -4.4181,2.7e-4 -7.99953,3.58193 -7.99951,8.00003 0.006,2.34034 1.03726,4.56053 2.82102,6.0756 -1.78376,1.51507 -2.81464,3.73526 -2.82102,6.0756 -2e-5,4.4181 3.58141,7.99976 7.99951,8.00003 0.61448,5e-5 1.22714,-0.0711 1.82543,-0.21119 -3.61703,-0.84782 -6.17466,-4.07378 -6.17452,-7.78884 0.006,-2.34034 1.03726,-4.56053 2.82102,-6.0756 -1.78376,-1.51507 -2.81464,-3.73526 -2.82102,-6.0756 -1.4e-4,-3.71506 2.55749,-6.94114 6.17452,-7.78896 -0.59829,-0.14014 -1.21095,-0.21112 -1.82543,-0.21107 z"
|
||||
style="fill:#070098;fill-opacity:1;stroke:#030033;stroke-width:1.604;stroke-opacity:1"
|
||||
id="path36-1" /><path
|
||||
id="rect83-5-3"
|
||||
style="fill:#c04040;fill-opacity:1;stroke:#bf4141;stroke-width:0.399604;stroke-linecap:butt;stroke-dasharray:none;stroke-opacity:0.498353"
|
||||
d="m 172.72687,766.02988 c 0.20968,1.88003 7.36137,4.2774 7.36137,2.18354 0,-0.43048 0.002,-1.62424 0,-2.18355 0.002,-0.55929 0,-1.75348 0,-2.18395 0,-2.09388 -7.15169,0.30393 -7.36137,2.18396 z" /><path
|
||||
id="path86-3"
|
||||
style="fill:#999999;fill-opacity:1;stroke:#000000;stroke-width:0.399604;stroke-opacity:0.498039"
|
||||
d="m 161.23317,763.86155 -10.0552,2.26911 10.0552,2.2686 z" /><path
|
||||
id="path88"
|
||||
style="fill:#070098;fill-opacity:1;stroke:#030033;stroke-width:0.419304;stroke-opacity:1"
|
||||
d="m 157.61727,765.84131 v 0.57818 h 22.12149 c -1.2e-4,-0.20698 2.3e-4,-0.4071 5.8e-4,-0.57818 z" /><path
|
||||
d="m 166.393,752.281 13.58876,13.749 -13.58376,13.75 c 0.14274,0.0419 0.28736,0.0802 0.43366,0.1145 L 180.5316,766.03 166.83166,752.16544 c -0.14795,0.0347 -0.29436,0.0731 -0.43866,0.11556 z"
|
||||
style="fill:#070098;fill-opacity:1;stroke:#030033;stroke-width:0.264583px;stroke-opacity:1"
|
||||
id="path39-7" /></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 2.2 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
|
@ -2,9 +2,9 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="44.180328mm"
|
||||
height="65.150642mm"
|
||||
viewBox="0 0 44.180328 65.150642"
|
||||
width="32.022381mm"
|
||||
height="20.678709mm"
|
||||
viewBox="0 0 32.022381 20.678709"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
|
|
@ -24,9 +24,9 @@
|
|||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2"
|
||||
inkscape:cx="93.5"
|
||||
inkscape:cy="1629.25"
|
||||
inkscape:zoom="0.35355339"
|
||||
inkscape:cx="660.43774"
|
||||
inkscape:cy="2131.2199"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1042"
|
||||
inkscape:window-x="0"
|
||||
|
|
@ -35,8 +35,8 @@
|
|||
inkscape:current-layer="layer4"><inkscape:grid
|
||||
id="grid1"
|
||||
units="mm"
|
||||
originx="-11.103428"
|
||||
originy="-387.35551"
|
||||
originx="-160.21693"
|
||||
originy="-815.99713"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458334"
|
||||
empcolor="#0099e5"
|
||||
|
|
@ -47,71 +47,268 @@
|
|||
enabled="true"
|
||||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="44.180328"
|
||||
height="65.150642"
|
||||
y="-6.3560056e-12"
|
||||
width="32.022381"
|
||||
height="20.678711"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
id="defs1" /><g
|
||||
id="defs1"><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-5"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-8"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-5-67"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /><inkscape:path-effect
|
||||
effect="interpolate_points"
|
||||
id="path-effect41-8-3"
|
||||
is_visible="true"
|
||||
lpeversion="1"
|
||||
interpolator_type="CubicBezierJohan" /></defs><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
transform="translate(-11.103428,-387.3555)"><g
|
||||
id="g113"
|
||||
transform="translate(-160.21694,-815.99709)"><path
|
||||
id="path82-8"
|
||||
style="fill:#cccccc;fill-opacity:1;stroke:#ffffff;stroke-linecap:round;stroke-opacity:0.7"
|
||||
d="m 162.68403,816.49763 c -0.0381,-9.5e-4 -0.0769,-6.2e-4 -0.11679,0.002 -0.58468,0.0318 -0.9946,0.44045 -1.27951,0.92811 -0.38899,0.40947 -0.66926,0.91944 -0.53692,1.48983 0.31028,1.33731 2.14116,1.1325 2.88458,1.68207 v 5.73608 5.73815 c -0.74342,0.54957 -2.5743,0.34424 -2.88458,1.68155 -0.0248,0.10695 -0.0358,0.21228 -0.0336,0.31471 0.01,0.44389 0.25446,0.84295 0.57051,1.17564 0.28491,0.48766 0.69483,0.89632 1.27951,0.92811 1.2754,0.0694 1.63346,-1.49789 2.26911,-2.16834 h 11.39104 11.39258 c 0.63565,0.67045 0.99371,2.23774 2.26911,2.16834 0.58468,-0.0318 0.9946,-0.44045 1.27951,-0.92811 0.31605,-0.33269 0.56051,-0.73175 0.57051,-1.17564 0.002,-0.10243 -0.009,-0.20776 -0.0336,-0.31471 -0.31028,-1.33731 -2.14116,-1.13198 -2.88458,-1.68155 v -5.73815 -5.73608 c 0.74342,-0.54957 2.5743,-0.34476 2.88458,-1.68207 0.13234,-0.57039 -0.14793,-1.08036 -0.53692,-1.48983 -0.28491,-0.48766 -0.69483,-0.89631 -1.27951,-0.92811 -1.2754,-0.0694 -1.63346,1.49789 -2.26911,2.16834 h -11.39258 -11.39104 c -0.61579,-0.6495 -0.97116,-2.14045 -2.15232,-2.16989 z"
|
||||
inkscape:export-filename="insomniac.svg"
|
||||
inkscape:export-xdpi="900.08"
|
||||
inkscape:export-ydpi="900.08"
|
||||
style="stroke-width:1.3;stroke-dasharray:none"><path
|
||||
id="path95-7"
|
||||
style="fill:#b3b3b3;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 42.63286,411.37387 -30.86943,0.0811 v 36.01795 a 15.478125,4.2333331 0 0 0 -0.01,0.14986 15.478125,4.2333331 0 0 0 0.01,0.14935 v 0.015 h 0.002 a 15.478125,4.2333331 0 0 0 15.46675,4.06901 15.478125,4.2333331 0 0 0 15.47813,-4.23334 15.478125,4.2333331 0 0 0 -0.077,-0.42168 z" /><path
|
||||
id="path102"
|
||||
style="fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 42.63398,417.52803 v 3.99975 a 8,8 0 0 1 8.00003,8.00003 8,8 0 0 1 -8.00003,8.00003 v 3.99976 A 12,12 0 0 0 54.63376,429.52781 12,12 0 0 0 42.63398,417.52803 Z" /><g
|
||||
id="g105"
|
||||
transform="translate(-334.81852,-83.628473)"
|
||||
style="stroke-width:1.3;stroke-dasharray:none"><ellipse
|
||||
style="fill:#808080;fill-opacity:1;stroke:#000000;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path95"
|
||||
cx="362.05063"
|
||||
cy="495.0354"
|
||||
rx="15.478125"
|
||||
ry="4.2333331" /><path
|
||||
d="m 376.92945,495.93872 c -0.44231,-1.99962 -6.96407,-3.56361 -14.86007,-3.56361 -8.02391,-2e-5 -14.55676,1.54313 -14.8283,3.57717 1.94328,1.91824 7.94419,3.085 14.77662,3.08496 6.95042,10e-6 12.71785,-1.1183 14.91175,-3.09852 z"
|
||||
style="fill:#804600;stroke:none;stroke-width:1.3;stroke-dasharray:none"
|
||||
id="path105"
|
||||
sodipodi:nodetypes="cscsc" /></g><g
|
||||
id="g103"
|
||||
transform="translate(-332.61358,-85.305623)"
|
||||
style="stroke-width:1.3;stroke-dasharray:none"><path
|
||||
style="fill:none;stroke:#804600;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 356.82899,473.29282 c -3.38145,0.8196 -7.31399,4.94369 -5.26687,8.00676 3.51296,5.25639 4.89649,7.3595 1.05739,12.10527"
|
||||
id="path103"
|
||||
sodipodi:nodetypes="csc" /><path
|
||||
style="fill:none;stroke:#804600;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 363.70426,473.29282 c -3.38145,0.8196 -7.31399,4.94369 -5.26687,8.00676 3.51296,5.25639 4.89649,7.3595 1.05739,12.10527"
|
||||
id="path103-2"
|
||||
sodipodi:nodetypes="csc" /><path
|
||||
style="fill:none;stroke:#804600;stroke-width:1.3;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 370.57952,473.29282 c -3.38145,0.8196 -7.31399,4.94369 -5.26687,8.00676 3.51296,5.25639 4.89649,7.3595 1.05739,12.10527"
|
||||
id="path103-6"
|
||||
sodipodi:nodetypes="csc" /></g><path
|
||||
sodipodi:type="star"
|
||||
style="fill:none;fill-opacity:0;stroke:#4d2a00;stroke-width:3.17383;stroke-dasharray:none;stroke-opacity:0.698282"
|
||||
id="path84"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="5"
|
||||
sodipodi:cx="358.90729"
|
||||
sodipodi:cy="61.780209"
|
||||
sodipodi:r1="16.631435"
|
||||
sodipodi:r2="24.204502"
|
||||
sodipodi:arg1="-2.7064071"
|
||||
sodipodi:arg2="0.21005099"
|
||||
inkscape:rounded="0.6"
|
||||
inkscape:randomized="0"
|
||||
d="m 343.82604,54.76875 c 9.88839,22.253806 17.47085,23.892638 38.75374,12.058334 16.02631,-8.911392 -8.02828,-33.81664 -21.66456,-21.556656 -18.10895,16.281225 -17.32446,23.999 0.50741,40.58323 13.42764,12.488151 29.68066,-18.085265 13.80688,-27.265597 -21.08034,-12.191455 -28.17796,-9.06044 -38.44015,13.023483 -7.72757,16.629493 26.37194,22.63933 30.19768,4.70559 5.08058,-23.815959 -0.0905,-29.598661 -24.26472,-32.534276 -18.20354,-2.210558 -13.38191,32.077141 4.85631,30.173812 24.22031,-2.527617 28.12204,-9.232538 23.44372,-33.130771 -3.52283,-17.995694 -34.64241,-2.814567 -27.19631,13.942851 z"
|
||||
transform="matrix(0.40959999,0,0,0.40959999,-120.0474,406.73268)" /><path
|
||||
d="m 26.89965,426.0882 c -0.667117,0.62908 -1.259228,1.22677 -1.777669,1.80041 -1.449367,0.19777 -2.672199,0.97551 -3.838526,2.36885 0.392134,0.82886 0.777882,1.57708 1.163237,2.24741 -0.259786,1.43954 0.101868,2.84235 1.066601,4.38216 0.909469,-0.11681 1.740402,-0.252 2.497006,-0.41135 1.288809,0.69192 2.734836,0.78121 4.4974,0.33952 0.169949,-0.90105 0.29757,-1.73284 0.379821,-2.50166 1.056314,-1.01192 1.588482,-2.35957 1.713074,-4.17235 -0.804435,-0.44008 -1.556108,-0.8185 -2.261877,-1.1343 -0.635972,-1.31732 -1.753505,-2.24001 -3.439067,-2.91869 z"
|
||||
style="fill:#804600;fill-opacity:0.701315;stroke:none;stroke-width:1.3;stroke-dasharray:none;stroke-opacity:0.497152"
|
||||
id="path113" /></g></g></svg>
|
||||
inkscape:export-ydpi="900.08" /><g
|
||||
id="g43"
|
||||
transform="translate(0.07389574)"><g
|
||||
id="g42-4"
|
||||
transform="matrix(0.70710678,0.70711411,-0.70710678,0.70711411,584.35218,19.075908)"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0"
|
||||
cx="640.62866"
|
||||
cy="431.71231"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264582px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||
id="path45-1"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-4"
|
||||
inkscape:path-effect="#path-effect41-5"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-4"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-3"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-0"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-7"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(7.5790061e-6)" /></g><g
|
||||
id="g42-1"
|
||||
transform="rotate(-45,70.430131,928.10363)"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.00001;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0-9"
|
||||
cx="-213.94203"
|
||||
cy="831.42212"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||
id="path45-1-7"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-9"
|
||||
inkscape:path-effect="#path-effect41-8"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-2"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-0"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-6"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-9"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-4"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(-9.3315134e-6)" /></g><g
|
||||
id="g63"
|
||||
inkscape:export-filename="apprentice.svg"
|
||||
inkscape:export-xdpi="900.08"
|
||||
inkscape:export-ydpi="900.08"><path
|
||||
id="rect33-6"
|
||||
style="fill:#005c01;fill-opacity:1;stroke:#005c01;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 57.136103,850.95756 -1.50032,-0.57183 -1.84004,1.853 3.34036,2.084 1.2e-4,13.42718 c 0,0 0.4451,1.28623 1.346,1.28623 0.83175,0 1.346,-1.28623 1.346,-1.28623 l -2.9e-4,-13.42718 2.71198,-2.084 -1.37887,-1.853 -1.33311,0.57183 z"
|
||||
sodipodi:nodetypes="cccccscccccc" /><path
|
||||
d="m 60.527143,867.65428 c 0,0 -1.28908,1.47478 -2.12083,1.47478 -0.9009,0 -1.99075,-1.42264 -1.99075,-1.42264 -4.65194,0.59664 -5.92716,3.58427 -8.28253,4.70926 -2.52584,1.20643 -11.53768,1.98812 -11.53779,3.7052 10e-6,2.41107 9.83202,4.36562 21.96041,4.36562 12.12839,0 21.96041,-1.95455 21.96042,-4.36562 10e-6,-1.68893 -9.37115,-3.14116 -11.61634,-3.7052 -2.57054,-0.64577 -3.5869,-4.17719 -8.37259,-4.7614 z"
|
||||
style="fill:#8f4c00;stroke:#5c3100;stroke-linecap:round"
|
||||
id="path57-3"
|
||||
sodipodi:nodetypes="cscscsssc" /><g
|
||||
id="g42-4-4"
|
||||
transform="matrix(0.46393276,0.46393757,-0.46393276,0.46393757,360.22685,355.15504)"
|
||||
style="stroke-width:1.52415;stroke-dasharray:none"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0-8"
|
||||
cx="640.62866"
|
||||
cy="431.71231"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(0.99999481,0,0,0.99999481,-426.68795,399.71083)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52415;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 214.03823,813.19332 c 0.71309,-15.32992 8.97923,-0.53496 12.11391,4.16265 6.26402,9.3872 0.46548,27.18307 -0.66824,29.84063 -2.16386,5.07231 -9.57501,9.49461 -9.57501,9.49461 l -1.73056,0.14033 c 0.22402,-14.53822 -0.81593,-29.10943 -0.1401,-43.63822 z"
|
||||
id="path45-1-1"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-4-2"
|
||||
inkscape:path-effect="#path-effect41-5-67"
|
||||
style="stroke-width:1.52415;stroke-dasharray:none"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-4-9"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-3-3"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-0-9"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-7-0"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52415;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-8-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52415;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-8"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(7.5790061e-6)" /></g><g
|
||||
id="g42-1-5"
|
||||
transform="matrix(0.46393276,-0.46393276,0.46393276,0.46393276,-441.6633,553.4829)"
|
||||
style="stroke-width:1.52416;stroke-dasharray:none"><ellipse
|
||||
style="fill:#00c002;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-7-0-9-0"
|
||||
cx="-213.94203"
|
||||
cy="831.42212"
|
||||
rx="15.000077"
|
||||
ry="25.00013"
|
||||
transform="matrix(-1,5.1823562e-6,-5.1823562e-6,1,0,0)" /><path
|
||||
style="fill:#008f01;fill-opacity:1;stroke:none;stroke-width:1.52416;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.63731,813.23745 c -0.71301,-15.33 -8.97927,-0.53492 -12.11399,4.16273 -6.2641,9.38728 -0.46562,27.18322 0.66809,29.84078 2.16384,5.07233 9.57501,9.49461 9.57501,9.49461 l 1.73057,0.14033 c -0.22395,-14.5383 0.81608,-29.10959 0.14032,-43.63845 z"
|
||||
id="path45-1-7-9"
|
||||
sodipodi:nodetypes="sssccs" /><g
|
||||
id="g37-9-6"
|
||||
inkscape:path-effect="#path-effect41-8-3"
|
||||
style="stroke-width:1.52416;stroke-dasharray:none"><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 213.93749,856.03808 c 0,0 2e-5,-42.87351 2e-5,-42.87351"
|
||||
id="path36-6-2-3"
|
||||
inkscape:original-d="m 213.93749,856.03808 2e-5,-42.87351" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,838.83313 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-1-5-0-8"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,838.83313 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,830.04627 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-4-6-6-5"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,830.04627 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 222.85539,821.2594 c -1.78358,0 -7.13431,8.99583 -8.91789,8.99583 -1.78358,0 -7.13431,-8.99583 -8.91789,-8.99583"
|
||||
id="path37-2-6-10-9-8-6"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 222.85539,821.2594 -8.91789,8.99583 -8.91789,-8.99583" /><path
|
||||
style="fill:none;stroke:#005c01;stroke-width:1.52416;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 220.85563,815.47302 c -1.38363,0 -5.5345,5.99535 -6.91813,5.99535 -1.38357,0 -5.5343,-5.99535 -6.91787,-5.99535"
|
||||
id="path37-2-6-6-3-9-1"
|
||||
sodipodi:nodetypes="ccc"
|
||||
inkscape:original-d="m 220.85563,815.47302 -6.91813,5.99535 -6.91787,-5.99535" /></g><ellipse
|
||||
style="fill:none;fill-opacity:1;stroke:#005c01;stroke-width:1.52416;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path30-7-7-4-1"
|
||||
cx="213.9375"
|
||||
cy="831.42084"
|
||||
rx="15"
|
||||
ry="25"
|
||||
transform="translate(-9.3315134e-6)" /></g></g></g><g
|
||||
id="g100-5"
|
||||
transform="matrix(1.771561,0,0,1.771561,-126.11368,-663.87619)"><path
|
||||
style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path89-4-9"
|
||||
sodipodi:arc-type="arc"
|
||||
sodipodi:type="arc"
|
||||
sodipodi:cx="170.66406"
|
||||
sodipodi:cy="-843.37482"
|
||||
sodipodi:rx="2"
|
||||
sodipodi:ry="2"
|
||||
sodipodi:start="0"
|
||||
sodipodi:end="3.1415927"
|
||||
d="m 172.66406,-843.37482 a 2,2 0 0 1 -1,1.73205 2,2 0 0 1 -2,0 2,2 0 0 1 -1,-1.73205"
|
||||
sodipodi:open="true"
|
||||
transform="scale(1,-1)" /><g
|
||||
id="g97-4"><circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path90-6"
|
||||
cx="169.00261"
|
||||
cy="839.49762"
|
||||
r="1" /><circle
|
||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path90-9-9"
|
||||
cx="172.32553"
|
||||
cy="839.49762"
|
||||
r="1" /></g></g></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 20 KiB |
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
<svg
|
||||
width="81.560997mm"
|
||||
height="79.98938mm"
|
||||
viewBox="0 0 81.560997 79.98938"
|
||||
height="79.989365mm"
|
||||
viewBox="0 0 81.560997 79.989365"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
|
|
@ -13,20 +13,21 @@
|
|||
id="defs1" /><g
|
||||
id="layer4"
|
||||
transform="translate(-50.121599,-596.91049)"><g
|
||||
id="g9"><rect
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="g9"
|
||||
style="stroke:#000000;stroke-opacity:1"><rect
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6"
|
||||
width="80"
|
||||
height="20"
|
||||
x="50.902103"
|
||||
y="613.53406" /><rect
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#0f07ff;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#3c34ff;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6-1"
|
||||
width="80"
|
||||
height="20"
|
||||
x="50.902103"
|
||||
y="640.27625" /></g><rect
|
||||
style="fill:#ff0707;fill-opacity:1;stroke:#c10000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
style="fill:#ff0707;fill-opacity:1;stroke:#000000;stroke-width:1.561;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="rect6-3"
|
||||
width="100"
|
||||
height="10"
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
|
|
@ -2,94 +2,26 @@
|
|||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="22.945572mm"
|
||||
height="28.24629mm"
|
||||
viewBox="0 0 22.945572 28.24629"
|
||||
width="22.945564mm"
|
||||
height="28.246271mm"
|
||||
viewBox="0 0 22.945564 28.246271"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
sodipodi:docname="icons.svg"
|
||||
xml:space="preserve"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="2.8284272"
|
||||
inkscape:cx="329.68853"
|
||||
inkscape:cy="1037.856"
|
||||
inkscape:window-width="1918"
|
||||
inkscape:window-height="1042"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="17"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer4"><inkscape:grid
|
||||
id="grid1"
|
||||
units="mm"
|
||||
originx="-84.294216"
|
||||
originy="-256.51758"
|
||||
spacingx="0.26458333"
|
||||
spacingy="0.26458334"
|
||||
empcolor="#0099e5"
|
||||
empopacity="0.30196078"
|
||||
color="#0099e5"
|
||||
opacity="0.14901961"
|
||||
empspacing="5"
|
||||
enabled="true"
|
||||
visible="false" /><inkscape:page
|
||||
x="0"
|
||||
y="0"
|
||||
width="22.945572"
|
||||
height="28.24629"
|
||||
id="page2"
|
||||
margin="0"
|
||||
bleed="0" /></sodipodi:namedview><defs
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
||||
id="defs1" /><g
|
||||
inkscape:groupmode="layer"
|
||||
id="layer4"
|
||||
inkscape:label="Layer 4"
|
||||
transform="translate(-84.29421,-256.51757)"><g
|
||||
id="g17"><path
|
||||
id="path13-2"
|
||||
transform="translate(-403.75417,-318.29373)"><g
|
||||
id="g28"><path
|
||||
id="path13-2-7"
|
||||
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665;stroke-linecap:butt;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m 102.26758,259.22827 -4.652431,14.9748 c 0,0 -3.853406,-2.83967 -6.441467,-7.23625 -0.281201,4.36457 0.45945,18.75999 11.093898,19.05465 10.63444,-0.29466 11.37665,-14.69008 11.09545,-19.05465 -2.58806,4.39658 -6.44097,7.23625 -6.44096,7.23625 z m -6.895188,20.4246 h 4.976438 a 2.48832,2.48832 0 0 1 -2.488217,2.48822 2.48832,2.48832 0 0 1 -2.488221,-2.48822 z m 8.815478,0 h 4.97645 a 2.48832,2.48832 0 0 1 -2.48822,2.48822 2.48832,2.48832 0 0 1 -2.48823,-2.48822 z"
|
||||
transform="translate(-6.5013311,-1.5902536)" /><path
|
||||
sodipodi:type="star"
|
||||
d="m 415.2262,319.4142 -4.65243,14.9748 c 0,0 -3.85341,-2.83967 -6.44147,-7.23625 -0.2812,4.36457 0.45945,18.75999 11.0939,19.05465 10.63444,-0.29466 11.37665,-14.69008 11.09545,-19.05465 -2.58806,4.39658 -6.44097,7.23625 -6.44096,7.23625 z m -6.89519,20.4246 h 4.97644 a 2.48832,2.48832 0 0 1 -2.48822,2.48822 2.48832,2.48832 0 0 1 -2.48822,-2.48822 z m 8.81548,0 h 4.97645 a 2.48832,2.48832 0 0 1 -2.48822,2.48822 2.48832,2.48832 0 0 1 -2.48823,-2.48822 z" /><path
|
||||
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665001;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path14"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="3"
|
||||
sodipodi:cx="63.610146"
|
||||
sodipodi:cy="259.77261"
|
||||
sodipodi:r1="2.4050174"
|
||||
sodipodi:r2="0.8495208"
|
||||
sodipodi:arg1="1.3352513"
|
||||
sodipodi:arg2="2.2426747"
|
||||
inkscape:rounded="0.2"
|
||||
inkscape:randomized="0"
|
||||
d="m 64.171412,262.11122 c -0.371671,0.14644 -0.815095,-1.38393 -1.090057,-1.67373 -0.307066,-0.32362 -1.843044,-0.90688 -1.777137,-1.34811 0.05902,-0.3951 1.606071,-0.0139 1.994519,-0.10715 0.433804,-0.10412 1.706909,-1.14268 2.05607,-0.86499 0.312654,0.24866 -0.790976,1.39786 -0.904463,1.78088 -0.126738,0.42774 0.136135,2.04957 -0.278932,2.2131 z"
|
||||
transform="translate(25.967913,5.8850319)" /><path
|
||||
sodipodi:type="star"
|
||||
id="path14-5"
|
||||
transform="translate(345.42786,67.661219)"
|
||||
d="m 64.171412,262.11122 c -0.371671,0.14644 -0.815095,-1.38393 -1.090057,-1.67373 -0.307066,-0.32362 -1.843044,-0.90688 -1.777137,-1.34811 0.05902,-0.3951 1.606071,-0.0139 1.994519,-0.10715 0.433804,-0.10412 1.706909,-1.14268 2.05607,-0.86499 0.312654,0.24866 -0.790976,1.39786 -0.904463,1.78088 -0.126738,0.42774 0.136135,2.04957 -0.278932,2.2131 z" /><path
|
||||
style="fill:#ff0707;fill-opacity:0.796663;stroke:#ffbb33;stroke-width:0.665001;stroke-dasharray:none;stroke-opacity:1"
|
||||
id="path15"
|
||||
inkscape:flatsided="false"
|
||||
sodipodi:sides="4"
|
||||
sodipodi:cx="73.712936"
|
||||
sodipodi:cy="258.36945"
|
||||
sodipodi:r1="1.9284658"
|
||||
sodipodi:r2="1.1014973"
|
||||
sodipodi:arg1="1.3258177"
|
||||
sodipodi:arg2="2.1112159"
|
||||
inkscape:rounded="0.2"
|
||||
inkscape:randomized="0"
|
||||
d="m 74.180658,260.24033 c -0.269428,0.0674 -0.796295,-0.78347 -1.034437,-0.92636 -0.238143,-0.14288 -1.236814,-0.20738 -1.304171,-0.4768 -0.06736,-0.26943 0.783475,-0.7963 0.92636,-1.03444 0.142886,-0.23814 0.207377,-1.23681 0.476805,-1.30417 0.269427,-0.0674 0.796295,0.78347 1.034437,0.92636 0.238143,0.14289 1.236814,0.20738 1.304171,0.4768 0.06736,0.26943 -0.783475,0.7963 -0.926361,1.03444 -0.142885,0.23814 -0.207376,1.23682 -0.476804,1.30417 z"
|
||||
transform="translate(28.887732,7.0275078)" /></g></g></svg>
|
||||
id="path15-9"
|
||||
transform="translate(348.34768,68.803695)"
|
||||
d="m 74.180658,260.24033 c -0.269428,0.0674 -0.796295,-0.78347 -1.034437,-0.92636 -0.238143,-0.14288 -1.236814,-0.20738 -1.304171,-0.4768 -0.06736,-0.26943 0.783475,-0.7963 0.92636,-1.03444 0.142886,-0.23814 0.207377,-1.23681 0.476805,-1.30417 0.269427,-0.0674 0.796295,0.78347 1.034437,0.92636 0.238143,0.14289 1.236814,0.20738 1.304171,0.4768 0.06736,0.26943 -0.783475,0.7963 -0.926361,1.03444 -0.142885,0.23814 -0.207376,1.23682 -0.476804,1.30417 z" /></g></g></svg>
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
|
@ -17,6 +17,10 @@ $offensive_color: color.adjust($village_color, $hue: 30deg);
|
|||
$offensive_border: color.change($offensive_color, $alpha: 1.0);
|
||||
$starts_as_villager_color: color.adjust($offensive_color, $hue: 30deg);
|
||||
$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);
|
||||
$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);
|
||||
$intel_border_faint: color.change($intel_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);
|
||||
$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);
|
||||
$intel_color_faint: color.change($intel_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() {
|
||||
display: -webkit-box;
|
||||
|
|
@ -280,7 +287,6 @@ nav.host-nav {
|
|||
align-self: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2rem;
|
||||
// background-color: hsl(283, 100%, 80%);
|
||||
border: 1px solid rgba(0, 255, 0, 0.7);
|
||||
background-color: black;
|
||||
color: rgba(0, 255, 0, 0.7);
|
||||
|
|
@ -298,6 +304,7 @@ nav.host-nav {
|
|||
border: 1px solid rgba(255, 0, 0, 1);
|
||||
color: rgba(255, 0, 0, 1);
|
||||
filter: none;
|
||||
background-color: rgba(255, 0, 0, 0.1);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(255, 0, 0, 0.3);
|
||||
|
|
@ -404,6 +411,7 @@ button {
|
|||
border: 3px solid rgba(0, 0, 0, 0.4);
|
||||
// min-width: 20%;
|
||||
flex-shrink: 1;
|
||||
flex-grow: 1;
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
|
|
@ -656,19 +664,22 @@ clients {
|
|||
justify-content: space-evenly;
|
||||
color: black;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
|
||||
gap: 10px;
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.role-reveal-card {
|
||||
justify-content: center;
|
||||
min-width: 5cm;
|
||||
// min-width: 5cm;
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-content: center;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 1cm;
|
||||
padding: 20px;
|
||||
border: 1px solid $wolves_color;
|
||||
background-color: color.change($wolves_color, $alpha: 0.1);
|
||||
min-width: 100px;
|
||||
|
|
@ -809,12 +820,9 @@ clients {
|
|||
}
|
||||
|
||||
.client-nav {
|
||||
// position: absolute;
|
||||
// left: 0;
|
||||
// top: 0;
|
||||
max-width: 100%;
|
||||
padding: 10px;
|
||||
// background-color: rgba(255, 107, 255, 0.2);
|
||||
height: 37px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: baseline;
|
||||
|
|
@ -946,6 +954,11 @@ input {
|
|||
font-size: 2rem;
|
||||
align-content: stretch;
|
||||
margin: 0;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
z-index: 3;
|
||||
background-color: #000;
|
||||
min-width: 2cm;
|
||||
|
||||
& * {
|
||||
margin: 0;
|
||||
|
|
@ -1110,15 +1123,20 @@ input {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
|
||||
&>.submenu {
|
||||
min-width: 30vw;
|
||||
width: 30vw;
|
||||
// position: absolute;
|
||||
|
||||
.assign-list {
|
||||
// min-width: 5cm;
|
||||
gap: 10px;
|
||||
|
||||
& .submenu button {
|
||||
width: 5cm;
|
||||
width: 10vw;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1126,6 +1144,7 @@ input {
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 5px;
|
||||
}
|
||||
}
|
||||
|
|
@ -1270,6 +1289,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 {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
|
@ -1385,6 +1442,23 @@ input {
|
|||
}
|
||||
}
|
||||
|
||||
li.change,
|
||||
li.choice {
|
||||
width: fit-content;
|
||||
|
||||
&:hover {
|
||||
.li-icon {
|
||||
filter: brightness(5000%);
|
||||
}
|
||||
|
||||
backdrop-filter: invert(15%);
|
||||
}
|
||||
}
|
||||
|
||||
.li-icon {
|
||||
filter: grayscale(100%) brightness(150%);
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
|
@ -1790,6 +1864,31 @@ input {
|
|||
flex-shrink: 1;
|
||||
}
|
||||
|
||||
.info-icon-grow {
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
img {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info-icon-list-grow {
|
||||
padding: 20px 0 20px 0;
|
||||
flex-grow: 1;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
|
||||
img {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.info-player-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
@ -1799,7 +1898,8 @@ input {
|
|||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
&.masons {
|
||||
&.masons,
|
||||
&.large {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
|
@ -1807,13 +1907,15 @@ input {
|
|||
.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 3fr 2fr;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.seer-check {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
zoom: 90%;
|
||||
height: 100%;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.role-title-span {
|
||||
|
|
@ -1860,6 +1962,7 @@ input {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.add-player {
|
||||
|
|
@ -1942,7 +2045,168 @@ input {
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
justify-content: space-around;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
height: var(--information-height);
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
|
||||
&:first-child {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.guardian-select {
|
||||
max-height: 100vh;
|
||||
}
|
||||
|
||||
.story-text {
|
||||
font-size: 1.7em;
|
||||
}
|
||||
|
||||
|
||||
.dialog {
|
||||
z-index: 5;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.dialog-box {
|
||||
border: 1px solid white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
padding-left: 30px;
|
||||
padding-right: 30px;
|
||||
padding-top: 10px;
|
||||
padding-bottom: 10px;
|
||||
background-color: black;
|
||||
|
||||
.options {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
gap: 20px;
|
||||
|
||||
&>button {
|
||||
min-width: 4cm;
|
||||
font-size: 1em;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: underline dotted;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.build-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
|
||||
label {
|
||||
color: hsl(280, 65%, 43%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
overflow: hidden;
|
||||
max-height: 1cm;
|
||||
justify-content: center;
|
||||
background-color: #000;
|
||||
z-index: 3;
|
||||
padding: 10px 0 10px 0;
|
||||
border-top: 1px solid white;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
}
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
//
|
||||
// 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::sync::atomic::{AtomicBool, AtomicI64, AtomicPtr, AtomicU64, AtomicUsize, Ordering};
|
||||
use core::sync::atomic::{AtomicBool, AtomicI64, Ordering};
|
||||
use std::rc::Rc;
|
||||
|
||||
use chrono::{DateTime, TimeDelta, Utc};
|
||||
|
|
@ -30,7 +30,7 @@ use crate::{
|
|||
clients::client::connection::{Connection2, ConnectionError},
|
||||
components::{
|
||||
Button, CoverOfDarkness, Identity, Story,
|
||||
client::{ClientNav, Signin},
|
||||
client::{ClientFooter, ClientNav, Signin},
|
||||
},
|
||||
storage::StorageKey,
|
||||
};
|
||||
|
|
@ -251,8 +251,9 @@ pub fn Client2(ClientProps { auto_join }: &ClientProps) -> Html {
|
|||
|
||||
html! {
|
||||
<>
|
||||
{nav}
|
||||
{content}
|
||||
{nav}
|
||||
{content}
|
||||
<ClientFooter />
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ use yew::{html::Scope, prelude::*};
|
|||
use crate::{
|
||||
callback,
|
||||
components::{
|
||||
Button, CoverOfDarkness, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story, Victory,
|
||||
Button, Lobby, LobbyPlayerAction, RoleReveal, Settings, Story, Victory,
|
||||
action::{ActionResultView, Prompt},
|
||||
host::{DaytimePlayerList, Setup},
|
||||
},
|
||||
|
|
@ -224,6 +224,7 @@ pub enum HostState {
|
|||
Result(Option<CharacterIdentity>, ActionResult),
|
||||
Story {
|
||||
story: GameStory,
|
||||
#[allow(unused)]
|
||||
page: usize,
|
||||
},
|
||||
}
|
||||
|
|
@ -311,7 +312,7 @@ impl Component for Host {
|
|||
fn view(&self, _ctx: &Context<Self>) -> Html {
|
||||
log::trace!("state: {:?}", self.state);
|
||||
let content = match self.state.clone() {
|
||||
HostState::Story { story, page } => {
|
||||
HostState::Story { story, .. } => {
|
||||
if let Some(outcome) = story
|
||||
.final_village()
|
||||
.ok()
|
||||
|
|
@ -611,8 +612,7 @@ impl Component for Host {
|
|||
}
|
||||
},
|
||||
HostEvent::Error(err) => {
|
||||
self.error_callback
|
||||
.emit(Some(WerewolfError::GameError(err)));
|
||||
self.error_callback.emit(Some(WerewolfError::Game(err)));
|
||||
false
|
||||
}
|
||||
HostEvent::SetBigScreenState(state) => {
|
||||
|
|
|
|||
|
|
@ -16,8 +16,8 @@ use core::num::NonZeroU8;
|
|||
|
||||
use werewolves_proto::{
|
||||
character::{Character, CharacterId},
|
||||
diedto::{DiedTo, DiedToTitle},
|
||||
game::{self, Game, GameOver, GameSettings, OrRandom, SetupRole, Village, story::GameStory},
|
||||
diedto::DiedToTitle,
|
||||
game::{self, Game, GameOver, GameSettings, OrRandom, SetupRole, story::GameStory},
|
||||
message::{
|
||||
CharacterState, Identification, PublicIdentity,
|
||||
host::{HostDayMessage, HostGameMessage, HostNightMessage, ServerToHostMessage},
|
||||
|
|
@ -121,7 +121,7 @@ pub fn test_story() -> GameStory {
|
|||
settings.add_and_assign(hunter.0, hunter.1);
|
||||
settings.add_and_assign(diseased.0, diseased.1);
|
||||
settings.fill_remaining_slots_with_villagers(players.len());
|
||||
|
||||
#[allow(unused)]
|
||||
let (
|
||||
werewolf,
|
||||
dire_wolf,
|
||||
|
|
@ -596,6 +596,7 @@ impl ActionPromptTitleExt for ActionPromptTitle {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait ActionResultExt {
|
||||
fn sleep(&self);
|
||||
fn r#continue(&self);
|
||||
|
|
@ -655,7 +656,7 @@ impl ActionResultExt for ActionResult {
|
|||
|
||||
fn seer(&self) -> Alignment {
|
||||
match self {
|
||||
ActionResult::Seer(a) => a.clone(),
|
||||
ActionResult::Seer(a) => *a,
|
||||
_ => panic!("expected a seer result"),
|
||||
}
|
||||
}
|
||||
|
|
@ -674,7 +675,7 @@ impl ActionResultExt for ActionResult {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait AlignmentExt {
|
||||
fn village(&self);
|
||||
fn wolves(&self);
|
||||
|
|
@ -689,7 +690,7 @@ impl AlignmentExt for Alignment {
|
|||
assert_eq!(*self, Alignment::Wolves)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait ServerToHostMessageExt {
|
||||
fn prompt(self) -> ActionPrompt;
|
||||
fn result(self) -> ActionResult;
|
||||
|
|
@ -725,6 +726,7 @@ impl ServerToHostMessageExt for ServerToHostMessage {
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub trait GameExt {
|
||||
fn villager_character_ids(&self) -> Box<[CharacterId]>;
|
||||
fn character_by_player_id(&self, player_id: PlayerId) -> Character;
|
||||
|
|
@ -822,7 +824,8 @@ impl GameExt for Game {
|
|||
fn mark_and_check(&mut self, mark: CharacterId) {
|
||||
let prompt = self.mark(mark);
|
||||
match prompt {
|
||||
ActionPrompt::Insomniac { .. }
|
||||
ActionPrompt::TraitorIntro { .. }
|
||||
| ActionPrompt::Insomniac { .. }
|
||||
| ActionPrompt::MasonsWake { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::CoverOfDarkness
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ use werewolves_proto::{
|
|||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
night::{ActionPrompt, ActionResponse},
|
||||
},
|
||||
role::PreviousGuardianAction,
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
|
|
@ -31,7 +30,7 @@ use crate::{
|
|||
Button, CoverOfDarkness, Identity,
|
||||
action::{BinaryChoice, TargetPicker, WolvesIntro},
|
||||
},
|
||||
pages::MasonsWake,
|
||||
pages::TraitorIntroPage,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
|
|
@ -66,9 +65,16 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
if let Some(page) = props.pages.get(props.page_idx).map(|page| {
|
||||
let next = props.big_screen.not().then(|| {
|
||||
let send = props.on_complete.clone();
|
||||
let page_idx = props.page_idx;
|
||||
let pages_total = props.pages.len();
|
||||
let prompt = props.prompt.clone();
|
||||
let on_page_next = Callback::from(move |_| {
|
||||
send.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::NextPage,
|
||||
if page_idx + 1 >= pages_total && !prompt.interactive() {
|
||||
HostNightMessage::ActionResponse(ActionResponse::ContinueToResult)
|
||||
} else {
|
||||
HostNightMessage::NextPage
|
||||
},
|
||||
)))
|
||||
});
|
||||
html! {
|
||||
|
|
@ -113,6 +119,15 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
}
|
||||
});
|
||||
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 => {
|
||||
return html! {
|
||||
<CoverOfDarkness next={continue_callback}/>
|
||||
|
|
@ -143,41 +158,29 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
ActionPrompt::RoleChange { .. }
|
||||
| ActionPrompt::ElderReveal { .. }
|
||||
| ActionPrompt::Insomniac { .. } => {
|
||||
if let Some(cb) = continue_callback {
|
||||
cb.emit(());
|
||||
if !props.big_screen {
|
||||
props
|
||||
.on_complete
|
||||
.emit(HostMessage::InGame(HostGameMessage::Night(
|
||||
HostNightMessage::ActionResponse(ActionResponse::ContinueToResult),
|
||||
)));
|
||||
}
|
||||
return html! {};
|
||||
return html! {
|
||||
<CoverOfDarkness message={"oops".to_string()} />
|
||||
};
|
||||
}
|
||||
|
||||
ActionPrompt::Guardian {
|
||||
character_id,
|
||||
previous,
|
||||
living_players,
|
||||
marked,
|
||||
..
|
||||
} => {
|
||||
let last_protect = previous.as_ref().map(|prev| match prev {
|
||||
PreviousGuardianAction::Protect(target) => {
|
||||
html! {
|
||||
<>
|
||||
<b>{"last night you protected: "}</b>
|
||||
<Identity ident={Into::<PublicIdentity>::into(target)}/>
|
||||
</>
|
||||
}
|
||||
}
|
||||
PreviousGuardianAction::Guard(target) => html! {
|
||||
<>
|
||||
<b>{"last night you guarded: "}</b>
|
||||
<Identity ident={Into::<PublicIdentity>::into(target)}/>
|
||||
</>
|
||||
},
|
||||
});
|
||||
let marked = marked.iter().cloned().collect::<Box<[CharacterId]>>();
|
||||
|
||||
return html! {
|
||||
<div>
|
||||
<div class="guardian-select">
|
||||
{identity_html(props, Some(character_id))}
|
||||
<h2>{"guardian"}</h2>
|
||||
{last_protect}
|
||||
<TargetPicker
|
||||
targets={living_players.clone()}
|
||||
marked={marked}
|
||||
|
|
@ -403,24 +406,26 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
|||
)));
|
||||
}
|
||||
});
|
||||
let choice = props.big_screen.not().then_some(html! {
|
||||
<BinaryChoice on_chosen={on_select} />
|
||||
});
|
||||
return html! {
|
||||
<div class="role-page">
|
||||
{identity_html(props, Some(character_id))}
|
||||
<h1 class="wolves">{"SHAPESHIFTER"}</h1>
|
||||
<div class="information wolves faint">
|
||||
<h3>
|
||||
<h2>
|
||||
{"WOULD YOU LIKE TO USE YOUR "}
|
||||
<span class="yellow">{"ONCE PER GAME"}</span>
|
||||
{" SHAPESHIFT ABILITY?"}
|
||||
</h3>
|
||||
<h4>
|
||||
</h2>
|
||||
<h2>
|
||||
<span class="yellow">{"YOU WILL DIE"}</span>{", AND THE "}
|
||||
{"TARGET OF THE WOLFPACK KILL"}
|
||||
{" SHALL INSTEAD BECOME A WOLF"}
|
||||
</h4>
|
||||
</h2>
|
||||
</div>
|
||||
<BinaryChoice on_chosen={on_select}>
|
||||
</BinaryChoice>
|
||||
{choice}
|
||||
</div>
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,6 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use core::ops::Not;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use werewolves_proto::message::{
|
||||
PublicIdentity,
|
||||
host::{HostGameMessage, HostMessage, HostNightMessage},
|
||||
|
|
@ -23,10 +22,11 @@ use werewolves_proto::message::{
|
|||
use yew::prelude::*;
|
||||
|
||||
use crate::{
|
||||
components::{Button, CoverOfDarkness, Icon, IconSource, Identity},
|
||||
components::{Button, CoverOfDarkness, Identity},
|
||||
pages::{
|
||||
AdjudicatorResult, ArcanistResult, BeholderSawNothing, EmpathResult, GravediggerResultPage,
|
||||
InsomniacResult, MorticianResultPage, PowerSeerResult, RoleblockPage, SeerResult,
|
||||
AdjudicatorResult, ArcanistResult, BeholderSawEverything, BeholderSawNothing, DrunkPage,
|
||||
EmpathResult, GravediggerResultPage, InsomniacResult, MorticianResultPage, PowerSeerResult,
|
||||
RoleblockPage, SeerResult, ShiftFailed,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -58,6 +58,15 @@ pub fn ActionResultView(props: &ActionResultProps) -> Html {
|
|||
.not()
|
||||
.then(|| html! {<Button on_click={on_continue}>{"continue"}</Button>});
|
||||
let body = match &props.result {
|
||||
ActionResult::ShiftFailed => html! {
|
||||
<ShiftFailed />
|
||||
},
|
||||
ActionResult::Drunk => html! {
|
||||
<DrunkPage />
|
||||
},
|
||||
ActionResult::BeholderSawEverything => html! {
|
||||
<BeholderSawEverything />
|
||||
},
|
||||
ActionResult::BeholderSawNothing => html! {
|
||||
<BeholderSawNothing />
|
||||
},
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
//
|
||||
// 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::{
|
||||
message::{CharacterIdentity, PublicIdentity},
|
||||
role::RoleTitle,
|
||||
|
|
@ -42,7 +43,7 @@ pub fn WolvesIntro(props: &WolvesIntroProps) -> Html {
|
|||
{
|
||||
props.wolves.iter().map(|w| html!{
|
||||
<div class="character wolves faint">
|
||||
<p class="role">{w.1.to_string()}</p>
|
||||
<p class="role yellow">{w.1.to_string().to_case(Case::Title)}</p>
|
||||
<Identity ident={Into::<PublicIdentity>::into(&w.0)} />
|
||||
</div>
|
||||
}).collect::<Html>()
|
||||
|
|
|
|||
|
|
@ -13,14 +13,10 @@
|
|||
// 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::{
|
||||
diedto::{DiedTo, DiedToTitle},
|
||||
game::{Category, SetupRoleTitle},
|
||||
role,
|
||||
};
|
||||
use werewolves_proto::{diedto::DiedToTitle, game::SetupRoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||
pub struct DiedToSpanProps {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
//
|
||||
// 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 werewolves_proto::role::{self, Killer};
|
||||
use werewolves_proto::role::Killer;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{AssociatedIcon, Icon, IconType};
|
||||
|
|
|
|||
|
|
@ -12,28 +12,39 @@
|
|||
//
|
||||
// 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 werewolves_proto::aura;
|
||||
use werewolves_proto::aura::{self, Aura, AuraTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconType, PartialAssociatedIcon};
|
||||
use crate::{
|
||||
class::Class,
|
||||
components::{Icon, IconType, PartialAssociatedIcon},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct AuraProps {
|
||||
pub aura: aura::Aura,
|
||||
impl Class for AuraTitle {
|
||||
fn class(&self) -> Option<&'static str> {
|
||||
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> {
|
||||
Some(match aura {
|
||||
aura::Aura::Traitor => "traitor",
|
||||
aura::Aura::Drunk => "drunk",
|
||||
aura::Aura::Insane => "insane",
|
||||
aura::Aura::Bloodlet { .. } => "wolves",
|
||||
})
|
||||
impl Class for Aura {
|
||||
fn class(&self) -> Option<&'static str> {
|
||||
self.title().class()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct AuraSpanProps {
|
||||
pub aura: aura::AuraTitle,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Aura(AuraProps { aura }: &AuraProps) -> Html {
|
||||
let class = aura_class(aura);
|
||||
pub fn AuraSpan(AuraSpanProps { aura }: &AuraSpanProps) -> Html {
|
||||
let class = aura.class();
|
||||
let icon = aura.icon().map(|icon| {
|
||||
html! {
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
// 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 yew::prelude::*;
|
||||
|
||||
use crate::components::{Dialog, WithConfirmation};
|
||||
|
||||
const SOURCE_CODE_URL: &str = "https://sectorinf.com/emilis/werewolves";
|
||||
|
||||
#[function_component]
|
||||
pub fn ClientFooter() -> Html {
|
||||
let about_dialog_state = use_state(|| false);
|
||||
let about_dialog = about_dialog_state.then(|| {
|
||||
let cancel_signout = {
|
||||
let dialog = about_dialog_state.clone();
|
||||
Callback::from(move |_| {
|
||||
dialog.set(false);
|
||||
})
|
||||
};
|
||||
let callback = Callback::from(move |_| ());
|
||||
let options: Box<[String]> = Box::new([]);
|
||||
html! {
|
||||
<Dialog
|
||||
options={options}
|
||||
cancel_callback={Some(cancel_signout)}
|
||||
callback={callback}
|
||||
>
|
||||
<About />
|
||||
</Dialog>
|
||||
}
|
||||
});
|
||||
let about_click = {
|
||||
let dialog_set = about_dialog_state.setter();
|
||||
move |_| {
|
||||
dialog_set.set(true);
|
||||
}
|
||||
};
|
||||
|
||||
html! {
|
||||
<nav class="footer">
|
||||
<button class="default-button solid" onclick={about_click}>{"about"}</button>
|
||||
{about_dialog}
|
||||
</nav>
|
||||
}
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn About() -> Html {
|
||||
let confirm_state = use_state(|| false);
|
||||
let source_code_confirm = {
|
||||
let confirm_callback = {
|
||||
move |_| {
|
||||
let _ = gloo::utils::window().location().set_href(SOURCE_CODE_URL);
|
||||
}
|
||||
};
|
||||
let message = html! {
|
||||
<>
|
||||
<h1>{"this will take you away from the game"}</h1>
|
||||
<h3>{"make sure this isn't an oopsie"}</h3>
|
||||
</>
|
||||
};
|
||||
html! {
|
||||
<WithConfirmation
|
||||
state={confirm_state}
|
||||
confirm_callback={confirm_callback}
|
||||
message={message}
|
||||
>
|
||||
{"source code"}
|
||||
</WithConfirmation>
|
||||
}
|
||||
};
|
||||
let dirty = crate::BUILD_DIRTY.then_some(html! {
|
||||
<>
|
||||
{" "}
|
||||
<span class="dirty">{"(dirty)"}</span>
|
||||
</>
|
||||
});
|
||||
html! {
|
||||
<div class="about">
|
||||
<h1>{"werewolves"}</h1>
|
||||
<div class="build-info">
|
||||
<p class="build-id">
|
||||
<label>{"build: "}</label>
|
||||
<a href={format!("{SOURCE_CODE_URL}/commit/{}", crate::BUILD_ID_LONG)}>
|
||||
{crate::BUILD_ID}
|
||||
{dirty}
|
||||
</a>
|
||||
</p>
|
||||
<p class="build-time">
|
||||
<label>{"built at: "}</label>
|
||||
<span class="time">{crate::BUILD_TIME}</span>
|
||||
</p>
|
||||
</div>
|
||||
<nav class="links">
|
||||
{source_code_confirm}
|
||||
</nav>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -53,6 +53,9 @@ pub fn ClientNav(
|
|||
}
|
||||
});
|
||||
|
||||
let number_open = use_state(|| false);
|
||||
let name_open = use_state(|| false);
|
||||
let pronouns_open = use_state(|| false);
|
||||
let number = {
|
||||
let current_value = use_state(String::new);
|
||||
let message_callback = message_callback.clone();
|
||||
|
|
@ -63,8 +66,7 @@ pub fn ClientNav(
|
|||
.number
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_else(|| String::from("???"));
|
||||
let open = use_state(|| false);
|
||||
let open_set = open.setter();
|
||||
let open_set = number_open.setter();
|
||||
let on_submit = {
|
||||
let val = current_value.clone();
|
||||
Callback::from(move |_| {
|
||||
|
|
@ -85,19 +87,27 @@ pub fn ClientNav(
|
|||
open_set.set(false);
|
||||
})
|
||||
};
|
||||
let close_others = {
|
||||
let name_open = name_open.clone();
|
||||
let pronouns_open = pronouns_open.clone();
|
||||
Callback::from(move |_| {
|
||||
name_open.set(false);
|
||||
pronouns_open.set(false);
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ClickableNumberEdit
|
||||
value={current_value.clone()}
|
||||
field_name="number"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
state={number_open.clone()}
|
||||
on_open={close_others}
|
||||
>
|
||||
<div class="number">{current_num}</div>
|
||||
</ClickableNumberEdit>
|
||||
}
|
||||
};
|
||||
let name = {
|
||||
let open = use_state(|| false);
|
||||
let name = use_state(String::new);
|
||||
let on_submit = {
|
||||
let ident = identity.clone();
|
||||
|
|
@ -115,20 +125,29 @@ pub fn ClientNav(
|
|||
})
|
||||
})
|
||||
};
|
||||
let close_others = {
|
||||
let number_open = number_open.clone();
|
||||
let pronouns_open = pronouns_open.clone();
|
||||
Callback::from(move |_| {
|
||||
number_open.set(false);
|
||||
pronouns_open.set(false);
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ClickableTextEdit
|
||||
value={name.clone()}
|
||||
submit_ident={identity.clone()}
|
||||
field_name="pronouns"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
state={name_open.clone()}
|
||||
on_open={close_others}
|
||||
>
|
||||
<div class="name">{identity.1.name.as_str()}</div>
|
||||
</ClickableTextEdit>
|
||||
}
|
||||
};
|
||||
let pronouns = {
|
||||
let pronouns_state = use_state(String::new);
|
||||
let pronuns_state = use_state(String::new);
|
||||
|
||||
let on_submit = {
|
||||
let ident = identity.clone();
|
||||
|
|
@ -145,14 +164,22 @@ pub fn ClientNav(
|
|||
})
|
||||
})
|
||||
};
|
||||
let open = use_state(|| false);
|
||||
let close_others = {
|
||||
let number_open = number_open.clone();
|
||||
let name_open = name_open.clone();
|
||||
Callback::from(move |_| {
|
||||
number_open.set(false);
|
||||
name_open.set(false);
|
||||
})
|
||||
};
|
||||
html! {
|
||||
<ClickableTextEdit
|
||||
value={pronouns_state}
|
||||
value={pronuns_state}
|
||||
submit_ident={identity.clone()}
|
||||
field_name="pronouns"
|
||||
on_submit={on_submit}
|
||||
state={open}
|
||||
state={pronouns_open}
|
||||
on_open={close_others}
|
||||
>
|
||||
{pronouns}
|
||||
</ClickableTextEdit>
|
||||
|
|
@ -199,6 +226,8 @@ struct ClickableTextEditProps {
|
|||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or(100)]
|
||||
pub max_length: usize,
|
||||
#[prop_or_default]
|
||||
pub on_open: Option<Callback<()>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -211,6 +240,7 @@ fn ClickableTextEdit(
|
|||
on_submit,
|
||||
state,
|
||||
max_length,
|
||||
on_open,
|
||||
}: &ClickableTextEditProps,
|
||||
) -> Html {
|
||||
let on_input = crate::components::input_element_string_oninput(value.setter(), *max_length);
|
||||
|
|
@ -237,7 +267,7 @@ fn ClickableTextEdit(
|
|||
</div>
|
||||
};
|
||||
html! {
|
||||
<ClickableField options={options} state={state.clone()}>
|
||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||
{children.clone()}
|
||||
</ClickableField>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,139 @@
|
|||
// 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 web_sys::Element;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::Button;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct DialogProps {
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub options: Box<[String]>,
|
||||
#[prop_or_default]
|
||||
pub cancel_callback: Option<Callback<()>>,
|
||||
pub callback: Callback<String>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn Dialog(
|
||||
DialogProps {
|
||||
children,
|
||||
options,
|
||||
cancel_callback,
|
||||
callback,
|
||||
}: &DialogProps,
|
||||
) -> Html {
|
||||
let options = options
|
||||
.iter()
|
||||
.map(|opt| {
|
||||
let callback = callback.clone();
|
||||
let option = opt.clone();
|
||||
let cb = Callback::from(move |_| {
|
||||
callback.emit(option.clone());
|
||||
});
|
||||
html! {
|
||||
<Button on_click={cb}>{opt.clone()}</Button>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
let backdrop_click = cancel_callback.clone().map(|cancel_callback| {
|
||||
Callback::from(move |ev: MouseEvent| {
|
||||
if let Some(div) = ev.target_dyn_into::<Element>()
|
||||
&& div.class_name() == "dialog"
|
||||
{
|
||||
ev.stop_propagation();
|
||||
cancel_callback.emit(());
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
html! {
|
||||
<div class="click-backdrop" onclick={backdrop_click}>
|
||||
<div class="dialog">
|
||||
<div class="dialog-box">
|
||||
<div class="message">
|
||||
{children.clone()}
|
||||
</div>
|
||||
<div class="options">
|
||||
{options}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct WithConfirmationProps {
|
||||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or_default]
|
||||
pub children: Html,
|
||||
pub confirm_callback: Callback<()>,
|
||||
pub message: Html,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
pub fn WithConfirmation(
|
||||
WithConfirmationProps {
|
||||
state,
|
||||
children,
|
||||
confirm_callback,
|
||||
message,
|
||||
}: &WithConfirmationProps,
|
||||
) -> Html {
|
||||
let about_dialog_state = state.clone();
|
||||
let confirmation_dialog = about_dialog_state.then(|| {
|
||||
let cancel_signout = {
|
||||
let dialog = about_dialog_state.clone();
|
||||
Callback::from(move |_| {
|
||||
dialog.set(false);
|
||||
})
|
||||
};
|
||||
let confirm_callback = confirm_callback.clone();
|
||||
let callback = {
|
||||
let dialog = about_dialog_state.clone();
|
||||
Callback::from(move |opt: String| {
|
||||
if opt == "ok" {
|
||||
confirm_callback.emit(());
|
||||
} else {
|
||||
dialog.set(false);
|
||||
}
|
||||
})
|
||||
};
|
||||
let options: Box<[String]> = Box::new([String::from("ok"), String::from("take me back")]);
|
||||
html! {
|
||||
<Dialog
|
||||
options={options}
|
||||
cancel_callback={Some(cancel_signout)}
|
||||
callback={callback}
|
||||
>
|
||||
{message.clone()}
|
||||
</Dialog>
|
||||
}
|
||||
});
|
||||
let confirmation_click = {
|
||||
let dialog_set = about_dialog_state.setter();
|
||||
move |_| {
|
||||
dialog_set.set(true);
|
||||
}
|
||||
};
|
||||
html! {
|
||||
<>
|
||||
<Button on_click={confirmation_click}>{children.clone()}</Button>
|
||||
{confirmation_dialog}
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,8 @@ pub struct ClickableFieldProps {
|
|||
#[prop_or_default]
|
||||
pub with_backdrop_exit: bool,
|
||||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or_default]
|
||||
pub on_open: Option<Callback<()>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -39,11 +41,20 @@ pub fn ClickableField(
|
|||
button_class,
|
||||
with_backdrop_exit,
|
||||
state,
|
||||
on_open,
|
||||
}: &ClickableFieldProps,
|
||||
) -> Html {
|
||||
let open = state.clone();
|
||||
let on_click_open = open.clone();
|
||||
let open_close = Callback::from(move |_| on_click_open.set(!(*on_click_open)));
|
||||
let open_close = {
|
||||
let open = open.clone();
|
||||
let on_open = on_open.clone();
|
||||
Callback::from(move |_| {
|
||||
if !*open && let Some(on_open) = on_open.as_ref() {
|
||||
on_open.emit(());
|
||||
}
|
||||
open.set(!(*open));
|
||||
})
|
||||
};
|
||||
let submenu_open_close = open_close.clone();
|
||||
let submenu = open.clone().then(|| {
|
||||
let backdrop = with_backdrop_exit.then(|| {
|
||||
|
|
@ -79,6 +90,8 @@ pub struct ClickableNumberEditProps {
|
|||
pub on_submit: Callback<()>,
|
||||
pub field_name: &'static str,
|
||||
pub state: UseStateHandle<bool>,
|
||||
#[prop_or_default]
|
||||
pub on_open: Option<Callback<()>>,
|
||||
}
|
||||
|
||||
#[function_component]
|
||||
|
|
@ -89,6 +102,7 @@ pub fn ClickableNumberEdit(
|
|||
field_name,
|
||||
on_submit,
|
||||
state,
|
||||
on_open,
|
||||
}: &ClickableNumberEditProps,
|
||||
) -> Html {
|
||||
let on_input = crate::components::input_element_string_oninput(value.setter(), 20);
|
||||
|
|
@ -101,7 +115,7 @@ pub fn ClickableNumberEdit(
|
|||
</div>
|
||||
};
|
||||
html! {
|
||||
<ClickableField options={options} state={state.clone()}>
|
||||
<ClickableField options={options} state={state.clone()} on_open={on_open.clone()}>
|
||||
{children.clone()}
|
||||
</ClickableField>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,7 +24,6 @@ use yew::prelude::*;
|
|||
|
||||
use crate::components::{
|
||||
AssociatedIcon, Button, Icon, IconType, Identity, PartialAssociatedIcon,
|
||||
attributes::RoleTitleSpan,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
|
|
|
|||
|
|
@ -16,11 +16,7 @@ use core::ops::Not;
|
|||
use std::collections::HashMap;
|
||||
|
||||
use convert_case::{Case, Casing};
|
||||
use rand::Rng;
|
||||
use werewolves_proto::{
|
||||
game::{Category, GameSettings, SetupRole, SetupRoleTitle},
|
||||
role::Alignment,
|
||||
};
|
||||
use werewolves_proto::game::{Category, GameSettings, SetupRole, SetupRoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
||||
|
|
@ -151,16 +147,13 @@ pub fn SetupCategory(
|
|||
</div>
|
||||
<div class="attributes">
|
||||
<div class="alignment">
|
||||
// <img class="icon" src={alignment} alt={"alignment"}/>
|
||||
<Icon source={alignment} icon_type={IconType::Small}/>
|
||||
</div>
|
||||
<div class={classes!("killer", killer_inactive)}>
|
||||
<Icon source={IconSource::Killer} icon_type={IconType::Small}/>
|
||||
// <img class="icon" src="/img/killer.svg" alt="killer icon"/>
|
||||
</div>
|
||||
<div class={classes!("poweful", powerful_inactive)}>
|
||||
<Icon source={IconSource::Powerful} icon_type={IconType::Small}/>
|
||||
// <img class="icon" src="/img/powerful.svg" alt="powerful icon"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@
|
|||
// 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 werewolves_proto::{
|
||||
aura::Aura,
|
||||
aura::AuraTitle,
|
||||
diedto::DiedToTitle,
|
||||
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||
};
|
||||
|
|
@ -79,6 +79,8 @@ decl_icon!(
|
|||
RedX: "/img/red-x.svg",
|
||||
Traitor: "/img/traitor.svg",
|
||||
Bloodlet: "/img/bloodlet.svg",
|
||||
Drunk: "/img/drunk.svg",
|
||||
Insane: "/img/insane.svg",
|
||||
);
|
||||
|
||||
impl IconSource {
|
||||
|
|
@ -95,7 +97,6 @@ impl IconSource {
|
|||
pub enum IconType {
|
||||
List,
|
||||
Small,
|
||||
RoleAdd,
|
||||
Fit,
|
||||
Icon15Pct,
|
||||
Informational,
|
||||
|
|
@ -110,7 +111,6 @@ impl IconType {
|
|||
IconType::Fit => "icon-fit",
|
||||
IconType::List => "icon-in-list",
|
||||
IconType::Small => "icon",
|
||||
IconType::RoleAdd => "icon-role-add",
|
||||
IconType::Informational => "icon-info",
|
||||
IconType::RoleCheck => "check-icon",
|
||||
}
|
||||
|
|
@ -124,6 +124,8 @@ pub struct IconProps {
|
|||
pub inactive: bool,
|
||||
#[prop_or_default]
|
||||
pub icon_type: IconType,
|
||||
#[prop_or_default]
|
||||
pub classes: Classes,
|
||||
}
|
||||
#[function_component]
|
||||
pub fn Icon(
|
||||
|
|
@ -131,12 +133,13 @@ pub fn Icon(
|
|||
source,
|
||||
inactive,
|
||||
icon_type,
|
||||
classes,
|
||||
}: &IconProps,
|
||||
) -> Html {
|
||||
html! {
|
||||
<img
|
||||
src={source.source()}
|
||||
class={classes!(source.class(), icon_type.class(), inactive.then_some("inactive"))}
|
||||
class={classes!(source.class(), icon_type.class(), inactive.then_some("inactive"), classes.clone())}
|
||||
/>
|
||||
}
|
||||
}
|
||||
|
|
@ -227,13 +230,13 @@ impl PartialAssociatedIcon for DiedToTitle {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialAssociatedIcon for Aura {
|
||||
impl PartialAssociatedIcon for AuraTitle {
|
||||
fn icon(&self) -> Option<IconSource> {
|
||||
match self {
|
||||
Aura::Traitor => Some(IconSource::Traitor),
|
||||
Aura::Drunk => todo!(),
|
||||
Aura::Insane => todo!(),
|
||||
Aura::Bloodlet { .. } => Some(IconSource::Bloodlet),
|
||||
AuraTitle::Traitor => Some(IconSource::Traitor),
|
||||
AuraTitle::Drunk => Some(IconSource::Drunk),
|
||||
AuraTitle::Insane => Some(IconSource::Insane),
|
||||
AuraTitle::Bloodlet => Some(IconSource::Bloodlet),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ use std::rc::Rc;
|
|||
|
||||
use convert_case::{Case, Casing};
|
||||
use werewolves_proto::{
|
||||
aura::AuraTitle,
|
||||
error::GameError,
|
||||
game::{GameSettings, OrRandom, SetupRole, SetupSlot, SlotId},
|
||||
message::{Identification, PlayerState, PublicIdentity},
|
||||
|
|
@ -24,9 +25,11 @@ use werewolves_proto::{
|
|||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
Button, ClickableField, Icon, IconSource, IconType, Identity, PartialAssociatedIcon,
|
||||
client::Signin,
|
||||
use crate::{
|
||||
class::Class,
|
||||
components::{
|
||||
Button, ClickableField, Icon, IconType, Identity, PartialAssociatedIcon, client::Signin,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Debug, PartialEq, Properties)]
|
||||
|
|
@ -346,6 +349,7 @@ pub fn SettingsSlot(
|
|||
}: &SettingsSlotProps,
|
||||
) -> Html {
|
||||
let open = use_state(|| false);
|
||||
let aura_open = use_state(|| false);
|
||||
let open_update = open.setter();
|
||||
let update = update.clone();
|
||||
let update = Callback::from(move |act| {
|
||||
|
|
@ -361,9 +365,15 @@ pub fn SettingsSlot(
|
|||
on_kick_update.emit(SettingSlotAction::Remove(slot_id));
|
||||
open_update.set(false);
|
||||
});
|
||||
let assign_to = assign_to_submenu(players_for_assign, slot, &update, &open.setter());
|
||||
let options =
|
||||
setup_options_for_slot(slot, &update, roles_in_setup, apprentice_open, open.clone());
|
||||
let assign_to = assign_to_submenu(players_for_assign, slot, &update, &assign_open.setter());
|
||||
let options = setup_options_for_slot(
|
||||
slot,
|
||||
&update,
|
||||
roles_in_setup,
|
||||
apprentice_open,
|
||||
open.clone(),
|
||||
aura_open.clone(),
|
||||
);
|
||||
let assign_text = slot
|
||||
.assign_to
|
||||
.as_ref()
|
||||
|
|
@ -379,7 +389,7 @@ pub fn SettingsSlot(
|
|||
.unwrap_or_else(|| html! {{"assign"}});
|
||||
html! {
|
||||
<>
|
||||
<Button on_click={on_kick}>
|
||||
<Button on_click={on_kick} classes={classes!("red")}>
|
||||
{"remove"}
|
||||
</Button>
|
||||
<ClickableField
|
||||
|
|
@ -447,7 +457,71 @@ fn setup_options_for_slot(
|
|||
roles_in_setup: &[RoleTitle],
|
||||
open_apprentice_assign: UseStateHandle<bool>,
|
||||
slot_field_open: UseStateHandle<bool>,
|
||||
open_aura_assign: UseStateHandle<bool>,
|
||||
) -> 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 {
|
||||
SetupRole::MasonLeader { recruits_available } => {
|
||||
let next = {
|
||||
|
|
@ -660,5 +734,10 @@ fn setup_options_for_slot(
|
|||
_ => 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 werewolves_proto::{
|
||||
character::{Character, CharacterId},
|
||||
game::{
|
||||
aura::AuraTitle, character::{Character, CharacterId}, game::{
|
||||
GameTime, SetupRole,
|
||||
night::changes::NightChange,
|
||||
story::{
|
||||
DayDetail, GameActions, GameStory, NightChoice, StoryActionPrompt, StoryActionResult,
|
||||
},
|
||||
},
|
||||
role::Alignment,
|
||||
}, role::Alignment
|
||||
};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon,
|
||||
attributes::{
|
||||
AuraSpan, CharacterCard, Icon, IconSource, IconType, PartialAssociatedIcon, attributes::{
|
||||
AlignmentComparisonSpan, AlignmentSpan, CategorySpan, DiedToSpan, KillerSpan, PowerfulSpan,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
|
|
@ -80,9 +77,7 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
|||
.collect::<HashMap<_, _>>()));
|
||||
let changes = match changes {
|
||||
GameActions::DayDetails(day_changes) => {
|
||||
let execute_list = if day_changes.is_empty() {
|
||||
html! {<span>{"no one was executed"}</span>}
|
||||
} else {
|
||||
let execute_list =
|
||||
day_changes
|
||||
.iter()
|
||||
.map(|c| match c {
|
||||
|
|
@ -91,15 +86,12 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
|||
.filter_map(|c| story.starting_village.character_by_id(c).ok())
|
||||
.map(|c| {
|
||||
html! {
|
||||
// <span>
|
||||
<CharacterCard faint=true char={c.clone()}/>
|
||||
// </span>
|
||||
}
|
||||
})
|
||||
.collect::<Html>()
|
||||
};
|
||||
.collect::<Html>();
|
||||
|
||||
Some(html! {
|
||||
day_changes.is_empty().not().then_some(html! {
|
||||
<div class="day">
|
||||
<h3>{"village executed"}</h3>
|
||||
<div class="executed">
|
||||
|
|
@ -120,16 +112,19 @@ pub fn Story(StoryProps { story }: &StoryProps) -> Html {
|
|||
.collect::<Html>();
|
||||
|
||||
let changes = details
|
||||
.changes
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<li>
|
||||
<StoryNightChange change={c.clone()} characters={characters.clone()}/>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
.changes
|
||||
.iter()
|
||||
.map(|c| {
|
||||
html! {
|
||||
<li class="change">
|
||||
<StoryNightChange
|
||||
change={c.clone()}
|
||||
characters={characters.clone()}
|
||||
/>
|
||||
</li>
|
||||
}
|
||||
})
|
||||
.collect::<Html>();
|
||||
|
||||
html! {
|
||||
<div class="night">
|
||||
|
|
@ -181,7 +176,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
<>
|
||||
<CharacterCard faint=true char={character.clone()}/>
|
||||
{"lost the"}
|
||||
<crate::components::Aura aura={*aura}/>
|
||||
<AuraSpan aura={aura.title()}/>
|
||||
{"aura"}
|
||||
</>
|
||||
}).unwrap_or_default(),
|
||||
|
|
@ -189,9 +184,9 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
html!{
|
||||
<>
|
||||
<CharacterCard faint=true char={target.clone()}/>
|
||||
{"gained the"}
|
||||
<crate::components::Aura aura={*aura}/>
|
||||
{"aura from"}
|
||||
<span class="story-text">{"gained the"}</span>
|
||||
<AuraSpan aura={aura.title()}/>
|
||||
<span class="story-text">{"aura from"}</span>
|
||||
<CharacterCard faint=true char={source.clone()}/>
|
||||
</>
|
||||
}
|
||||
|
|
@ -205,26 +200,29 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={char.clone()}/>
|
||||
{"is now"}
|
||||
<span class="story-text">{"is now"}</span>
|
||||
<CharacterCard faint=true char={new_char.clone()}/>
|
||||
</>
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
|
||||
NightChange::Kill { target, died_to } => characters
|
||||
NightChange::Kill { target, died_to } => {
|
||||
|
||||
characters
|
||||
.get(target)
|
||||
.map(|target| {
|
||||
html! {
|
||||
<>
|
||||
<Icon source={IconSource::Skull} icon_type={IconType::Small}/>
|
||||
<CharacterCard faint=true char={target.clone()}/>
|
||||
{"died to"}
|
||||
<span class="story-text">{"died to"}</span>
|
||||
<DiedToSpan died_to={died_to.title()}/>
|
||||
</>
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_default()
|
||||
},
|
||||
NightChange::RoleBlock { source, target, .. } => characters
|
||||
.get(source)
|
||||
.and_then(|s| characters.get(target).map(|t| (s, t)))
|
||||
|
|
@ -232,7 +230,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={source.clone()}/>
|
||||
{"role blocked"}
|
||||
<span class="story-text">{"role blocked"}</span>
|
||||
<CharacterCard faint=true char={target.clone()}/>
|
||||
</>
|
||||
}
|
||||
|
|
@ -245,7 +243,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={source.clone()}/>
|
||||
{"shapeshifted into"}
|
||||
<span class="story-text">{"shapeshifted into"}</span>
|
||||
<CharacterCard faint=true char={into.clone()}/>
|
||||
</>
|
||||
}
|
||||
|
|
@ -258,7 +256,7 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={elder.clone()}/>
|
||||
{"learned they are the Elder"}
|
||||
<span class="story-text">{"learned they are the Elder"}</span>
|
||||
</>
|
||||
}
|
||||
})
|
||||
|
|
@ -270,15 +268,15 @@ fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightCha
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={empath.clone()}/>
|
||||
{"found the scapegoat in"}
|
||||
<span class="story-text">{"found the scapegoat in"}</span>
|
||||
<CharacterCard faint=true char={scapegoat.clone()}/>
|
||||
{"and took on their curse"}
|
||||
<span class="story-text">{"and took on their curse"}</span>
|
||||
</>
|
||||
}
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
|
||||
NightChange::HunterTarget { .. }
|
||||
NightChange::HunterTarget { .. }
|
||||
| NightChange::MasonRecruit { .. }
|
||||
| NightChange::Protection { .. } => html! {}, // sorted in prompt side
|
||||
}
|
||||
|
|
@ -293,16 +291,29 @@ struct StoryNightResultProps {
|
|||
#[function_component]
|
||||
fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightResultProps) -> Html {
|
||||
match result {
|
||||
StoryActionResult::ShiftFailed => html!{
|
||||
<span class="story-text">{"but it failed"}</span>
|
||||
},
|
||||
StoryActionResult::Drunk => html! {
|
||||
<>
|
||||
<span class="story-text">{"but got "}</span>
|
||||
<AuraSpan aura={AuraTitle::Drunk}/>
|
||||
<span class="story-text">{" instead"}</span>
|
||||
</>
|
||||
},
|
||||
StoryActionResult::BeholderSawEverything => html!{
|
||||
<span class="story-text">{"and saw everything 👁️"}</span>
|
||||
},
|
||||
StoryActionResult::BeholderSawNothing => html!{
|
||||
<span>{"but saw nothing"}</span>
|
||||
<span class="story-text">{"but saw nothing"}</span>
|
||||
},
|
||||
StoryActionResult::RoleBlocked => html! {
|
||||
<span>{"but was role blocked"}</span>
|
||||
<span class="story-text">{"but was role blocked"}</span>
|
||||
},
|
||||
StoryActionResult::Seer(alignment) => {
|
||||
html! {
|
||||
<span>
|
||||
<span>{"and saw"}</span>
|
||||
<span class="story-text">{"and saw"}</span>
|
||||
<AlignmentSpan alignment={*alignment}/>
|
||||
</span>
|
||||
}
|
||||
|
|
@ -310,25 +321,25 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes
|
|||
StoryActionResult::PowerSeer { powerful } => {
|
||||
html! {
|
||||
<span>
|
||||
<span>{"and discovered they are"}</span>
|
||||
<span class="story-text">{"and discovered they are"}</span>
|
||||
<PowerfulSpan powerful={*powerful}/>
|
||||
</span>
|
||||
}
|
||||
}
|
||||
StoryActionResult::Adjudicator { killer } => html! {
|
||||
<span>
|
||||
<span>{"and saw"}</span>
|
||||
<span class="story-text">{"and saw"}</span>
|
||||
<KillerSpan killer={*killer}/>
|
||||
</span>
|
||||
},
|
||||
StoryActionResult::Arcanist(same) => html! {
|
||||
<span>
|
||||
<span>{"and saw"}</span>
|
||||
<span class="story-text">{"and saw"}</span>
|
||||
<AlignmentComparisonSpan comparison={*same}/>
|
||||
</span>
|
||||
},
|
||||
StoryActionResult::GraveDigger(None) => html! {
|
||||
<span>
|
||||
<span class="story-text">
|
||||
{"found an empty grave"}
|
||||
</span>
|
||||
},
|
||||
|
|
@ -336,7 +347,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes
|
|||
let category = Into::<SetupRole>::into(*role_title).category();
|
||||
html! {
|
||||
<span>
|
||||
<span>{"found the body of a"}</span>
|
||||
<span class="story-text">{"found the body of a"}</span>
|
||||
<CategorySpan category={category} icon={role_title.icon()}>
|
||||
{role_title.to_string().to_case(Case::Title)}
|
||||
</CategorySpan>
|
||||
|
|
@ -345,7 +356,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes
|
|||
}
|
||||
StoryActionResult::Mortician(died_to_title) => html! {
|
||||
<>
|
||||
{"and found the cause of death to be"}
|
||||
<span class="story-text">{"and found the cause of death to be"}</span>
|
||||
<DiedToSpan died_to={*died_to_title}/>
|
||||
</>
|
||||
},
|
||||
|
|
@ -366,7 +377,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes
|
|||
|
||||
StoryActionResult::Empath { scapegoat: false } => html! {
|
||||
<>
|
||||
{"and saw that they are"}
|
||||
<span class="story-text">{"and saw that they are"}</span>
|
||||
<span class="attribute-span faint">
|
||||
<div class="inactive">
|
||||
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||
|
|
@ -377,7 +388,7 @@ fn StoryNightResult(StoryNightResultProps { result, characters }: &StoryNightRes
|
|||
},
|
||||
StoryActionResult::Empath { scapegoat: true } => html! {
|
||||
<>
|
||||
{"and saw that they are"}
|
||||
<span class="story-text">{"and saw that they are"}</span>
|
||||
<span class="attribute-span faint wolves">
|
||||
<div>
|
||||
<Icon source={IconSource::Heart} icon_type={IconType::Small}/>
|
||||
|
|
@ -396,7 +407,7 @@ struct StoryNightChoiceProps {
|
|||
}
|
||||
|
||||
#[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| {
|
||||
characters
|
||||
.get(character_id)
|
||||
|
|
@ -405,7 +416,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={char.clone()}/>
|
||||
<span>{action}</span>
|
||||
<span class="story-text">{action}</span>
|
||||
<CharacterCard faint=true char={chosen.clone()}/>
|
||||
</>
|
||||
}
|
||||
|
|
@ -427,9 +438,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={arcanist.clone()}/>
|
||||
<span>{"compared"}</span>
|
||||
<span class="story-text">{"compared"}</span>
|
||||
<CharacterCard faint=true char={chosen1.clone()}/>
|
||||
<span>{"and"}</span>
|
||||
<span class="story-text">{"and"}</span>
|
||||
<CharacterCard faint=true char={chosen2.clone()}/>
|
||||
</>
|
||||
}
|
||||
|
|
@ -447,9 +458,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={leader.clone()}/>
|
||||
<span>{"'s masons"}</span>
|
||||
<span class="story-text">{"'s masons"}</span>
|
||||
{masons}
|
||||
<span>{"convened in secret"}</span>
|
||||
<span class="story-text">{"convened in secret"}</span>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
|
|
@ -500,9 +511,9 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={char.clone()}/>
|
||||
<span>{"invited"}</span>
|
||||
<span class="story-text">{"invited"}</span>
|
||||
<CharacterCard faint=true char={chosen.clone()}/>
|
||||
<span>{"for dinner"}</span>
|
||||
<span class="story-text">{"for dinner"}</span>
|
||||
</>
|
||||
}
|
||||
}),
|
||||
|
|
@ -537,18 +548,21 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
html! {
|
||||
<>
|
||||
<AlignmentSpan alignment={Alignment::Wolves}/>
|
||||
<span>{"attempted a kill on"}</span>
|
||||
<span class="story-text">{"attempted a kill on"}</span>
|
||||
<CharacterCard faint=true char={chosen.clone()} />
|
||||
</>
|
||||
}
|
||||
})
|
||||
}
|
||||
StoryActionPrompt::Shapeshifter { character_id } => {
|
||||
if choice.result.is_none() {
|
||||
return html!{};
|
||||
}
|
||||
characters.get(character_id).map(|shifter| {
|
||||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={shifter.clone()} />
|
||||
<span>{"decided to shapeshift into the wolf kill target"}</span>
|
||||
<span class="story-text">{"decided to shapeshift into the wolf kill target"}</span>
|
||||
</>
|
||||
}
|
||||
})
|
||||
|
|
@ -570,7 +584,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
html! {
|
||||
<>
|
||||
<CharacterCard faint=true char={insomniac.clone()} />
|
||||
<span>{"witnessed visits from"}</span>
|
||||
<span class="story-text">{"witnessed visits from"}</span>
|
||||
</>
|
||||
}
|
||||
})
|
||||
|
|
@ -584,8 +598,12 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
|||
choice_body
|
||||
.map(|choice_body| {
|
||||
html! {
|
||||
<li>
|
||||
<Icon source={IconSource::ListItem} icon_type={IconType::Small}/>
|
||||
<li class="choice">
|
||||
<Icon
|
||||
source={IconSource::ListItem}
|
||||
icon_type={IconType::Small}
|
||||
classes={classes!("li-icon")}
|
||||
/>
|
||||
{choice_body}
|
||||
{result}
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@
|
|||
// 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/>.
|
||||
mod assets;
|
||||
mod class;
|
||||
mod clients;
|
||||
mod storage;
|
||||
mod components {
|
||||
|
|
@ -39,6 +40,11 @@ use pages::{ErrorComponent, WerewolfError};
|
|||
use web_sys::Url;
|
||||
use yew::{context::ContextProviderProps, prelude::*};
|
||||
|
||||
const BUILD_ID: &str = werewolves_macros::build_id!();
|
||||
const BUILD_ID_LONG: &str = werewolves_macros::build_id_long!();
|
||||
const BUILD_DIRTY: bool = werewolves_macros::build_dirty!();
|
||||
const BUILD_TIME: &str = werewolves_macros::build_time!();
|
||||
|
||||
use crate::clients::{
|
||||
client::{Client2, ClientContext},
|
||||
host::{Host, HostEvent},
|
||||
|
|
@ -46,6 +52,7 @@ use crate::clients::{
|
|||
|
||||
fn main() {
|
||||
wasm_logger::init(wasm_logger::Config::new(log::Level::Trace));
|
||||
log::debug!("starting werewolves build {BUILD_ID}");
|
||||
let document = gloo::utils::document();
|
||||
let url = document.document_uri().expect("get uri");
|
||||
let url_obj = Url::new(&url).unwrap();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,21 @@
|
|||
use werewolves_proto::aura::AuraTitle;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, 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>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={icon}/>
|
||||
</div>
|
||||
<h3 class="yellow">{"YOUR NIGHT ACTION DID NOT TAKE PLACE"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
|
@ -22,22 +22,20 @@ use crate::clients::client::connection::ConnectionError;
|
|||
#[derive(Debug, Error)]
|
||||
pub enum WerewolfError {
|
||||
#[error("{0}")]
|
||||
GameError(#[from] GameError),
|
||||
Game(#[from] GameError),
|
||||
#[error("local storage error: {0}")]
|
||||
LocalStorageError(String),
|
||||
#[error("invalid target")]
|
||||
InvalidTarget,
|
||||
LocalStorage(String),
|
||||
#[error("send error: {0}")]
|
||||
SendError(#[from] futures::channel::mpsc::SendError),
|
||||
Send(#[from] futures::channel::mpsc::SendError),
|
||||
#[error("send error: {0}")]
|
||||
ClientSendError(#[from] yew::platform::pinned::mpsc::SendError<ClientMessage>),
|
||||
ClientSend(#[from] yew::platform::pinned::mpsc::SendError<ClientMessage>),
|
||||
#[error("connection error: {0}")]
|
||||
ConnectionError(#[from] ConnectionError),
|
||||
Connection(#[from] ConnectionError),
|
||||
}
|
||||
|
||||
impl From<StorageError> for WerewolfError {
|
||||
fn from(storage_error: StorageError) -> Self {
|
||||
Self::LocalStorageError(storage_error.to_string())
|
||||
Self::LocalStorage(storage_error.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,13 +13,10 @@
|
|||
// 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 werewolves_proto::{game::SetupRole, role::RoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
use crate::components::{Icon, PartialAssociatedIcon};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||
pub struct RoleChangePageProps {
|
||||
|
|
@ -31,19 +28,21 @@ pub fn RoleChangePage(RoleChangePageProps { role }: &RoleChangePageProps) -> Htm
|
|||
let class = Into::<SetupRole>::into(*role).category().class();
|
||||
let icon = role.icon().map(|icon| {
|
||||
html! {
|
||||
<h4 class="icons">
|
||||
<Icon source={icon} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={icon}/>
|
||||
</div>
|
||||
}
|
||||
});
|
||||
html! {
|
||||
<div class="role-page">
|
||||
<h1 class="intel">{"ROLE CHANGE"}</h1>
|
||||
<h1 class={classes!(class)}>{"ROLE CHANGE"}</h1>
|
||||
<div class={classes!("information", class, "faint")}>
|
||||
<h2>{"YOUR ROLE HAS CHANGED"}</h2>
|
||||
{icon}
|
||||
<h3 class="yellow">{"YOUR NEW ROLE IS"}</h3>
|
||||
<h3>{role.to_string().to_case(Case::Upper)}</h3>
|
||||
<h3>
|
||||
<span>{"YOUR NEW ROLE IS "}</span>
|
||||
<span class="yellow">{role.to_string().to_case(Case::Upper)}</span>
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,10 @@
|
|||
//
|
||||
// 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::ops::Not;
|
||||
|
||||
use werewolves_proto::role::Killer;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn AdjudicatorPage1() -> Html {
|
||||
|
|
@ -26,10 +24,9 @@ pub fn AdjudicatorPage1() -> Html {
|
|||
<h1 class="defensive">{"ADJUDICATOR"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::Killer} icon_type={IconType::Informational}/>
|
||||
<Icon source={IconSource::Killer} icon_type={IconType::Informational} inactive={true}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Killer}/>
|
||||
</div>
|
||||
<h3 class="yellow">{"YOU WILL CHECK IF THEY APPEAR AS A KILLER"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,13 +48,11 @@ pub fn AdjudicatorResult(AdjudicatorResultProps { killer }: &AdjudicatorResultPr
|
|||
Killer::Killer => html! {
|
||||
<Icon
|
||||
source={IconSource::Killer}
|
||||
icon_type={IconType::Informational}
|
||||
/>
|
||||
},
|
||||
Killer::NotKiller => html! {
|
||||
<Icon
|
||||
source={IconSource::RedX}
|
||||
icon_type={IconType::Informational}
|
||||
/>
|
||||
},
|
||||
};
|
||||
|
|
@ -66,7 +61,7 @@ pub fn AdjudicatorResult(AdjudicatorResultProps { killer }: &AdjudicatorResultPr
|
|||
<h1 class="defensive">{"ADJUDICATOR"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"YOUR TARGET"}</h2>
|
||||
<h4>{icon}</h4>
|
||||
<div class="info-icon-grow">{icon}</div>
|
||||
<h3 class="yellow">{text}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ pub fn AlphaWolfPage1() -> Html {
|
|||
<span class="yellow">{"ONCE PER GAME"}</span>
|
||||
{" KILL ABILITY"}
|
||||
</h2>
|
||||
<h3 class="yellow">
|
||||
<h2>
|
||||
{"POINT AT YOUR TARGET "}
|
||||
{"OR GO BACK TO SLEEP"}
|
||||
</h3>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,22 +49,12 @@ pub fn ArcanistResult(ArcanistResultProps { alignment_eq }: &ArcanistResultProps
|
|||
};
|
||||
let icons = match alignment_eq {
|
||||
AlignmentEq::Same => html! {
|
||||
<>
|
||||
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
|
||||
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
|
||||
// <span>{"OR"}</span>
|
||||
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
|
||||
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
|
||||
<Icon source={IconSource::Equal} icon_type={IconType::Informational} />
|
||||
</>
|
||||
<Icon source={IconSource::Equal} />
|
||||
},
|
||||
AlignmentEq::Different => html! {
|
||||
<>
|
||||
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
|
||||
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
|
||||
// {"OR"}
|
||||
// <Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
|
||||
// <Icon source={IconSource::Village} icon_type={IconType::Informational}/>
|
||||
<Icon source={IconSource::Village} />
|
||||
<Icon source={IconSource::Wolves} />
|
||||
</>
|
||||
},
|
||||
};
|
||||
|
|
@ -73,9 +63,9 @@ pub fn ArcanistResult(ArcanistResultProps { alignment_eq }: &ArcanistResultProps
|
|||
<h1 class="intel">{"ARCANIST"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOUR TARGETS APPEAR AS"}</h2>
|
||||
<h4 class="icons">
|
||||
<div class="info-icon-list-grow">
|
||||
{icons}
|
||||
</h4>
|
||||
</div>
|
||||
<h1 class="yellow">{text}</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -43,3 +43,24 @@ pub fn BeholderSawNothing() -> Html {
|
|||
</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>
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,8 @@ pub fn BloodletterPage1() -> Html {
|
|||
<div class="information wolves faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h3>{"THEY WILL BE COVERED IN WOLF BLOOD"}</h3>
|
||||
<h4>{"AND"}</h4>
|
||||
<h2 class="yellow inline-icons">
|
||||
{"APPEAR AS A WOLF "}
|
||||
{"AND APPEAR AS A WOLF "}
|
||||
<Icon source={IconSource::Wolves} icon_type={IconType::Fit}/>
|
||||
{" KILLER "}
|
||||
<Icon source={IconSource::Killer} icon_type={IconType::Fit}/>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,10 @@
|
|||
//
|
||||
// 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::ops::Not;
|
||||
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn EmpathPage1() -> Html {
|
||||
|
|
@ -26,7 +25,7 @@ pub fn EmpathPage1() -> Html {
|
|||
<div class="information intel faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h3 class="yellow">{"YOU WILL CHECK IF THEY ARE THE SCAPEGOAT"}</h3>
|
||||
<h3>{"AND IF THEY ARE, TAKE ON THEIR CURSE"}</h3>
|
||||
<h3>{"IF THEY ARE, YOU WILL TAKE ON THEIR CURSE"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
@ -43,18 +42,26 @@ pub fn EmpathResult(EmpathResultProps { scapegoat }: &EmpathResultProps) -> Html
|
|||
true => "THE SCAPEGOAT",
|
||||
false => "NOT THE SCAPEGOAT",
|
||||
};
|
||||
let icon = match scapegoat {
|
||||
true => html! {
|
||||
<Icon
|
||||
source={IconSource::Scapegoat}
|
||||
/>
|
||||
},
|
||||
false => html! {
|
||||
<Icon
|
||||
source={IconSource::RedX}
|
||||
/>
|
||||
},
|
||||
};
|
||||
html! {
|
||||
<div class="role-page">
|
||||
<h1 class="intel">{"EMPATH"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOUR TARGET IS"}</h2>
|
||||
<h4>
|
||||
<Icon
|
||||
source={IconSource::Scapegoat}
|
||||
icon_type={IconType::Informational}
|
||||
inactive={scapegoat.not()}
|
||||
/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
{icon}
|
||||
</div>
|
||||
<h3 class="yellow">{text}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ use convert_case::{Case, Casing};
|
|||
use werewolves_proto::role::RoleTitle;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType, PartialAssociatedIcon};
|
||||
use crate::components::{Icon, IconSource, PartialAssociatedIcon};
|
||||
|
||||
#[function_component]
|
||||
pub fn GravediggerPage1() -> Html {
|
||||
|
|
@ -29,8 +29,8 @@ pub fn GravediggerPage1() -> Html {
|
|||
<span class="yellow">{"DEAD"}</span>
|
||||
{" PLAYER"}
|
||||
</h2>
|
||||
<div class="icons">
|
||||
<Icon source={IconSource::Gravedigger} icon_type={IconType::Informational}/>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Gravedigger}/>
|
||||
</div>
|
||||
<h3 class="yellow">
|
||||
{"YOU WILL LEARN THEIR ROLE"}
|
||||
|
|
@ -54,7 +54,7 @@ pub fn GravediggerResultPage(
|
|||
.map(|r| {
|
||||
html! {
|
||||
<h4>
|
||||
{"YOU DIG UP THE BODY OF A "}
|
||||
{"AND FIND A "}
|
||||
<span class="yellow">
|
||||
{r.to_string().to_case(Case::Upper)}
|
||||
</span>
|
||||
|
|
@ -75,12 +75,9 @@ pub fn GravediggerResultPage(
|
|||
<h1 class="intel">{"GRAVEDIGGER"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOU CHECK THE ROLE OF YOUR TARGET"}</h2>
|
||||
<h4>
|
||||
<Icon
|
||||
source={icon}
|
||||
icon_type={IconType::Informational}
|
||||
/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={icon} />
|
||||
</div>
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use werewolves_proto::message::CharacterIdentity;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{CharacterTargetCard, Icon, IconSource, IconType};
|
||||
use crate::components::{CharacterTargetCard, Icon, IconSource};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||
pub struct GuardianPageProps {
|
||||
|
|
@ -29,9 +29,9 @@ pub fn GuardianPageNoPrevProtect() -> Html {
|
|||
<h1 class="defensive">{"GUARDIAN"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::ShieldAndSword} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::ShieldAndSword}/>
|
||||
</div>
|
||||
<h3 class="yellow">{"CHOOSE SOMEONE TO PROTECT FROM DEATH"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -90,12 +90,12 @@ pub fn GuardianPagePreviousGuard(GuardianPageProps { previous }: &GuardianPagePr
|
|||
<h1 class="defensive">{"GUARDIAN"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"LAST TIME YOU GUARDED"}</h2>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::ShieldAndSword} />
|
||||
</div>
|
||||
<div class="info-player-list">
|
||||
<CharacterTargetCard ident={previous.clone()} />
|
||||
</div>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::ShieldAndSword} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<h3 class="yellow">{"YOU CANNOT PROTECT THEM TONIGHT"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn HunterPage1() -> Html {
|
||||
|
|
@ -23,12 +23,12 @@ pub fn HunterPage1() -> Html {
|
|||
<h1 class="offensive">{"HUNTER"}</h1>
|
||||
<div class="information offensive faint">
|
||||
<h2>{"SET A HUNTER'S TRAP ON A PLAYER"}</h2>
|
||||
<div class="icons">
|
||||
<Icon source={IconSource::Hunter} icon_type={IconType::Informational}/>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Hunter}/>
|
||||
</div>
|
||||
<h3 class="yellow">
|
||||
{"IF YOU DIE TONIGHT, OR ARE EXECUTED TOMORROW"}
|
||||
{"THIS PLAYER WILL DIE AT NIGHT AS WELL"}
|
||||
{"IF YOU DIE TONIGHT, OR ARE EXECUTED TOMORROW "}
|
||||
{"THIS PLAYER WILL DIE AT NIGHT"}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@
|
|||
use werewolves_proto::message::night::Visits;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::CharacterTargetCard;
|
||||
use crate::components::{CharacterTargetCard, Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn InsomniacPage1() -> Html {
|
||||
|
|
@ -23,9 +23,12 @@ pub fn InsomniacPage1() -> Html {
|
|||
<div class="role-page">
|
||||
<h1 class="intel">{"INSOMNIAC"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOUR POOR SLEEP RESULTS IN BEING WOKEN BY VISITORS IN THE NIGHT"}</h2>
|
||||
<h2>{"YOUR SLEEP IS INTERRUPTED"}</h2>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Insomniac}/>
|
||||
</div>
|
||||
<h3 class="yellow">
|
||||
{"YOU WILL REMEMBER WHO VISITED YOU TONIGHT"}
|
||||
{"THE FOLLOWING PEOPLE VISITED YOU TONIGHT"}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -52,7 +55,7 @@ pub fn InsomniacResult(InsomniacResultProps { visits }: &InsomniacResultProps) -
|
|||
<h1 class="intel">{"INSOMNIAC"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOU WERE VISITED IN THE NIGHT BY:"}</h2>
|
||||
<div class="info-player-list">
|
||||
<div class="info-player-list large">
|
||||
{visitors}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -25,10 +25,10 @@ pub struct MapleWolfPage1Props {
|
|||
pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) -> Html {
|
||||
let starving = starving
|
||||
.then_some(html! {
|
||||
<>
|
||||
<div>
|
||||
<h3 class="red">{"YOU ARE STARVING"}</h3>
|
||||
<h3>{"IF YOU FAIL TO EAT TONIGHT, YOU WILL DIE"}</h3>
|
||||
</>
|
||||
</div>
|
||||
})
|
||||
.unwrap_or_else(|| {
|
||||
html! {
|
||||
|
|
@ -45,8 +45,8 @@ pub fn MapleWolfPage1(MapleWolfPage1Props { starving }: &MapleWolfPage1Props) ->
|
|||
<h2>
|
||||
{"YOU CAN CHOOSE TO EAT A PLAYER TONIGHT"}
|
||||
</h2>
|
||||
<div class="icons">
|
||||
<Icon source={IconSource::MapleWolf} icon_type={IconType::Informational}/>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::MapleWolf} icon_type={IconType::Fit}/>
|
||||
</div>
|
||||
{starving}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ pub fn MasonRecruitPage1(
|
|||
<span class="yellow">{"EVERY NIGHT"}</span>
|
||||
{", AS LONG AS THEY ARE ALIVE AND REMAIN VILLAGE ALIGNED"}
|
||||
</h4>
|
||||
<h3 class="yellow">{"WOULD YOU LIKE TO RECRUIT TONIGHT?"}</h3>
|
||||
<h2 class="yellow">{"WOULD YOU LIKE TO RECRUIT TONIGHT?"}</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn MilitiaPage1() -> Html {
|
||||
|
|
@ -27,8 +27,8 @@ pub fn MilitiaPage1() -> Html {
|
|||
<span class="yellow">{"ONCE PER GAME"}</span>
|
||||
{" KILL ABILITY"}
|
||||
</h2>
|
||||
<div class="icons">
|
||||
<Icon source={IconSource::Sword} icon_type={IconType::Informational}/>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Sword}/>
|
||||
</div>
|
||||
<h3 class="yellow">
|
||||
{"POINT AT YOUR TARGET "}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,11 @@
|
|||
//
|
||||
// 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::ops::Not;
|
||||
|
||||
use werewolves_proto::role::Powerful;
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn PowerSeerPage1() -> Html {
|
||||
|
|
@ -26,10 +25,9 @@ pub fn PowerSeerPage1() -> Html {
|
|||
<h1 class="intel">{"POWER SEER"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::Powerful} icon_type={IconType::Informational}/>
|
||||
<Icon source={IconSource::Powerful} icon_type={IconType::Informational} inactive={true}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Powerful} />
|
||||
</div>
|
||||
<h3 class="yellow">{"YOU WILL CHECK IF THEY ARE POWERFUL"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -51,13 +49,13 @@ pub fn PowerSeerResult(PowerSeerResultProps { powerful }: &PowerSeerResultProps)
|
|||
Powerful::Powerful => html! {
|
||||
<Icon
|
||||
source={IconSource::Powerful}
|
||||
icon_type={IconType::Informational}
|
||||
// icon_type={IconType::Informational}
|
||||
/>
|
||||
},
|
||||
Powerful::NotPowerful => html! {
|
||||
<Icon
|
||||
source={IconSource::RedX}
|
||||
icon_type={IconType::Informational}
|
||||
// icon_type={IconType::Informational}
|
||||
/>
|
||||
},
|
||||
};
|
||||
|
|
@ -66,7 +64,7 @@ pub fn PowerSeerResult(PowerSeerResultProps { powerful }: &PowerSeerResultProps)
|
|||
<h1 class="intel">{"POWER SEER"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"YOUR TARGET APPEARS AS"}</h2>
|
||||
<h4>{icon}</h4>
|
||||
<div class="info-icon-grow">{icon}</div>
|
||||
<h3 class="yellow">{text}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn ProtectorPage1() -> Html {
|
||||
|
|
@ -23,9 +23,9 @@ pub fn ProtectorPage1() -> Html {
|
|||
<h1 class="defensive">{"PROTECTOR"}</h1>
|
||||
<div class="information defensive faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::Shield} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Shield} />
|
||||
</div>
|
||||
<h3 class="yellow">{"YOU WILL PROTECT THEM FROM A DEATH TONIGHT"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn PyremasterPage1() -> Html {
|
||||
|
|
@ -22,12 +22,14 @@ pub fn PyremasterPage1() -> Html {
|
|||
<div class="role-page">
|
||||
<h1 class="offensive">{"PYREMASTER"}</h1>
|
||||
<div class="information offensive faint">
|
||||
<h2>{"IF YOU WISH TO THROW A PLAYER ON THE PYRE"}</h2>
|
||||
<div class="icons">
|
||||
<Icon source={IconSource::Pyremaster} icon_type={IconType::Informational}/>
|
||||
<h3>{"YOU CAN CHOOSE TO THROW A PLAYER ON THE PYRE"}</h3>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Pyremaster}/>
|
||||
</div>
|
||||
<h3 class="yellow">
|
||||
{"IF YOU KILL TWO GOOD VILLAGERS LIKE THIS "}
|
||||
<h3>
|
||||
{"IF YOU KILL "}
|
||||
<span class="yellow">{"TWO"}</span>
|
||||
{" GOOD VILLAGERS LIKE THIS "}
|
||||
{"YOU WILL DIE AS WELL"}
|
||||
</h3>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -15,9 +15,7 @@
|
|||
use werewolves_proto::role::{Alignment, RoleTitle};
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{
|
||||
AssociatedIcon, Icon, IconSource, IconType, PartialAssociatedIcon, attributes::RoleTitleSpan,
|
||||
};
|
||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType, attributes::RoleTitleSpan};
|
||||
|
||||
#[function_component]
|
||||
pub fn SeerPage1() -> Html {
|
||||
|
|
@ -26,10 +24,10 @@ pub fn SeerPage1() -> Html {
|
|||
<h1 class="intel">{"SEER"}</h1>
|
||||
<div class="information intel faint">
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::Village} icon_type={IconType::Informational}/>
|
||||
<Icon source={IconSource::Wolves} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<div class="info-icon-list-grow">
|
||||
<Icon source={IconSource::Village} />
|
||||
<Icon source={IconSource::Wolves} />
|
||||
</div>
|
||||
<h3 class="yellow">{"YOU WILL CHECK IF THEY APPEAR AS A VILLAGER OR PART OF THE WOLFPACK"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn VindicatorPage1() -> Html {
|
||||
|
|
@ -24,9 +24,9 @@ pub fn VindicatorPage1() -> Html {
|
|||
<div class="information defensive faint">
|
||||
<h3>{"A VILLAGER WAS EXECUTED"}</h3>
|
||||
<h2>{"PICK A PLAYER"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::Shield} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Vindicator} />
|
||||
</div>
|
||||
<h3 class="yellow">{"YOU WILL PROTECT THEM FROM A DEATH TONIGHT"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource, IconType};
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[function_component]
|
||||
pub fn RoleblockPage() -> Html {
|
||||
|
|
@ -23,9 +23,9 @@ pub fn RoleblockPage() -> Html {
|
|||
<h1 class="wolves">{"ROLE BLOCKED"}</h1>
|
||||
<div class="information wolves faint">
|
||||
<h2>{"YOU WERE ROLE BLOCKED"}</h2>
|
||||
<h4 class="icons">
|
||||
<Icon source={IconSource::Roleblock} icon_type={IconType::Informational}/>
|
||||
</h4>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Roleblock} />
|
||||
</div>
|
||||
<h3 class="yellow">{"YOUR NIGHT ACTION DID NOT TAKE PLACE"}</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,35 @@
|
|||
// 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 yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[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,23 @@
|
|||
use yew::prelude::*;
|
||||
|
||||
use crate::components::{Icon, IconSource};
|
||||
|
||||
#[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>
|
||||
<div class="info-icon-grow">
|
||||
<Icon source={IconSource::Traitor}/>
|
||||
</div>
|
||||
<h4>{"HOWEVER"}</h4>
|
||||
<h2 class="yellow">
|
||||
{"YOU CONTRIBUTE TO VILLAGE PARITY"}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||