2025-11-09 16:40:50 +00:00
|
|
|
// 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/>.
|
2025-11-12 20:05:40 +00:00
|
|
|
use core::fmt::Display;
|
|
|
|
|
|
2025-11-09 16:40:50 +00:00
|
|
|
use serde::{Deserialize, Serialize};
|
2025-11-12 20:05:40 +00:00
|
|
|
use werewolves_macros::{ChecksAs, Titles};
|
2025-11-09 16:40:50 +00:00
|
|
|
|
|
|
|
|
use crate::{
|
2025-11-12 20:05:40 +00:00
|
|
|
bag::DrunkBag,
|
2025-11-09 16:40:50 +00:00
|
|
|
game::{GameTime, Village},
|
|
|
|
|
role::{Alignment, Killer, Powerful},
|
|
|
|
|
team::Team,
|
|
|
|
|
};
|
|
|
|
|
const BLOODLET_DURATION_DAYS: u8 = 2;
|
|
|
|
|
|
2025-11-12 20:05:40 +00:00
|
|
|
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ChecksAs, Titles)]
|
2025-11-09 16:40:50 +00:00
|
|
|
pub enum Aura {
|
2025-11-12 20:05:40 +00:00
|
|
|
#[checks("assignable")]
|
2025-11-09 16:40:50 +00:00
|
|
|
Traitor,
|
2025-11-12 20:05:40 +00:00
|
|
|
#[checks("assignable")]
|
2025-11-09 16:40:50 +00:00
|
|
|
#[checks("cleansible")]
|
2025-11-12 20:05:40 +00:00
|
|
|
Drunk(DrunkBag),
|
|
|
|
|
#[checks("assignable")]
|
2025-11-09 16:40:50 +00:00
|
|
|
Insane,
|
|
|
|
|
#[checks("cleansible")]
|
2025-11-12 20:05:40 +00:00
|
|
|
Bloodlet { night: u8 },
|
2025-11-09 16:40:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Display for Aura {
|
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
|
f.write_str(match self {
|
|
|
|
|
Aura::Traitor => "Traitor",
|
2025-11-12 20:05:40 +00:00
|
|
|
Aura::Drunk(_) => "Drunk",
|
2025-11-09 16:40:50 +00:00
|
|
|
Aura::Insane => "Insane",
|
|
|
|
|
Aura::Bloodlet { .. } => "Bloodlet",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Aura {
|
|
|
|
|
pub const fn expired(&self, village: &Village) -> bool {
|
|
|
|
|
match self {
|
2025-11-12 20:05:40 +00:00
|
|
|
Aura::Traitor | Aura::Drunk(_) | Aura::Insane => false,
|
2025-11-09 16:40:50 +00:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-12 20:05:40 +00:00
|
|
|
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)
|
2025-11-09 16:40:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// 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),
|
2025-11-12 20:05:40 +00:00
|
|
|
Aura::Drunk(_) | Aura::Insane => {}
|
2025-11-09 16:40:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-11-12 20:05:40 +00:00
|
|
|
|
|
|
|
|
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 },
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|