// 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 . use core::fmt::Display; use serde::{Deserialize, Serialize}; 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, PartialEq, Serialize, Deserialize, ChecksAs, Titles)] pub enum Aura { #[checks("assignable")] Traitor, #[checks("assignable")] #[checks("cleansible")] Drunk(DrunkBag), #[checks("assignable")] Insane, #[checks("cleansible")] Bloodlet { night: u8 }, #[checks("assignable")] Scapegoat, /// the first village power role that dies is passed to the scapegoat, /// but they remain the scapegoat #[checks("assignable")] RedeemableScapegoat, /// upon execution, passes the scapegoat trait onto a random living /// village-aligned player #[checks("assignable")] VindictiveScapegoat, /// the scapegoat trait is passed to the first player to select them at night #[checks("assignable")] SpitefulScapegoat, /// the first village role to act will create the scapegoat - thus always /// getting a first result of wolf/killer/powerful if seer/adjudicator/power seer #[checks("assignable")] InevitableScapegoat, #[checks("assignable")] Notorious, } 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", Aura::Scapegoat => "Scapegoat", Aura::RedeemableScapegoat => "Redeemable Scapegoat", Aura::VindictiveScapegoat => "Vindictive Scapegoat", Aura::SpitefulScapegoat => "Spiteful Scapegoat", Aura::InevitableScapegoat => "Inevitable Scapegoat", Aura::Notorious => "Notorious", }) } } impl Aura { pub const fn expired(&self, village: &Village) -> bool { match self { Aura::Scapegoat | Aura::RedeemableScapegoat | Aura::VindictiveScapegoat | Aura::SpitefulScapegoat | Aura::InevitableScapegoat | Aura::Notorious | 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); impl Auras { pub const fn new(auras: Vec) -> Self { Self(auras) } pub fn list(&self) -> &[Aura] { &self.0 } 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 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 { 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 { for aura in self.0.iter() { match aura { Aura::Traitor => return Some(Alignment::Traitor), Aura::Notorious | Aura::RedeemableScapegoat | Aura::VindictiveScapegoat | Aura::SpitefulScapegoat | Aura::Scapegoat | Aura::Bloodlet { .. } => return Some(Alignment::Wolves), Aura::InevitableScapegoat | Aura::Drunk(_) | Aura::Insane => {} } } None } /// returns [Some] if the auras override whether the player is a [Killer] pub fn overrides_killer(&self) -> Option { 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 { self.0 .iter() .any(|a| matches!(a, Aura::Bloodlet { .. })) .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 }, AuraTitle::Scapegoat => Aura::Scapegoat, AuraTitle::RedeemableScapegoat => Aura::RedeemableScapegoat, AuraTitle::VindictiveScapegoat => Aura::VindictiveScapegoat, AuraTitle::SpitefulScapegoat => Aura::SpitefulScapegoat, AuraTitle::InevitableScapegoat => Aura::InevitableScapegoat, AuraTitle::Notorious => Aura::Notorious, } } }