bloodletter implementation + aura logic + traitor
This commit is contained in:
parent
ad29c3d59c
commit
f193e4e691
|
|
@ -0,0 +1,153 @@
|
||||||
|
use core::fmt::Display;
|
||||||
|
|
||||||
|
// 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 serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::ChecksAs;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
game::{GameTime, Village},
|
||||||
|
role::{Alignment, Killer, Powerful},
|
||||||
|
team::Team,
|
||||||
|
};
|
||||||
|
const BLOODLET_DURATION_DAYS: u8 = 2;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ChecksAs)]
|
||||||
|
pub enum Aura {
|
||||||
|
Traitor,
|
||||||
|
#[checks("cleansible")]
|
||||||
|
Drunk,
|
||||||
|
Insane,
|
||||||
|
#[checks("cleansible")]
|
||||||
|
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::Insane => "Insane",
|
||||||
|
Aura::Bloodlet { .. } => "Bloodlet",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Aura {
|
||||||
|
pub const fn expired(&self, village: &Village) -> bool {
|
||||||
|
match self {
|
||||||
|
Aura::Traitor | Aura::Drunk | Aura::Insane => false,
|
||||||
|
Aura::Bloodlet {
|
||||||
|
night: applied_night,
|
||||||
|
} => match village.time() {
|
||||||
|
GameTime::Day { .. } => false,
|
||||||
|
GameTime::Night {
|
||||||
|
number: current_night,
|
||||||
|
} => current_night >= applied_night.saturating_add(BLOODLET_DURATION_DAYS),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const fn refreshes(&self, other: &Aura) -> bool {
|
||||||
|
matches!(
|
||||||
|
(self, other),
|
||||||
|
(Aura::Bloodlet { .. }, Aura::Bloodlet { .. })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn refresh(&mut self, other: Aura) {
|
||||||
|
if let (Aura::Bloodlet { night }, Aura::Bloodlet { night: new_night }) = (self, other) {
|
||||||
|
*night = new_night
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||||
|
pub struct Auras(Vec<Aura>);
|
||||||
|
|
||||||
|
impl Auras {
|
||||||
|
pub const fn new(auras: Vec<Aura>) -> Self {
|
||||||
|
Self(auras)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn list(&self) -> &[Aura] {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_aura(&mut self, aura: Aura) {
|
||||||
|
self.0.retain(|a| *a != aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// purges expired [Aura]s and returns the ones that were removed
|
||||||
|
pub fn purge_expired(&mut self, village: &Village) -> Box<[Aura]> {
|
||||||
|
let mut auras = Vec::with_capacity(self.0.len());
|
||||||
|
core::mem::swap(&mut self.0, &mut auras);
|
||||||
|
let (expired, retained): (Vec<_>, Vec<_>) =
|
||||||
|
auras.into_iter().partition(|aura| aura.expired(village));
|
||||||
|
|
||||||
|
self.0 = retained;
|
||||||
|
expired.into_boxed_slice()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add(&mut self, aura: Aura) {
|
||||||
|
if let Some(existing) = self.0.iter_mut().find(|aura| aura.refreshes(aura)) {
|
||||||
|
existing.refresh(aura);
|
||||||
|
} else {
|
||||||
|
self.0.push(aura);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn cleanse(&mut self) {
|
||||||
|
self.0.retain(|aura| !aura.cleansible());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override the player's [Team]
|
||||||
|
pub fn overrides_team(&self) -> Option<Team> {
|
||||||
|
if self.0.iter().any(|a| matches!(a, Aura::Traitor)) {
|
||||||
|
return Some(Team::AnyEvil);
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override the player's [Alignment]
|
||||||
|
pub fn overrides_alignment(&self) -> Option<Alignment> {
|
||||||
|
for aura in self.0.iter() {
|
||||||
|
match aura {
|
||||||
|
Aura::Traitor => return Some(Alignment::Traitor),
|
||||||
|
Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
||||||
|
Aura::Drunk | Aura::Insane => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override whether the player is a [Killer]
|
||||||
|
pub fn overrides_killer(&self) -> Option<Killer> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.any(|a| matches!(a, Aura::Bloodlet { .. }))
|
||||||
|
.then_some(Killer::Killer)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns [Some] if the auras override whether the player is [Powerful]
|
||||||
|
pub fn overrides_powerful(&self) -> Option<Powerful> {
|
||||||
|
self.0
|
||||||
|
.iter()
|
||||||
|
.any(|a| matches!(a, Aura::Bloodlet { .. }))
|
||||||
|
.then_some(Powerful::Powerful)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -22,11 +22,11 @@ use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::{Aura, Auras},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameTime, Village},
|
game::{GameTime, Village},
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
modifier::Modifier,
|
|
||||||
player::{PlayerId, RoleChange},
|
player::{PlayerId, RoleChange},
|
||||||
role::{
|
role::{
|
||||||
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
|
Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful,
|
||||||
|
|
@ -59,7 +59,7 @@ pub struct Character {
|
||||||
player_id: PlayerId,
|
player_id: PlayerId,
|
||||||
identity: CharacterIdentity,
|
identity: CharacterIdentity,
|
||||||
role: Role,
|
role: Role,
|
||||||
modifier: Option<Modifier>,
|
auras: Auras,
|
||||||
died_to: Option<DiedTo>,
|
died_to: Option<DiedTo>,
|
||||||
role_changes: Vec<RoleChange>,
|
role_changes: Vec<RoleChange>,
|
||||||
}
|
}
|
||||||
|
|
@ -76,19 +76,20 @@ impl Character {
|
||||||
},
|
},
|
||||||
}: Identification,
|
}: Identification,
|
||||||
role: Role,
|
role: Role,
|
||||||
|
auras: Vec<Aura>,
|
||||||
) -> Option<Self> {
|
) -> Option<Self> {
|
||||||
Some(Self {
|
Some(Self {
|
||||||
role,
|
role,
|
||||||
|
player_id,
|
||||||
|
died_to: None,
|
||||||
|
auras: Auras::new(auras),
|
||||||
|
role_changes: Vec::new(),
|
||||||
identity: CharacterIdentity {
|
identity: CharacterIdentity {
|
||||||
character_id: CharacterId::new(),
|
character_id: CharacterId::new(),
|
||||||
name,
|
name,
|
||||||
pronouns,
|
pronouns,
|
||||||
number: number?,
|
number: number?,
|
||||||
},
|
},
|
||||||
player_id,
|
|
||||||
modifier: None,
|
|
||||||
died_to: None,
|
|
||||||
role_changes: Vec::new(),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,13 +190,6 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn alignment(&self) -> Alignment {
|
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
|
||||||
return Alignment::Wolves;
|
|
||||||
}
|
|
||||||
self.role.alignment()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn elder_reveal(&mut self) {
|
pub fn elder_reveal(&mut self) {
|
||||||
if let Role::Elder {
|
if let Role::Elder {
|
||||||
woken_for_reveal, ..
|
woken_for_reveal, ..
|
||||||
|
|
@ -205,6 +199,14 @@ impl Character {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn purge_expired_auras(&mut self, village: &Village) -> Box<[Aura]> {
|
||||||
|
self.auras.purge_expired(village)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn auras(&self) -> &[Aura] {
|
||||||
|
self.auras.list()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> {
|
pub fn role_change(&mut self, new_role: RoleTitle, at: GameTime) -> Result<()> {
|
||||||
let mut role = new_role.title_to_role_excl_apprentice();
|
let mut role = new_role.title_to_role_excl_apprentice();
|
||||||
core::mem::swap(&mut role, &mut self.role);
|
core::mem::swap(&mut role, &mut self.role);
|
||||||
|
|
@ -298,6 +300,14 @@ impl Character {
|
||||||
AsCharacter(char)
|
AsCharacter(char)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn apply_aura(&mut self, aura: Aura) {
|
||||||
|
self.auras.add(aura);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove_aura(&mut self, aura: Aura) {
|
||||||
|
self.auras.remove_aura(aura);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
||||||
if self.mason_leader().is_ok() {
|
if self.mason_leader().is_ok() {
|
||||||
return self.mason_prompts(village);
|
return self.mason_prompts(village);
|
||||||
|
|
@ -345,6 +355,11 @@ impl Character {
|
||||||
return Ok(Box::new([]));
|
return Ok(Box::new([]));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Role::Bloodletter => ActionPrompt::Bloodletter {
|
||||||
|
character_id: self.identity(),
|
||||||
|
living_players: village.living_villagers(),
|
||||||
|
marked: None,
|
||||||
|
},
|
||||||
Role::Seer => ActionPrompt::Seer {
|
Role::Seer => ActionPrompt::Seer {
|
||||||
character_id: self.identity(),
|
character_id: self.identity(),
|
||||||
living_players: village.living_players_excluding(self.character_id()),
|
living_players: village.living_players_excluding(self.character_id()),
|
||||||
|
|
@ -574,14 +589,30 @@ impl Character {
|
||||||
self.role.killing_wolf_order()
|
self.role.killing_wolf_order()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn killer(&self) -> Killer {
|
pub fn alignment(&self) -> Alignment {
|
||||||
|
if let Some(alignment) = self.auras.overrides_alignment() {
|
||||||
|
return alignment;
|
||||||
|
}
|
||||||
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
|
return Alignment::Wolves;
|
||||||
|
}
|
||||||
|
self.role.alignment()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn killer(&self) -> Killer {
|
||||||
|
if let Some(killer) = self.auras.overrides_killer() {
|
||||||
|
return killer;
|
||||||
|
}
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
return Killer::Killer;
|
return Killer::Killer;
|
||||||
}
|
}
|
||||||
self.role.killer()
|
self.role.killer()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const fn powerful(&self) -> Powerful {
|
pub fn powerful(&self) -> Powerful {
|
||||||
|
if let Some(powerful) = self.auras.overrides_powerful() {
|
||||||
|
return powerful;
|
||||||
|
}
|
||||||
if let Role::Empath { cursed: true } = &self.role {
|
if let Role::Empath { cursed: true } = &self.role {
|
||||||
return Powerful::Powerful;
|
return Powerful::Powerful;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ use crate::{
|
||||||
kill::{self},
|
kill::{self},
|
||||||
night::changes::{ChangesLookup, NightChange},
|
night::changes::{ChangesLookup, NightChange},
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionResponse, ActionResult, Visits},
|
message::night::{ActionPrompt, ActionPromptTitle, ActionResponse, ActionResult, Visits},
|
||||||
role::RoleTitle,
|
role::RoleTitle,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -71,7 +71,11 @@ impl ActionPrompt {
|
||||||
..
|
..
|
||||||
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
} => Some(Unless::TargetsBlocked(*marked1, *marked2)),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
|
@ -148,7 +152,8 @@ impl ActionPrompt {
|
||||||
..
|
..
|
||||||
} => Some(Unless::TargetBlocked(*marked)),
|
} => Some(Unless::TargetBlocked(*marked)),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill { marked: None, .. }
|
ActionPrompt::Bloodletter { .. }
|
||||||
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||||
| ActionPrompt::Seer { marked: None, .. }
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
| ActionPrompt::Protector { marked: None, .. }
|
| ActionPrompt::Protector { marked: None, .. }
|
||||||
| ActionPrompt::Gravedigger { marked: None, .. }
|
| ActionPrompt::Gravedigger { marked: None, .. }
|
||||||
|
|
@ -258,15 +263,29 @@ pub struct Night {
|
||||||
night: u8,
|
night: u8,
|
||||||
action_queue: VecDeque<ActionPrompt>,
|
action_queue: VecDeque<ActionPrompt>,
|
||||||
used_actions: Vec<(ActionPrompt, ActionResult, Vec<NightChange>)>,
|
used_actions: Vec<(ActionPrompt, ActionResult, Vec<NightChange>)>,
|
||||||
|
start_of_night_changes: Vec<NightChange>,
|
||||||
night_state: NightState,
|
night_state: NightState,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Night {
|
impl Night {
|
||||||
pub fn new(village: Village) -> Result<Self> {
|
pub fn new(mut village: Village) -> Result<Self> {
|
||||||
let night = match village.time() {
|
let night = match village.time() {
|
||||||
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
GameTime::Night { number } => number,
|
GameTime::Night { number } => number,
|
||||||
};
|
};
|
||||||
|
let mut start_of_night_changes = Self::start_of_night_changes(&village, night);
|
||||||
|
// purge expired auras
|
||||||
|
{
|
||||||
|
let village_clone = village.clone();
|
||||||
|
for char in village.characters_mut() {
|
||||||
|
for expired_aura in char.purge_expired_auras(&village_clone) {
|
||||||
|
start_of_night_changes.push(NightChange::LostAura {
|
||||||
|
character: char.character_id(),
|
||||||
|
aura: expired_aura,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let filter = if village.executed_known_elder() {
|
let filter = if village.executed_known_elder() {
|
||||||
// there is a lynched elder, remove villager PRs from the prompts
|
// there is a lynched elder, remove villager PRs from the prompts
|
||||||
|
|
@ -380,6 +399,7 @@ impl Night {
|
||||||
village,
|
village,
|
||||||
night_state,
|
night_state,
|
||||||
action_queue,
|
action_queue,
|
||||||
|
start_of_night_changes,
|
||||||
used_actions: Vec::new(),
|
used_actions: Vec::new(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
@ -402,77 +422,37 @@ impl Night {
|
||||||
// for prompt in action_queue {
|
// for prompt in action_queue {
|
||||||
while let Some(prompt) = action_queue.pop_front() {
|
while let Some(prompt) = action_queue.pop_front() {
|
||||||
log::warn!("prompt: {:?}", prompt.title());
|
log::warn!("prompt: {:?}", prompt.title());
|
||||||
let (wolf_id, prompt) = match prompt {
|
match prompt {
|
||||||
ActionPrompt::WolvesIntro { mut wolves } => {
|
ActionPrompt::WolvesIntro { mut wolves } => {
|
||||||
if let Some(w) = wolves.iter_mut().find(|w| w.0.character_id == reverting) {
|
if let Some(w) = wolves.iter_mut().find(|w| w.0.character_id == reverting) {
|
||||||
w.1 = reverting_into;
|
w.1 = reverting_into;
|
||||||
}
|
}
|
||||||
new_queue.push_back(ActionPrompt::WolvesIntro { wolves });
|
new_queue.push_back(ActionPrompt::WolvesIntro { wolves });
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionPrompt::Shapeshifter { character_id } => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::Shapeshifter { character_id },
|
|
||||||
),
|
|
||||||
ActionPrompt::AlphaWolf {
|
|
||||||
character_id,
|
|
||||||
living_villagers,
|
|
||||||
marked,
|
|
||||||
} => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::AlphaWolf {
|
|
||||||
character_id,
|
|
||||||
living_villagers,
|
|
||||||
marked,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ActionPrompt::DireWolf {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
} => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::DireWolf {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ActionPrompt::LoneWolfKill {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
} => (
|
|
||||||
character_id.character_id,
|
|
||||||
ActionPrompt::LoneWolfKill {
|
|
||||||
character_id,
|
|
||||||
living_players,
|
|
||||||
marked,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
other => {
|
other => {
|
||||||
|
if let Some(char_id) = other.character_id()
|
||||||
|
&& char_id == reverting
|
||||||
|
&& !matches!(other.title(), ActionPromptTitle::RoleChange)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
new_queue.push_back(other);
|
new_queue.push_back(other);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if wolf_id != reverting {
|
|
||||||
new_queue.push_back(prompt);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
new_queue
|
new_queue
|
||||||
}
|
}
|
||||||
|
|
||||||
/// changes that require no input (such as hunter firing)
|
/// changes from the beginning of the night that require no input (such as hunter firing)
|
||||||
fn automatic_changes(&self) -> Vec<NightChange> {
|
fn start_of_night_changes(village: &Village, night: u8) -> Vec<NightChange> {
|
||||||
let mut changes = Vec::new();
|
let mut changes = Vec::new();
|
||||||
let night = match NonZeroU8::new(self.night) {
|
let night = match NonZeroU8::new(night) {
|
||||||
Some(night) => night,
|
Some(night) => night,
|
||||||
None => return changes,
|
None => return changes,
|
||||||
};
|
};
|
||||||
if !self.village.executed_known_elder() {
|
if !village.executed_known_elder() {
|
||||||
self.village
|
village
|
||||||
.dead_characters()
|
.dead_characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
.filter_map(|c| c.died_to().map(|d| (c, d)))
|
||||||
|
|
@ -576,9 +556,13 @@ impl Night {
|
||||||
if !matches!(self.night_state, NightState::Complete) {
|
if !matches!(self.night_state, NightState::Complete) {
|
||||||
return Err(GameError::NotEndOfNight);
|
return Err(GameError::NotEndOfNight);
|
||||||
}
|
}
|
||||||
let mut all_changes = self.automatic_changes();
|
Ok(self.current_changes())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn current_changes(&self) -> Box<[NightChange]> {
|
||||||
|
let mut all_changes = self.start_of_night_changes.clone();
|
||||||
all_changes.append(&mut self.changes_from_actions().into_vec());
|
all_changes.append(&mut self.changes_from_actions().into_vec());
|
||||||
Ok(all_changes.into_boxed_slice())
|
all_changes.into_boxed_slice()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn apply_mason_recruit(
|
fn apply_mason_recruit(
|
||||||
|
|
@ -1061,11 +1045,7 @@ impl Night {
|
||||||
/// resolves whether the target [CharacterId] dies tonight with the current
|
/// resolves whether the target [CharacterId] dies tonight with the current
|
||||||
/// state of the night
|
/// state of the night
|
||||||
fn dies_tonight(&self, character_id: CharacterId) -> Result<bool> {
|
fn dies_tonight(&self, character_id: CharacterId) -> Result<bool> {
|
||||||
let ch = self
|
let ch = self.current_changes();
|
||||||
.changes_from_actions()
|
|
||||||
.into_iter()
|
|
||||||
.chain(self.automatic_changes())
|
|
||||||
.collect::<Box<[_]>>();
|
|
||||||
let mut changes = ChangesLookup::new(&ch);
|
let mut changes = ChangesLookup::new(&ch);
|
||||||
if let Some(died_to) = changes.killed(character_id)
|
if let Some(died_to) = changes.killed(character_id)
|
||||||
&& kill::resolve_kill(
|
&& kill::resolve_kill(
|
||||||
|
|
@ -1083,6 +1063,23 @@ impl Night {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// returns the matching [Character] with the current night's aura changes
|
||||||
|
/// applied
|
||||||
|
fn character_with_current_auras(&self, id: CharacterId) -> Result<Character> {
|
||||||
|
let mut character = self.village.character_by_id(id)?.clone();
|
||||||
|
for aura in self
|
||||||
|
.changes_from_actions()
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|c| match c {
|
||||||
|
NightChange::ApplyAura { target, aura, .. } => (target == id).then_some(aura),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
character.apply_aura(aura);
|
||||||
|
}
|
||||||
|
Ok(character)
|
||||||
|
}
|
||||||
|
|
||||||
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
fn changes_from_actions(&self) -> Box<[NightChange]> {
|
||||||
self.used_actions
|
self.used_actions
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -1109,7 +1106,12 @@ impl Night {
|
||||||
.then(|| self.village.killing_wolf().map(|c| c.identity()))
|
.then(|| self.village.killing_wolf().map(|c| c.identity()))
|
||||||
.flatten(),
|
.flatten(),
|
||||||
|
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::Seer {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
|
|
@ -1200,7 +1202,8 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => (*marked == visit_char).then(|| character_id.clone()),
|
} => (*marked == visit_char).then(|| character_id.clone()),
|
||||||
|
|
||||||
ActionPrompt::WolfPackKill { marked: None, .. }
|
ActionPrompt::Bloodletter { .. }
|
||||||
|
| ActionPrompt::WolfPackKill { marked: None, .. }
|
||||||
| ActionPrompt::Arcanist { marked: _, .. }
|
| ActionPrompt::Arcanist { marked: _, .. }
|
||||||
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
| ActionPrompt::LoneWolfKill { marked: None, .. }
|
||||||
| ActionPrompt::Seer { marked: None, .. }
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use werewolves_macros::Extract;
|
use werewolves_macros::Extract;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
character::CharacterId,
|
character::CharacterId,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
player::Protection,
|
player::Protection,
|
||||||
|
|
@ -59,6 +60,15 @@ pub enum NightChange {
|
||||||
empath: CharacterId,
|
empath: CharacterId,
|
||||||
scapegoat: CharacterId,
|
scapegoat: CharacterId,
|
||||||
},
|
},
|
||||||
|
ApplyAura {
|
||||||
|
source: CharacterId,
|
||||||
|
target: CharacterId,
|
||||||
|
aura: Aura,
|
||||||
|
},
|
||||||
|
LostAura {
|
||||||
|
character: CharacterId,
|
||||||
|
aura: Aura,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::night::{
|
game::night::{
|
||||||
|
|
@ -108,6 +109,19 @@ impl Night {
|
||||||
};
|
};
|
||||||
|
|
||||||
match current_prompt {
|
match current_prompt {
|
||||||
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked: Some(marked),
|
||||||
|
} => Ok(ActionComplete {
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
|
change: Some(NightChange::ApplyAura {
|
||||||
|
source: character_id.character_id,
|
||||||
|
aura: Aura::Bloodlet { night: self.night },
|
||||||
|
target: *marked,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
.into()),
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::LoneWolfKill {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -143,7 +157,7 @@ impl Night {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let alignment = self.village.character_by_id(*marked)?.alignment();
|
let alignment = self.character_with_current_auras(*marked)?.alignment();
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Seer(alignment),
|
result: ActionResult::Seer(alignment),
|
||||||
change: None,
|
change: None,
|
||||||
|
|
@ -166,8 +180,8 @@ impl Night {
|
||||||
marked: (Some(marked1), Some(marked2)),
|
marked: (Some(marked1), Some(marked2)),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let same = self.village.character_by_id(*marked1)?.alignment()
|
let same = self.character_with_current_auras(*marked1)?.alignment()
|
||||||
== self.village.character_by_id(*marked2)?.alignment();
|
== self.character_with_current_auras(*marked2)?.alignment();
|
||||||
|
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
||||||
|
|
@ -178,7 +192,9 @@ impl Night {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let dig_role = self.village.character_by_id(*marked)?.gravedigger_dig();
|
let dig_role = self
|
||||||
|
.character_with_current_auras(*marked)?
|
||||||
|
.gravedigger_dig();
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GraveDigger(dig_role),
|
result: ActionResult::GraveDigger(dig_role),
|
||||||
change: None,
|
change: None,
|
||||||
|
|
@ -359,7 +375,7 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => Ok(ActionComplete {
|
} => Ok(ActionComplete {
|
||||||
result: ActionResult::Adjudicator {
|
result: ActionResult::Adjudicator {
|
||||||
killer: self.village.character_by_id(*marked)?.killer(),
|
killer: self.character_with_current_auras(*marked)?.killer(),
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
}
|
}
|
||||||
|
|
@ -369,7 +385,7 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => Ok(ActionComplete {
|
} => Ok(ActionComplete {
|
||||||
result: ActionResult::PowerSeer {
|
result: ActionResult::PowerSeer {
|
||||||
powerful: self.village.character_by_id(*marked)?.powerful(),
|
powerful: self.character_with_current_auras(*marked)?.powerful(),
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
}
|
}
|
||||||
|
|
@ -489,7 +505,8 @@ impl Night {
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
|
|
||||||
ActionPrompt::Adjudicator { marked: None, .. }
|
ActionPrompt::Bloodletter { marked: None, .. }
|
||||||
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
||||||
| ActionPrompt::PowerSeer { marked: None, .. }
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
||||||
| ActionPrompt::Mortician { marked: None, .. }
|
| ActionPrompt::Mortician { marked: None, .. }
|
||||||
| ActionPrompt::Beholder { marked: None, .. }
|
| ActionPrompt::Beholder { marked: None, .. }
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,10 @@ use uuid::Uuid;
|
||||||
use werewolves_macros::{All, ChecksAs, Titles};
|
use werewolves_macros::{All, ChecksAs, Titles};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
character::Character,
|
character::Character,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
message::Identification,
|
message::Identification,
|
||||||
modifier::Modifier,
|
|
||||||
player::PlayerId,
|
player::PlayerId,
|
||||||
role::{Role, RoleTitle},
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -127,6 +127,8 @@ pub enum SetupRole {
|
||||||
Shapeshifter,
|
Shapeshifter,
|
||||||
#[checks(Category::Wolves)]
|
#[checks(Category::Wolves)]
|
||||||
LoneWolf,
|
LoneWolf,
|
||||||
|
#[checks(Category::Wolves)]
|
||||||
|
Bloodletter,
|
||||||
|
|
||||||
#[checks(Category::Intel)]
|
#[checks(Category::Intel)]
|
||||||
Adjudicator,
|
Adjudicator,
|
||||||
|
|
@ -157,6 +159,7 @@ pub enum SetupRole {
|
||||||
impl SetupRoleTitle {
|
impl SetupRoleTitle {
|
||||||
pub fn into_role(self) -> Role {
|
pub fn into_role(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
|
SetupRoleTitle::Bloodletter => Role::Bloodletter,
|
||||||
SetupRoleTitle::Insomniac => Role::Insomniac,
|
SetupRoleTitle::Insomniac => Role::Insomniac,
|
||||||
SetupRoleTitle::LoneWolf => Role::LoneWolf,
|
SetupRoleTitle::LoneWolf => Role::LoneWolf,
|
||||||
SetupRoleTitle::Villager => Role::Villager,
|
SetupRoleTitle::Villager => Role::Villager,
|
||||||
|
|
@ -208,6 +211,7 @@ impl SetupRoleTitle {
|
||||||
impl Display for SetupRole {
|
impl Display for SetupRole {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
f.write_str(match self {
|
f.write_str(match self {
|
||||||
|
SetupRole::Bloodletter => "Bloodletter",
|
||||||
SetupRole::Insomniac => "Insomniac",
|
SetupRole::Insomniac => "Insomniac",
|
||||||
SetupRole::LoneWolf => "Lone Wolf",
|
SetupRole::LoneWolf => "Lone Wolf",
|
||||||
SetupRole::Villager => "Villager",
|
SetupRole::Villager => "Villager",
|
||||||
|
|
@ -244,6 +248,7 @@ impl Display for SetupRole {
|
||||||
impl SetupRole {
|
impl SetupRole {
|
||||||
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result<Role, GameError> {
|
||||||
Ok(match self {
|
Ok(match self {
|
||||||
|
Self::Bloodletter => Role::Bloodletter,
|
||||||
SetupRole::Insomniac => Role::Insomniac,
|
SetupRole::Insomniac => Role::Insomniac,
|
||||||
SetupRole::LoneWolf => Role::LoneWolf,
|
SetupRole::LoneWolf => Role::LoneWolf,
|
||||||
SetupRole::Villager => Role::Villager,
|
SetupRole::Villager => Role::Villager,
|
||||||
|
|
@ -321,6 +326,7 @@ impl From<SetupRole> for RoleTitle {
|
||||||
impl From<RoleTitle> for SetupRole {
|
impl From<RoleTitle> for SetupRole {
|
||||||
fn from(value: RoleTitle) -> Self {
|
fn from(value: RoleTitle) -> Self {
|
||||||
match value {
|
match value {
|
||||||
|
RoleTitle::Bloodletter => SetupRole::Bloodletter,
|
||||||
RoleTitle::Insomniac => SetupRole::Insomniac,
|
RoleTitle::Insomniac => SetupRole::Insomniac,
|
||||||
RoleTitle::LoneWolf => SetupRole::LoneWolf,
|
RoleTitle::LoneWolf => SetupRole::LoneWolf,
|
||||||
RoleTitle::Villager => SetupRole::Villager,
|
RoleTitle::Villager => SetupRole::Villager,
|
||||||
|
|
@ -373,7 +379,7 @@ impl SlotId {
|
||||||
pub struct SetupSlot {
|
pub struct SetupSlot {
|
||||||
pub slot_id: SlotId,
|
pub slot_id: SlotId,
|
||||||
pub role: SetupRole,
|
pub role: SetupRole,
|
||||||
pub modifiers: Vec<Modifier>,
|
pub auras: Vec<Aura>,
|
||||||
pub assign_to: Option<PlayerId>,
|
pub assign_to: Option<PlayerId>,
|
||||||
pub created_order: u32,
|
pub created_order: u32,
|
||||||
}
|
}
|
||||||
|
|
@ -384,7 +390,7 @@ impl SetupSlot {
|
||||||
created_order,
|
created_order,
|
||||||
assign_to: None,
|
assign_to: None,
|
||||||
role: title.into(),
|
role: title.into(),
|
||||||
modifiers: Vec::new(),
|
auras: Vec::new(),
|
||||||
slot_id: SlotId::new(),
|
slot_id: SlotId::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -394,8 +400,12 @@ impl SetupSlot {
|
||||||
ident: Identification,
|
ident: Identification,
|
||||||
roles_in_game: &[RoleTitle],
|
roles_in_game: &[RoleTitle],
|
||||||
) -> Result<Character, GameError> {
|
) -> Result<Character, GameError> {
|
||||||
Character::new(ident.clone(), self.role.into_role(roles_in_game)?)
|
Character::new(
|
||||||
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
ident.clone(),
|
||||||
|
self.role.into_role(roles_in_game)?,
|
||||||
|
self.auras,
|
||||||
|
)
|
||||||
|
.ok_or(GameError::PlayerNotAssignedNumber(ident.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -199,11 +199,23 @@ pub enum StoryActionPrompt {
|
||||||
Insomniac {
|
Insomniac {
|
||||||
character_id: CharacterId,
|
character_id: CharacterId,
|
||||||
},
|
},
|
||||||
|
Bloodletter {
|
||||||
|
character_id: CharacterId,
|
||||||
|
chosen: CharacterId,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StoryActionPrompt {
|
impl StoryActionPrompt {
|
||||||
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
pub fn new(prompt: ActionPrompt) -> Option<Self> {
|
||||||
Some(match prompt {
|
Some(match prompt {
|
||||||
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
} => Self::Bloodletter {
|
||||||
|
character_id: character_id.character_id,
|
||||||
|
chosen: marked,
|
||||||
|
},
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Seer {
|
||||||
character_id,
|
character_id,
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -378,7 +390,8 @@ impl StoryActionPrompt {
|
||||||
character_id: character_id.character_id,
|
character_id: character_id.character_id,
|
||||||
},
|
},
|
||||||
|
|
||||||
ActionPrompt::Protector { .. }
|
ActionPrompt::Bloodletter { .. }
|
||||||
|
| ActionPrompt::Protector { .. }
|
||||||
| ActionPrompt::Gravedigger { .. }
|
| ActionPrompt::Gravedigger { .. }
|
||||||
| ActionPrompt::Hunter { .. }
|
| ActionPrompt::Hunter { .. }
|
||||||
| ActionPrompt::Militia { .. }
|
| ActionPrompt::Militia { .. }
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -294,6 +295,7 @@ impl Village {
|
||||||
impl RoleTitle {
|
impl RoleTitle {
|
||||||
pub fn title_to_role_excl_apprentice(self) -> Role {
|
pub fn title_to_role_excl_apprentice(self) -> Role {
|
||||||
match self {
|
match self {
|
||||||
|
RoleTitle::Bloodletter => Role::Bloodletter,
|
||||||
RoleTitle::Insomniac => Role::Insomniac,
|
RoleTitle::Insomniac => Role::Insomniac,
|
||||||
RoleTitle::LoneWolf => Role::LoneWolf,
|
RoleTitle::LoneWolf => Role::LoneWolf,
|
||||||
RoleTitle::Villager => Role::Villager,
|
RoleTitle::Villager => Role::Villager,
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::Aura,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
|
|
@ -52,6 +53,10 @@ impl Village {
|
||||||
let mut new_village = self.clone();
|
let mut new_village = self.clone();
|
||||||
for change in all_changes {
|
for change in all_changes {
|
||||||
match change {
|
match change {
|
||||||
|
NightChange::ApplyAura { target, aura, .. } => {
|
||||||
|
let target = new_village.character_by_id_mut(*target)?;
|
||||||
|
target.apply_aura(*aura);
|
||||||
|
}
|
||||||
NightChange::ElderReveal { elder } => {
|
NightChange::ElderReveal { elder } => {
|
||||||
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
||||||
}
|
}
|
||||||
|
|
@ -125,7 +130,6 @@ impl Village {
|
||||||
.replace(*target);
|
.replace(*target);
|
||||||
}
|
}
|
||||||
|
|
||||||
NightChange::Protection { .. } => {}
|
|
||||||
NightChange::MasonRecruit {
|
NightChange::MasonRecruit {
|
||||||
mason_leader,
|
mason_leader,
|
||||||
recruiting,
|
recruiting,
|
||||||
|
|
@ -150,6 +154,12 @@ impl Village {
|
||||||
.role_change(RoleTitle::Villager, GameTime::Night { number: night })?;
|
.role_change(RoleTitle::Villager, GameTime::Night { number: night })?;
|
||||||
*new_village.character_by_id_mut(*empath)?.empath_mut()? = true;
|
*new_village.character_by_id_mut(*empath)?.empath_mut()? = true;
|
||||||
}
|
}
|
||||||
|
NightChange::LostAura { character, aura } => {
|
||||||
|
new_village
|
||||||
|
.character_by_id_mut(*character)?
|
||||||
|
.remove_aura(*aura);
|
||||||
|
}
|
||||||
|
NightChange::Protection { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// black knights death
|
// black knights death
|
||||||
|
|
|
||||||
|
|
@ -84,9 +84,13 @@ pub trait ActionPromptTitleExt {
|
||||||
fn power_seer(&self);
|
fn power_seer(&self);
|
||||||
fn mortician(&self);
|
fn mortician(&self);
|
||||||
fn elder_reveal(&self);
|
fn elder_reveal(&self);
|
||||||
|
fn bloodletter(&self);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPromptTitleExt for ActionPromptTitle {
|
impl ActionPromptTitleExt for ActionPromptTitle {
|
||||||
|
fn bloodletter(&self) {
|
||||||
|
assert_eq!(*self, ActionPromptTitle::Bloodletter);
|
||||||
|
}
|
||||||
fn elder_reveal(&self) {
|
fn elder_reveal(&self) {
|
||||||
assert_eq!(*self, ActionPromptTitle::ElderReveal);
|
assert_eq!(*self, ActionPromptTitle::ElderReveal);
|
||||||
}
|
}
|
||||||
|
|
@ -402,7 +406,11 @@ impl GameExt for Game {
|
||||||
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
||||||
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
|
@ -478,7 +486,8 @@ impl GameExt for Game {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => assert_eq!(marked, mark, "marked character"),
|
} => assert_eq!(marked, mark, "marked character"),
|
||||||
ActionPrompt::Seer { marked: None, .. }
|
ActionPrompt::Bloodletter { marked: None, .. }
|
||||||
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
| ActionPrompt::Adjudicator { marked: None, .. }
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
||||||
| ActionPrompt::PowerSeer { marked: None, .. }
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
||||||
| ActionPrompt::Mortician { marked: None, .. }
|
| ActionPrompt::Mortician { marked: None, .. }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
// 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::{
|
||||||
|
aura::Aura,
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::ActionPromptTitle,
|
||||||
|
role::{Alignment, Killer, Powerful},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lasts_2_nights() {
|
||||||
|
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 seer = player_ids.next().unwrap();
|
||||||
|
let adjudicator = player_ids.next().unwrap();
|
||||||
|
let power_seer = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let bloodletter = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Villager, target);
|
||||||
|
settings.add_and_assign(SetupRole::Seer, seer);
|
||||||
|
settings.add_and_assign(SetupRole::Adjudicator, adjudicator);
|
||||||
|
settings.add_and_assign(SetupRole::PowerSeer, power_seer);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Bloodletter, bloodletter);
|
||||||
|
settings.fill_remaining_slots_with_villagers(9);
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().bloodletter();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::Killer);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().power_seer(), Powerful::Powerful);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(target).auras(),
|
||||||
|
&[Aura::Bloodlet { night: 0 }]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(target).character_id());
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().bloodletter();
|
||||||
|
game.mark(game.character_by_player_id(seer).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Wolves);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::Killer);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().power_seer(), Powerful::Powerful);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(target).auras(),
|
||||||
|
&[Aura::Bloodlet { night: 0 }]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark_for_execution(game.character_by_player_id(bloodletter).character_id());
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(target).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().seer(), Alignment::Village);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().adjudicator();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::NotKiller);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().power_seer();
|
||||||
|
game.mark(game.character_by_player_id(target).character_id());
|
||||||
|
assert_eq!(game.r#continue().power_seer(), Powerful::NotPowerful);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(target).auras(), &[]);
|
||||||
|
}
|
||||||
|
|
@ -15,6 +15,7 @@
|
||||||
mod apprentice;
|
mod apprentice;
|
||||||
mod beholder;
|
mod beholder;
|
||||||
mod black_knight;
|
mod black_knight;
|
||||||
|
mod bloodletter;
|
||||||
mod diseased;
|
mod diseased;
|
||||||
mod elder;
|
mod elder;
|
||||||
mod empath;
|
mod empath;
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
#![allow(clippy::new_without_default)]
|
#![allow(clippy::new_without_default)]
|
||||||
|
|
||||||
|
pub mod aura;
|
||||||
pub mod character;
|
pub mod character;
|
||||||
pub mod diedto;
|
pub mod diedto;
|
||||||
pub mod error;
|
pub mod error;
|
||||||
|
|
@ -21,7 +22,7 @@ pub mod game;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod game_test;
|
mod game_test;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod modifier;
|
|
||||||
pub mod nonzero;
|
pub mod nonzero;
|
||||||
pub mod player;
|
pub mod player;
|
||||||
pub mod role;
|
pub mod role;
|
||||||
|
pub mod team;
|
||||||
|
|
|
||||||
|
|
@ -203,6 +203,12 @@ pub enum ActionPrompt {
|
||||||
},
|
},
|
||||||
#[checks(ActionType::Insomniac)]
|
#[checks(ActionType::Insomniac)]
|
||||||
Insomniac { character_id: CharacterIdentity },
|
Insomniac { character_id: CharacterIdentity },
|
||||||
|
#[checks(ActionType::OtherWolf)]
|
||||||
|
Bloodletter {
|
||||||
|
character_id: CharacterIdentity,
|
||||||
|
living_players: Box<[CharacterIdentity]>,
|
||||||
|
marked: Option<CharacterId>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ActionPrompt {
|
impl ActionPrompt {
|
||||||
|
|
@ -230,6 +236,7 @@ impl ActionPrompt {
|
||||||
| ActionPrompt::Empath { character_id, .. }
|
| ActionPrompt::Empath { character_id, .. }
|
||||||
| ActionPrompt::Vindicator { character_id, .. }
|
| ActionPrompt::Vindicator { character_id, .. }
|
||||||
| ActionPrompt::PyreMaster { character_id, .. }
|
| ActionPrompt::PyreMaster { character_id, .. }
|
||||||
|
| ActionPrompt::Bloodletter { character_id, .. }
|
||||||
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
|
| ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id),
|
||||||
|
|
||||||
ActionPrompt::WolvesIntro { .. }
|
ActionPrompt::WolvesIntro { .. }
|
||||||
|
|
@ -241,6 +248,7 @@ impl ActionPrompt {
|
||||||
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
|
pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool {
|
||||||
match self {
|
match self {
|
||||||
ActionPrompt::Insomniac { character_id, .. }
|
ActionPrompt::Insomniac { character_id, .. }
|
||||||
|
| ActionPrompt::Bloodletter { character_id, .. }
|
||||||
| ActionPrompt::Seer { character_id, .. }
|
| ActionPrompt::Seer { character_id, .. }
|
||||||
| ActionPrompt::Arcanist { character_id, .. }
|
| ActionPrompt::Arcanist { character_id, .. }
|
||||||
| ActionPrompt::Gravedigger { character_id, .. }
|
| ActionPrompt::Gravedigger { character_id, .. }
|
||||||
|
|
@ -344,7 +352,12 @@ impl ActionPrompt {
|
||||||
Ok(prompt)
|
Ok(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
living_players: targets,
|
||||||
|
marked,
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
living_players: targets,
|
living_players: targets,
|
||||||
marked,
|
marked,
|
||||||
..
|
..
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,11 @@ pub enum Role {
|
||||||
#[checks(Powerful::Powerful)]
|
#[checks(Powerful::Powerful)]
|
||||||
#[checks("wolf")]
|
#[checks("wolf")]
|
||||||
LoneWolf,
|
LoneWolf,
|
||||||
|
#[checks(Alignment::Wolves)]
|
||||||
|
#[checks(Killer::Killer)]
|
||||||
|
#[checks(Powerful::Powerful)]
|
||||||
|
#[checks("wolf")]
|
||||||
|
Bloodletter,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Role {
|
impl Role {
|
||||||
|
|
@ -313,6 +318,7 @@ impl Role {
|
||||||
|
|
||||||
Role::Werewolf => KillingWolfOrder::Werewolf,
|
Role::Werewolf => KillingWolfOrder::Werewolf,
|
||||||
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf,
|
||||||
|
Role::Bloodletter { .. } => KillingWolfOrder::Bloodletter,
|
||||||
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
Role::DireWolf { .. } => KillingWolfOrder::DireWolf,
|
||||||
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter,
|
||||||
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
Role::LoneWolf => KillingWolfOrder::LoneWolf,
|
||||||
|
|
@ -325,7 +331,7 @@ impl Role {
|
||||||
| Role::Adjudicator
|
| Role::Adjudicator
|
||||||
| Role::DireWolf { .. }
|
| Role::DireWolf { .. }
|
||||||
| Role::Arcanist
|
| Role::Arcanist
|
||||||
| Role::Seer => true,
|
| Role::Seer | Role::Bloodletter => true,
|
||||||
|
|
||||||
Role::Insomniac // has to at least get one good night of sleep, right?
|
Role::Insomniac // has to at least get one good night of sleep, right?
|
||||||
| Role::Beholder
|
| Role::Beholder
|
||||||
|
|
@ -400,6 +406,7 @@ impl Role {
|
||||||
| Role::Militia { targeted: None }
|
| Role::Militia { targeted: None }
|
||||||
| Role::MapleWolf { .. }
|
| Role::MapleWolf { .. }
|
||||||
| Role::Guardian { .. }
|
| Role::Guardian { .. }
|
||||||
|
| Role::Bloodletter
|
||||||
| Role::Seer => true,
|
| Role::Seer => true,
|
||||||
|
|
||||||
Role::Apprentice(title) => village
|
Role::Apprentice(title) => village
|
||||||
|
|
@ -444,6 +451,7 @@ impl RoleTitle {
|
||||||
pub enum Alignment {
|
pub enum Alignment {
|
||||||
Village,
|
Village,
|
||||||
Wolves,
|
Wolves,
|
||||||
|
Traitor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Alignment {
|
impl Alignment {
|
||||||
|
|
@ -461,6 +469,7 @@ impl Display for Alignment {
|
||||||
match self {
|
match self {
|
||||||
Alignment::Village => f.write_str("Village"),
|
Alignment::Village => f.write_str("Village"),
|
||||||
Alignment::Wolves => f.write_str("Wolves"),
|
Alignment::Wolves => f.write_str("Wolves"),
|
||||||
|
Alignment::Traitor => f.write_str("Damned"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use werewolves_macros::ChecksAs;
|
||||||
|
|
||||||
// Copyright (C) 2025 Emilis Bliūdžius
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
//
|
//
|
||||||
// This program is free software: you can redistribute it and/or modify
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
|
@ -12,10 +15,10 @@
|
||||||
//
|
//
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use serde::{Deserialize, Serialize};
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ChecksAs)]
|
||||||
|
pub enum Team {
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
|
Village,
|
||||||
pub enum Modifier {
|
#[checks("evil")]
|
||||||
Drunk,
|
Wolves,
|
||||||
Insane,
|
AnyEvil,
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="64.542282mm"
|
||||||
|
height="80.020309mm"
|
||||||
|
viewBox="0 0 64.542282 80.020309"
|
||||||
|
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(-501.45474,-289.80731)"><g
|
||||||
|
id="g178-3"
|
||||||
|
transform="translate(19.691291,95.898306)"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874"><path
|
||||||
|
id="path175-7-0-7-6"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874;stroke:#c10000;stroke-width:1.99668;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 513.95892,195.94614 c -3.10481,5.52989 -13.31235,25.09794 -14.05905,44.58016 -0.40935,10.68044 10.72122,10.85122 14.09046,10.65924 3.36947,0.18812 14.50014,0.005 14.07863,-10.67474 -0.76889,-19.48135 -10.99894,-39.03835 -14.11004,-44.56466 z" /><g
|
||||||
|
id="g177-5"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874"><path
|
||||||
|
id="path175-7-0-4-6"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874;stroke:#c10000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 496.88096,217.65547 c -3.11544,5.52938 -13.3579,25.09565 -14.10715,44.57609 -0.41075,10.67947 10.75789,10.85023 14.13867,10.65827 3.381,0.1881 14.54975,0.005 14.12679,-10.67377 -0.77152,-19.47957 -11.03656,-39.03478 -14.15831,-44.56059 z" /><path
|
||||||
|
id="path175-7-0-9-3"
|
||||||
|
style="fill:#ff0707;fill-opacity:0.694874;stroke:#c10000;stroke-width:2;stroke-dasharray:none;stroke-opacity:1"
|
||||||
|
d="m 531.13647,217.65547 c -3.11544,5.52938 -13.3579,25.09565 -14.10715,44.57609 -0.41075,10.67947 10.75789,10.85023 14.13867,10.65827 3.381,0.1881 14.54975,0.005 14.12679,-10.67377 -0.77152,-19.47957 -11.03656,-39.03478 -14.15831,-44.56059 z" /></g></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
|
|
@ -0,0 +1,22 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<svg
|
||||||
|
width="63.999481mm"
|
||||||
|
height="58.622898mm"
|
||||||
|
viewBox="0 0 63.999481 58.622898"
|
||||||
|
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(-494.49485,-99.66878)"><g
|
||||||
|
id="g174"><path
|
||||||
|
id="path31-2"
|
||||||
|
style="fill:#d77fff;fill-opacity:1;stroke:#a300eb;stroke-width:1.561"
|
||||||
|
d="m 497.38435,113.0436 a 29.821014,23.755724 75 0 0 -0.8444,21.15361 29.821014,23.755724 75 0 0 30.66443,22.65638 l -2.71657,-10.13837 a 16.30047,20.596718 75 0 0 -9.05328,5.10293 9.0979366,10.867 75 0 1 7.93756,-9.26688 l -8.20534,-30.62273 a 14.278707,28.051973 75 0 1 -17.7824,1.11506 z m 9.24862,11.76284 a 9.3881665,4.5422098 75 0 1 5.81712,5.06988 16.820465,8.6090562 75 0 0 -4.41462,-0.85597 16.820465,8.6090562 75 0 0 -3.81591,3.81033 9.3881665,4.5422098 75 0 1 2.41341,-8.02424 z" /><path
|
||||||
|
id="path32-7"
|
||||||
|
style="fill:#d77fff;fill-opacity:1;stroke:#a300eb;stroke-width:1.561"
|
||||||
|
d="m 555.88352,102.49759 a 28.051973,14.278707 15 0 1 -17.08676,-1.58883 28.051973,14.278707 15 0 1 -0.97402,-0.27031 l -8.92704,33.31616 a 20.596718,16.30047 15 0 0 0.34214,0.0948 20.596718,16.30047 15 0 0 9.83564,0.65972 10.867,9.0979366 15 0 1 -11.28974,3.39538 l -1.99873,7.45939 a 23.755724,29.821014 15 0 0 30.66441,-22.65638 23.755724,29.821014 15 0 0 -0.56609,-20.40922 z m -9.43994,11.04123 a 4.5422098,9.3881665 15 0 1 2.50283,7.2992 8.6090562,16.820465 15 0 0 -3.39532,-2.9481 8.6090562,16.820465 15 0 0 -5.21033,1.39175 4.5422098,9.3881665 15 0 1 6.10282,-5.74285 z" /></g></g></svg>
|
||||||
|
After Width: | Height: | Size: 1.7 KiB |
|
|
@ -2,19 +2,19 @@
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
width="52.348mm"
|
width="52.348mm"
|
||||||
height="52.348007mm"
|
height="52.348007mm"
|
||||||
viewBox="0 0 52.348 52.348007"
|
viewBox="0 0 52.348 52.348007"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
id="svg1"
|
id="svg1"
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||||
sodipodi:docname="icons.svg"
|
sodipodi:docname="icons.svg"
|
||||||
xml:space="preserve"
|
xml:space="preserve"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||||
id="namedview1"
|
id="namedview1"
|
||||||
pagecolor="#ffffff"
|
pagecolor="#ffffff"
|
||||||
bordercolor="#666666"
|
bordercolor="#666666"
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
|
@ -773,7 +773,6 @@ clients {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.sp-ace {
|
.sp-ace {
|
||||||
margin: 10px;
|
margin: 10px;
|
||||||
}
|
}
|
||||||
|
|
@ -1149,6 +1148,13 @@ input {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.icon-fit {
|
||||||
|
height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-15pct {
|
||||||
|
width: 15%;
|
||||||
|
}
|
||||||
|
|
||||||
.village {
|
.village {
|
||||||
background-color: $village_color;
|
background-color: $village_color;
|
||||||
|
|
@ -1762,12 +1768,6 @@ input {
|
||||||
font-size: 4vw;
|
font-size: 4vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
.information {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
padding-left: 5%;
|
|
||||||
padding-right: 5%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.yellow {
|
.yellow {
|
||||||
color: yellow;
|
color: yellow;
|
||||||
}
|
}
|
||||||
|
|
@ -1809,10 +1809,20 @@ input {
|
||||||
grid-template-columns: 3fr 2fr;
|
grid-template-columns: 3fr 2fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seer-check {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
zoom: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
.role-title-span {
|
.role-title-span {
|
||||||
display: grid;
|
// display: grid;
|
||||||
grid-template-columns: 1fr 100fr;
|
// grid-template-columns: 1fr 100fr;
|
||||||
max-height: 2rem;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
height: 2rem;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1820,11 +1830,18 @@ input {
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
padding-left: 5px;
|
padding-left: 5px;
|
||||||
padding-right: 10px;
|
padding-right: 10px;
|
||||||
|
width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
img {
|
img {
|
||||||
vertical-align: text-bottom;
|
vertical-align: text-bottom;
|
||||||
max-height: 2rem;
|
max-height: 2rem;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
@ -1834,6 +1851,7 @@ input {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
@ -1897,3 +1915,34 @@ input {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline-icons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 10px;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.big-screen {
|
||||||
|
--information-height: 75vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root:not(.big-screen) {
|
||||||
|
--information-height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.information {
|
||||||
|
font-size: 1.0rem;
|
||||||
|
padding-left: 5%;
|
||||||
|
padding-right: 5%;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
height: var(--information-height);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -831,7 +831,11 @@ impl GameExt for Game {
|
||||||
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
| ActionPrompt::Shapeshifter { .. } => panic!("expected a prompt with a mark"),
|
||||||
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
ActionPrompt::Arcanist { .. } => panic!("wrong call for arcanist"),
|
||||||
|
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::Bloodletter {
|
||||||
|
marked: Some(marked),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| ActionPrompt::LoneWolfKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
|
|
@ -907,7 +911,8 @@ impl GameExt for Game {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
..
|
..
|
||||||
} => assert_eq!(marked, mark, "marked character"),
|
} => assert_eq!(marked, mark, "marked character"),
|
||||||
ActionPrompt::Seer { marked: None, .. }
|
ActionPrompt::Bloodletter { marked: None, .. }
|
||||||
|
| ActionPrompt::Seer { marked: None, .. }
|
||||||
| ActionPrompt::Adjudicator { marked: None, .. }
|
| ActionPrompt::Adjudicator { marked: None, .. }
|
||||||
| ActionPrompt::PowerSeer { marked: None, .. }
|
| ActionPrompt::PowerSeer { marked: None, .. }
|
||||||
| ActionPrompt::Mortician { marked: None, .. }
|
| ActionPrompt::Mortician { marked: None, .. }
|
||||||
|
|
|
||||||
|
|
@ -188,6 +188,16 @@ pub fn Prompt(props: &ActionPromptProps) -> Html {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ActionPrompt::Bloodletter {
|
||||||
|
character_id,
|
||||||
|
living_players,
|
||||||
|
marked,
|
||||||
|
} => (
|
||||||
|
Some(character_id),
|
||||||
|
living_players,
|
||||||
|
marked.iter().cloned().collect::<Box<[CharacterId]>>(),
|
||||||
|
html! {{"bloodletter"}},
|
||||||
|
),
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::LoneWolfKill {
|
||||||
character_id,
|
character_id,
|
||||||
living_players,
|
living_players,
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,7 @@ pub fn AlignmentSpan(AlignmentSpanProps { alignment }: &AlignmentSpanProps) -> H
|
||||||
let class = match alignment {
|
let class = match alignment {
|
||||||
role::Alignment::Village => "village",
|
role::Alignment::Village => "village",
|
||||||
role::Alignment::Wolves => "wolves",
|
role::Alignment::Wolves => "wolves",
|
||||||
|
role::Alignment::Traitor => "traitor",
|
||||||
};
|
};
|
||||||
html! {
|
html! {
|
||||||
<span class={classes!("attribute-span", "faint", class)}>
|
<span class={classes!("attribute-span", "faint", class)}>
|
||||||
|
|
@ -81,16 +82,18 @@ pub fn CategorySpan(
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
#[derive(Debug, Clone, Copy, PartialEq, Properties)]
|
||||||
pub struct RoleTitleSpanProps {
|
pub struct RoleTitleSpanProps {
|
||||||
pub role: RoleTitle,
|
pub role: RoleTitle,
|
||||||
|
#[prop_or(IconType::List)]
|
||||||
|
pub icon_type: IconType,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
pub fn RoleTitleSpan(RoleTitleSpanProps { role }: &RoleTitleSpanProps) -> Html {
|
pub fn RoleTitleSpan(RoleTitleSpanProps { role, icon_type }: &RoleTitleSpanProps) -> Html {
|
||||||
let class = Into::<SetupRole>::into(*role).category().class();
|
let class = Into::<SetupRole>::into(*role).category().class();
|
||||||
let icon = role.icon().unwrap_or(role.alignment().icon());
|
let icon = role.icon().unwrap_or(role.alignment().icon());
|
||||||
let text = role.to_string().to_case(Case::Title);
|
let text = role.to_string().to_case(Case::Title);
|
||||||
html! {
|
html! {
|
||||||
<span class={classes!("role-title-span", "faint", class)}>
|
<span class={classes!("role-title-span", "faint", class)}>
|
||||||
<Icon source={icon} icon_type={IconType::List}/>
|
<Icon source={icon} icon_type={*icon_type}/>
|
||||||
<span>{text}</span>
|
<span>{text}</span>
|
||||||
</span>
|
</span>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,51 @@
|
||||||
|
// 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 werewolves_proto::aura;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconType, PartialAssociatedIcon};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
pub struct AuraProps {
|
||||||
|
pub aura: aura::Aura,
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn Aura(AuraProps { aura }: &AuraProps) -> Html {
|
||||||
|
let class = aura_class(aura);
|
||||||
|
let icon = aura.icon().map(|icon| {
|
||||||
|
html! {
|
||||||
|
<div>
|
||||||
|
<Icon source={icon} icon_type={IconType::Small}/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<span class={classes!("attribute-span", "faint", class)}>
|
||||||
|
{icon}
|
||||||
|
{aura.to_string()}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,8 @@ use werewolves_proto::{
|
||||||
};
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct SetupProps {
|
pub struct SetupProps {
|
||||||
pub settings: GameSettings,
|
pub settings: GameSettings,
|
||||||
|
|
@ -140,10 +142,7 @@ pub fn SetupCategory(
|
||||||
});
|
});
|
||||||
let killer_inactive = as_role.killer().killer().not().then_some("inactive");
|
let killer_inactive = as_role.killer().killer().not().then_some("inactive");
|
||||||
let powerful_inactive = as_role.powerful().powerful().not().then_some("inactive");
|
let powerful_inactive = as_role.powerful().powerful().not().then_some("inactive");
|
||||||
let alignment = match as_role.alignment() {
|
let alignment = as_role.alignment().icon();
|
||||||
Alignment::Village => "/img/village.svg",
|
|
||||||
Alignment::Wolves => "/img/wolf.svg",
|
|
||||||
};
|
|
||||||
html! {
|
html! {
|
||||||
<div class={classes!("slot")}>
|
<div class={classes!("slot")}>
|
||||||
{count}
|
{count}
|
||||||
|
|
@ -152,13 +151,16 @@ pub fn SetupCategory(
|
||||||
</div>
|
</div>
|
||||||
<div class="attributes">
|
<div class="attributes">
|
||||||
<div class="alignment">
|
<div class="alignment">
|
||||||
<img class="icon" src={alignment} alt={"alignment"}/>
|
// <img class="icon" src={alignment} alt={"alignment"}/>
|
||||||
|
<Icon source={alignment} icon_type={IconType::Small}/>
|
||||||
</div>
|
</div>
|
||||||
<div class={classes!("killer", killer_inactive)}>
|
<div class={classes!("killer", killer_inactive)}>
|
||||||
<img class="icon" src="/img/killer.svg" alt="killer icon"/>
|
<Icon source={IconSource::Killer} icon_type={IconType::Small}/>
|
||||||
|
// <img class="icon" src="/img/killer.svg" alt="killer icon"/>
|
||||||
</div>
|
</div>
|
||||||
<div class={classes!("poweful", powerful_inactive)}>
|
<div class={classes!("poweful", powerful_inactive)}>
|
||||||
<img class="icon" src="/img/powerful.svg" alt="powerful icon"/>
|
<Icon source={IconSource::Powerful} icon_type={IconType::Small}/>
|
||||||
|
// <img class="icon" src="/img/powerful.svg" alt="powerful icon"/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
// You should have received a copy of the GNU Affero General Public License
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
use werewolves_proto::{
|
use werewolves_proto::{
|
||||||
|
aura::Aura,
|
||||||
diedto::DiedToTitle,
|
diedto::DiedToTitle,
|
||||||
role::{Alignment, Killer, Powerful, RoleTitle},
|
role::{Alignment, Killer, Powerful, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
@ -76,6 +77,8 @@ decl_icon!(
|
||||||
NotEqual: "/img/not-equal.svg",
|
NotEqual: "/img/not-equal.svg",
|
||||||
Equal: "/img/equal.svg",
|
Equal: "/img/equal.svg",
|
||||||
RedX: "/img/red-x.svg",
|
RedX: "/img/red-x.svg",
|
||||||
|
Traitor: "/img/traitor.svg",
|
||||||
|
Bloodlet: "/img/bloodlet.svg",
|
||||||
);
|
);
|
||||||
|
|
||||||
impl IconSource {
|
impl IconSource {
|
||||||
|
|
@ -93,6 +96,8 @@ pub enum IconType {
|
||||||
List,
|
List,
|
||||||
Small,
|
Small,
|
||||||
RoleAdd,
|
RoleAdd,
|
||||||
|
Fit,
|
||||||
|
Icon15Pct,
|
||||||
Informational,
|
Informational,
|
||||||
#[default]
|
#[default]
|
||||||
RoleCheck,
|
RoleCheck,
|
||||||
|
|
@ -101,6 +106,8 @@ pub enum IconType {
|
||||||
impl IconType {
|
impl IconType {
|
||||||
pub const fn class(&self) -> &'static str {
|
pub const fn class(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
|
IconType::Icon15Pct => "icon-15pct",
|
||||||
|
IconType::Fit => "icon-fit",
|
||||||
IconType::List => "icon-in-list",
|
IconType::List => "icon-in-list",
|
||||||
IconType::Small => "icon",
|
IconType::Small => "icon",
|
||||||
IconType::RoleAdd => "icon-role-add",
|
IconType::RoleAdd => "icon-role-add",
|
||||||
|
|
@ -147,6 +154,7 @@ impl AssociatedIcon for Alignment {
|
||||||
match self {
|
match self {
|
||||||
Alignment::Village => IconSource::Village,
|
Alignment::Village => IconSource::Village,
|
||||||
Alignment::Wolves => IconSource::Wolves,
|
Alignment::Wolves => IconSource::Wolves,
|
||||||
|
Alignment::Traitor => IconSource::Traitor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -168,6 +176,7 @@ impl PartialAssociatedIcon for RoleTitle {
|
||||||
Some(match self {
|
Some(match self {
|
||||||
RoleTitle::AlphaWolf | RoleTitle::DireWolf => return None,
|
RoleTitle::AlphaWolf | RoleTitle::DireWolf => return None,
|
||||||
|
|
||||||
|
RoleTitle::Bloodletter => IconSource::Bloodlet,
|
||||||
RoleTitle::MasonLeader => IconSource::Mason,
|
RoleTitle::MasonLeader => IconSource::Mason,
|
||||||
RoleTitle::BlackKnight => IconSource::BlackKnight,
|
RoleTitle::BlackKnight => IconSource::BlackKnight,
|
||||||
RoleTitle::Insomniac => IconSource::Insomniac,
|
RoleTitle::Insomniac => IconSource::Insomniac,
|
||||||
|
|
@ -217,3 +226,14 @@ impl PartialAssociatedIcon for DiedToTitle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PartialAssociatedIcon for Aura {
|
||||||
|
fn icon(&self) -> Option<IconSource> {
|
||||||
|
match self {
|
||||||
|
Aura::Traitor => Some(IconSource::Traitor),
|
||||||
|
Aura::Drunk => todo!(),
|
||||||
|
Aura::Insane => todo!(),
|
||||||
|
Aura::Bloodlet { .. } => Some(IconSource::Bloodlet),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -177,6 +177,25 @@ struct StoryNightChangeProps {
|
||||||
#[function_component]
|
#[function_component]
|
||||||
fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
fn StoryNightChange(StoryNightChangeProps { change, characters }: &StoryNightChangeProps) -> Html {
|
||||||
match change {
|
match change {
|
||||||
|
NightChange::LostAura { character, aura } => characters.get(character).map(|character| html!{
|
||||||
|
<>
|
||||||
|
<CharacterCard faint=true char={character.clone()}/>
|
||||||
|
{"lost the"}
|
||||||
|
<crate::components::Aura aura={*aura}/>
|
||||||
|
{"aura"}
|
||||||
|
</>
|
||||||
|
}).unwrap_or_default(),
|
||||||
|
NightChange::ApplyAura { source, target, aura } => characters.get(source).and_then(|source| characters.get(target).map(|target| (source, target))).map(|(source, target)| {
|
||||||
|
html!{
|
||||||
|
<>
|
||||||
|
<CharacterCard faint=true char={target.clone()}/>
|
||||||
|
{"gained the"}
|
||||||
|
<crate::components::Aura aura={*aura}/>
|
||||||
|
{"aura from"}
|
||||||
|
<CharacterCard faint=true char={source.clone()}/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
}).unwrap_or_default(),
|
||||||
NightChange::RoleChange(character_id, role_title) => characters
|
NightChange::RoleChange(character_id, role_title) => characters
|
||||||
.get(character_id)
|
.get(character_id)
|
||||||
.map(|char| {
|
.map(|char| {
|
||||||
|
|
@ -434,6 +453,7 @@ fn StoryNightChoice(StoryNightChoiceProps { choice, characters }: &StoryNightCho
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
StoryActionPrompt::Bloodletter { character_id, chosen } => generate(character_id, chosen, "spilt wolf blood on"),
|
||||||
StoryActionPrompt::Vindicator {
|
StoryActionPrompt::Vindicator {
|
||||||
character_id,
|
character_id,
|
||||||
chosen,
|
chosen,
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,12 @@ impl RolePage for ActionPrompt {
|
||||||
<LoneWolfPage1 />
|
<LoneWolfPage1 />
|
||||||
</>
|
</>
|
||||||
}]),
|
}]),
|
||||||
|
ActionPrompt::Bloodletter { character_id, .. } => Rc::new([html! {
|
||||||
|
<>
|
||||||
|
{ident(character_id)}
|
||||||
|
<BloodletterPage1 />
|
||||||
|
</>
|
||||||
|
}]),
|
||||||
_ => Rc::new([]),
|
_ => Rc::new([]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,40 @@
|
||||||
|
// Copyright (C) 2025 Emilis Bliūdžius
|
||||||
|
//
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as
|
||||||
|
// published by the Free Software Foundation, either version 3 of the
|
||||||
|
// License, or (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
use crate::components::{Icon, IconSource, IconType};
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
pub fn BloodletterPage1() -> Html {
|
||||||
|
html! {
|
||||||
|
<div class="role-page">
|
||||||
|
<h1 class="wolves">{"BLOODLETTER"}</h1>
|
||||||
|
<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 "}
|
||||||
|
<Icon source={IconSource::Wolves} icon_type={IconType::Fit}/>
|
||||||
|
{" KILLER "}
|
||||||
|
<Icon source={IconSource::Killer} icon_type={IconType::Fit}/>
|
||||||
|
{" AND POWERFUL "}
|
||||||
|
<Icon source={IconSource::Powerful} icon_type={IconType::Fit}/>
|
||||||
|
{" IN CHECKS"}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -46,44 +46,81 @@ pub fn SeerResult(SeerResultProps { alignment }: &SeerResultProps) -> Html {
|
||||||
let text = match alignment {
|
let text = match alignment {
|
||||||
Alignment::Village => "VILLAGE",
|
Alignment::Village => "VILLAGE",
|
||||||
Alignment::Wolves => "WOLFPACK",
|
Alignment::Wolves => "WOLFPACK",
|
||||||
|
Alignment::Traitor => "TRAITOR",
|
||||||
|
};
|
||||||
|
let additional_info = match alignment {
|
||||||
|
Alignment::Village => html! {
|
||||||
|
<FalselyAppearsAs
|
||||||
|
roles={RoleTitle::falsely_appear_village()}
|
||||||
|
alignment_text={text}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
Alignment::Wolves => html! {
|
||||||
|
<FalselyAppearsAs
|
||||||
|
roles={RoleTitle::falsely_appear_wolf()}
|
||||||
|
alignment_text={text}
|
||||||
|
/>
|
||||||
|
},
|
||||||
|
Alignment::Traitor => html! {
|
||||||
|
<div class="bottom-bound">
|
||||||
|
<h1>
|
||||||
|
{"THIS PERSON IS A "}
|
||||||
|
<span class="yellow">{"TRAITOR"}</span>
|
||||||
|
</h1>
|
||||||
|
<h3>{"THEY WIN ALONGSIDE EVIL"}</h3>
|
||||||
|
</div>
|
||||||
|
},
|
||||||
};
|
};
|
||||||
let false_positives = match alignment {
|
|
||||||
Alignment::Village => RoleTitle::falsely_appear_village(),
|
|
||||||
Alignment::Wolves => RoleTitle::falsely_appear_wolf(),
|
|
||||||
}
|
|
||||||
.into_iter()
|
|
||||||
.map(|role| {
|
|
||||||
html! {
|
|
||||||
<RoleTitleSpan role={role}/>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect::<Html>();
|
|
||||||
html! {
|
html! {
|
||||||
<div class="role-page">
|
<div class="role-page">
|
||||||
<h1 class="intel">{"SEER"}</h1>
|
<h1 class="intel">{"SEER"}</h1>
|
||||||
<div class="information intel faint">
|
<div class="information intel faint">
|
||||||
<div class="two-column">
|
<div class="two-column">
|
||||||
<div>
|
<div class="seer-check">
|
||||||
<h2>{"YOUR TARGET APPEARS AS"}</h2>
|
<h2>{"YOUR TARGET APPEARS AS"}</h2>
|
||||||
<h4>
|
|
||||||
<Icon
|
<Icon
|
||||||
source={alignment.icon()}
|
source={alignment.icon()}
|
||||||
icon_type={IconType::Informational}
|
|
||||||
/>
|
/>
|
||||||
</h4>
|
|
||||||
<h3 class="yellow">{text}</h3>
|
<h3 class="yellow">{text}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="bottom-bound">
|
{additional_info}
|
||||||
<h5>
|
|
||||||
{"ROLES THAT FALSELY APPEAR AS "}
|
|
||||||
<span class="yellow">{text}</span>
|
|
||||||
</h5>
|
|
||||||
<div class="false-positives yellow">
|
|
||||||
{false_positives}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
|
struct FalselyAppearsAsProps {
|
||||||
|
roles: Box<[RoleTitle]>,
|
||||||
|
alignment_text: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component]
|
||||||
|
fn FalselyAppearsAs(
|
||||||
|
FalselyAppearsAsProps {
|
||||||
|
roles,
|
||||||
|
alignment_text,
|
||||||
|
}: &FalselyAppearsAsProps,
|
||||||
|
) -> Html {
|
||||||
|
let false_positives = roles
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.map(|role| {
|
||||||
|
html! {
|
||||||
|
<RoleTitleSpan role={role} icon_type={IconType::Icon15Pct}/>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect::<Html>();
|
||||||
|
html! {
|
||||||
|
<div class="bottom-bound">
|
||||||
|
<h5>
|
||||||
|
{"ROLES THAT FALSELY APPEAR AS "}
|
||||||
|
<span class="yellow">{alignment_text}</span>
|
||||||
|
</h5>
|
||||||
|
<div class="false-positives yellow">
|
||||||
|
{false_positives}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue