From bdfd4034b98b65a054af413cc84a52797803d73c Mon Sep 17 00:00:00 2001 From: emilis Date: Sun, 9 Nov 2025 16:40:50 +0000 Subject: [PATCH] bloodletter: wip --- werewolves-proto/src/aura.rs | 53 +++++++++++++++++++ werewolves-proto/src/character.rs | 26 ++++++--- werewolves-proto/src/game/night.rs | 25 +++++++-- werewolves-proto/src/game/night/changes.rs | 6 +++ werewolves-proto/src/game/night/process.rs | 31 ++++++++--- .../src/game/settings/settings_role.rs | 20 +++++-- werewolves-proto/src/game/story.rs | 15 +++++- werewolves-proto/src/game/village.rs | 2 + werewolves-proto/src/game/village/apply.rs | 5 ++ werewolves-proto/src/lib.rs | 2 +- werewolves-proto/src/message/night.rs | 15 +++++- werewolves-proto/src/modifier.rs | 21 -------- werewolves-proto/src/role.rs | 9 +++- 13 files changed, 183 insertions(+), 47 deletions(-) create mode 100644 werewolves-proto/src/aura.rs delete mode 100644 werewolves-proto/src/modifier.rs diff --git a/werewolves-proto/src/aura.rs b/werewolves-proto/src/aura.rs new file mode 100644 index 0000000..2b19a54 --- /dev/null +++ b/werewolves-proto/src/aura.rs @@ -0,0 +1,53 @@ +// 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 serde::{Deserialize, Serialize}; + +use crate::game::{GameTime, Village}; +const BLOODLET_DURATION_DAYS: u8 = 2; + +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum Aura { + Drunk, + Insane, + Bloodlet { night: u8 }, +} + +impl Aura { + pub const fn expired(&self, village: &Village) -> bool { + match self { + Aura::Drunk | Aura::Insane => false, + Aura::Bloodlet { night } => match village.time() { + GameTime::Day { .. } => false, + GameTime::Night { number } => { + night.saturating_add(BLOODLET_DURATION_DAYS) >= number + } + }, + } + } + + pub const fn refreshes(&self, other: &Aura) -> bool { + match (self, other) { + (Aura::Bloodlet { .. }, Aura::Bloodlet { .. }) => true, + _ => false, + } + } + + pub fn refresh(&mut self, other: Aura) { + match (self, other) { + (Aura::Bloodlet { night }, Aura::Bloodlet { night: new_night }) => *night = new_night, + _ => {} + } + } +} diff --git a/werewolves-proto/src/character.rs b/werewolves-proto/src/character.rs index eb92566..3695ca1 100644 --- a/werewolves-proto/src/character.rs +++ b/werewolves-proto/src/character.rs @@ -22,11 +22,11 @@ use rand::seq::SliceRandom; use serde::{Deserialize, Serialize}; use crate::{ + aura::Aura, diedto::DiedTo, error::GameError, game::{GameTime, Village}, message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt}, - modifier::Modifier, player::{PlayerId, RoleChange}, role::{ Alignment, Killer, KillingWolfOrder, MAPLE_WOLF_ABSTAIN_LIMIT, Powerful, @@ -59,7 +59,7 @@ pub struct Character { player_id: PlayerId, identity: CharacterIdentity, role: Role, - modifier: Option, + auras: Vec, died_to: Option, role_changes: Vec, } @@ -76,19 +76,20 @@ impl Character { }, }: Identification, role: Role, + auras: Vec, ) -> Option { Some(Self { role, + auras, + player_id, + died_to: None, + role_changes: Vec::new(), identity: CharacterIdentity { character_id: CharacterId::new(), name, pronouns, number: number?, }, - player_id, - modifier: None, - died_to: None, - role_changes: Vec::new(), }) } @@ -298,6 +299,14 @@ impl Character { AsCharacter(char) } + pub fn apply_aura(&mut self, aura: Aura) { + if let Some(existing) = self.auras.iter_mut().find(|aura| aura.refreshes(&aura)) { + existing.refresh(aura); + } else { + self.auras.push(aura); + } + } + pub fn night_action_prompts(&self, village: &Village) -> Result> { if self.mason_leader().is_ok() { return self.mason_prompts(village); @@ -345,6 +354,11 @@ impl Character { return Ok(Box::new([])); } } + Role::Bloodletter => ActionPrompt::Bloodletter { + character_id: self.identity(), + living_players: village.living_villagers(), + marked: None, + }, Role::Seer => ActionPrompt::Seer { character_id: self.identity(), living_players: village.living_players_excluding(self.character_id()), diff --git a/werewolves-proto/src/game/night.rs b/werewolves-proto/src/game/night.rs index f0bc422..8290ea5 100644 --- a/werewolves-proto/src/game/night.rs +++ b/werewolves-proto/src/game/night.rs @@ -71,7 +71,11 @@ impl ActionPrompt { .. } => Some(Unless::TargetsBlocked(*marked1, *marked2)), - ActionPrompt::LoneWolfKill { + ActionPrompt::Bloodletter { + marked: Some(marked), + .. + } + | ActionPrompt::LoneWolfKill { marked: Some(marked), .. } @@ -148,7 +152,8 @@ impl ActionPrompt { .. } => Some(Unless::TargetBlocked(*marked)), - ActionPrompt::LoneWolfKill { marked: None, .. } + ActionPrompt::Bloodletter { .. } + | ActionPrompt::LoneWolfKill { marked: None, .. } | ActionPrompt::Seer { marked: None, .. } | ActionPrompt::Protector { marked: None, .. } | ActionPrompt::Gravedigger { marked: None, .. } @@ -1083,6 +1088,12 @@ impl Night { } } + /// returns the matching [Character] with the current night's aura changes + /// applied + fn character_with_current_auras(&self, id: CharacterId) -> Result { + todo!() + } + fn changes_from_actions(&self) -> Box<[NightChange]> { self.used_actions .iter() @@ -1109,7 +1120,12 @@ impl Night { .then(|| self.village.killing_wolf().map(|c| c.identity())) .flatten(), - ActionPrompt::Seer { + ActionPrompt::Bloodletter { + character_id, + marked: Some(marked), + .. + } + | ActionPrompt::Seer { character_id, marked: Some(marked), .. @@ -1200,7 +1216,8 @@ impl Night { .. } => (*marked == visit_char).then(|| character_id.clone()), - ActionPrompt::WolfPackKill { marked: None, .. } + ActionPrompt::Bloodletter { .. } + | ActionPrompt::WolfPackKill { marked: None, .. } | ActionPrompt::Arcanist { marked: _, .. } | ActionPrompt::LoneWolfKill { marked: None, .. } | ActionPrompt::Seer { marked: None, .. } diff --git a/werewolves-proto/src/game/night/changes.rs b/werewolves-proto/src/game/night/changes.rs index d4febd2..46807dc 100644 --- a/werewolves-proto/src/game/night/changes.rs +++ b/werewolves-proto/src/game/night/changes.rs @@ -18,6 +18,7 @@ use serde::{Deserialize, Serialize}; use werewolves_macros::Extract; use crate::{ + aura::Aura, character::CharacterId, diedto::DiedTo, player::Protection, @@ -59,6 +60,11 @@ pub enum NightChange { empath: CharacterId, scapegoat: CharacterId, }, + ApplyAura { + source: CharacterId, + target: CharacterId, + aura: Aura, + }, } pub struct ChangesLookup<'a>(&'a [NightChange], Vec); diff --git a/werewolves-proto/src/game/night/process.rs b/werewolves-proto/src/game/night/process.rs index d13dc7e..6afb33d 100644 --- a/werewolves-proto/src/game/night/process.rs +++ b/werewolves-proto/src/game/night/process.rs @@ -15,6 +15,7 @@ use core::num::NonZeroU8; use crate::{ + aura::Aura, diedto::DiedTo, error::GameError, game::night::{ @@ -108,6 +109,19 @@ impl Night { }; 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 { character_id, marked: Some(marked), @@ -143,7 +157,7 @@ impl Night { 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 { result: ActionResult::Seer(alignment), change: None, @@ -166,8 +180,8 @@ impl Night { marked: (Some(marked1), Some(marked2)), .. } => { - let same = self.village.character_by_id(*marked1)?.alignment() - == self.village.character_by_id(*marked2)?.alignment(); + let same = self.character_with_current_auras(*marked1)?.alignment() + == self.character_with_current_auras(*marked2)?.alignment(); Ok(ResponseOutcome::ActionComplete(ActionComplete { result: ActionResult::Arcanist(AlignmentEq::new(same)), @@ -178,7 +192,9 @@ impl Night { 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 { result: ActionResult::GraveDigger(dig_role), change: None, @@ -359,7 +375,7 @@ impl Night { .. } => Ok(ActionComplete { result: ActionResult::Adjudicator { - killer: self.village.character_by_id(*marked)?.killer(), + killer: self.character_with_current_auras(*marked)?.killer(), }, change: None, } @@ -369,7 +385,7 @@ impl Night { .. } => Ok(ActionComplete { result: ActionResult::PowerSeer { - powerful: self.village.character_by_id(*marked)?.powerful(), + powerful: self.character_with_current_auras(*marked)?.powerful(), }, change: None, } @@ -489,7 +505,8 @@ impl Night { } .into()), - ActionPrompt::Adjudicator { marked: None, .. } + ActionPrompt::Bloodletter { marked: None, .. } + | ActionPrompt::Adjudicator { marked: None, .. } | ActionPrompt::PowerSeer { marked: None, .. } | ActionPrompt::Mortician { marked: None, .. } | ActionPrompt::Beholder { marked: None, .. } diff --git a/werewolves-proto/src/game/settings/settings_role.rs b/werewolves-proto/src/game/settings/settings_role.rs index 053fed1..bf3f138 100644 --- a/werewolves-proto/src/game/settings/settings_role.rs +++ b/werewolves-proto/src/game/settings/settings_role.rs @@ -23,10 +23,10 @@ use uuid::Uuid; use werewolves_macros::{All, ChecksAs, Titles}; use crate::{ + aura::Aura, character::Character, error::GameError, message::Identification, - modifier::Modifier, player::PlayerId, role::{Role, RoleTitle}, }; @@ -127,6 +127,8 @@ pub enum SetupRole { Shapeshifter, #[checks(Category::Wolves)] LoneWolf, + #[checks(Category::Wolves)] + Bloodletter, #[checks(Category::Intel)] Adjudicator, @@ -157,6 +159,7 @@ pub enum SetupRole { impl SetupRoleTitle { pub fn into_role(self) -> Role { match self { + SetupRoleTitle::Bloodletter => Role::Bloodletter, SetupRoleTitle::Insomniac => Role::Insomniac, SetupRoleTitle::LoneWolf => Role::LoneWolf, SetupRoleTitle::Villager => Role::Villager, @@ -208,6 +211,7 @@ impl SetupRoleTitle { impl Display for SetupRole { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.write_str(match self { + SetupRole::Bloodletter => "Bloodletter", SetupRole::Insomniac => "Insomniac", SetupRole::LoneWolf => "Lone Wolf", SetupRole::Villager => "Villager", @@ -244,6 +248,7 @@ impl Display for SetupRole { impl SetupRole { pub fn into_role(self, roles_in_game: &[RoleTitle]) -> Result { Ok(match self { + Self::Bloodletter => Role::Bloodletter, SetupRole::Insomniac => Role::Insomniac, SetupRole::LoneWolf => Role::LoneWolf, SetupRole::Villager => Role::Villager, @@ -321,6 +326,7 @@ impl From for RoleTitle { impl From for SetupRole { fn from(value: RoleTitle) -> Self { match value { + RoleTitle::Bloodletter => SetupRole::Bloodletter, RoleTitle::Insomniac => SetupRole::Insomniac, RoleTitle::LoneWolf => SetupRole::LoneWolf, RoleTitle::Villager => SetupRole::Villager, @@ -373,7 +379,7 @@ impl SlotId { pub struct SetupSlot { pub slot_id: SlotId, pub role: SetupRole, - pub modifiers: Vec, + pub auras: Vec, pub assign_to: Option, pub created_order: u32, } @@ -384,7 +390,7 @@ impl SetupSlot { created_order, assign_to: None, role: title.into(), - modifiers: Vec::new(), + auras: Vec::new(), slot_id: SlotId::new(), } } @@ -394,8 +400,12 @@ impl SetupSlot { ident: Identification, roles_in_game: &[RoleTitle], ) -> Result { - Character::new(ident.clone(), self.role.into_role(roles_in_game)?) - .ok_or(GameError::PlayerNotAssignedNumber(ident.to_string())) + Character::new( + ident.clone(), + self.role.into_role(roles_in_game)?, + self.auras, + ) + .ok_or(GameError::PlayerNotAssignedNumber(ident.to_string())) } } diff --git a/werewolves-proto/src/game/story.rs b/werewolves-proto/src/game/story.rs index de210d5..dc6c5c0 100644 --- a/werewolves-proto/src/game/story.rs +++ b/werewolves-proto/src/game/story.rs @@ -199,11 +199,23 @@ pub enum StoryActionPrompt { Insomniac { character_id: CharacterId, }, + BloodLetter { + character_id: CharacterId, + chosen: CharacterId, + }, } impl StoryActionPrompt { pub fn new(prompt: ActionPrompt) -> Option { Some(match prompt { + ActionPrompt::Bloodletter { + character_id, + marked: Some(marked), + .. + } => Self::BloodLetter { + character_id: character_id.character_id, + chosen: marked, + }, ActionPrompt::Seer { character_id, marked: Some(marked), @@ -378,7 +390,8 @@ impl StoryActionPrompt { character_id: character_id.character_id, }, - ActionPrompt::Protector { .. } + ActionPrompt::Bloodletter { .. } + | ActionPrompt::Protector { .. } | ActionPrompt::Gravedigger { .. } | ActionPrompt::Hunter { .. } | ActionPrompt::Militia { .. } diff --git a/werewolves-proto/src/game/village.rs b/werewolves-proto/src/game/village.rs index fd9efc4..6427e0c 100644 --- a/werewolves-proto/src/game/village.rs +++ b/werewolves-proto/src/game/village.rs @@ -20,6 +20,7 @@ use serde::{Deserialize, Serialize}; use super::Result; use crate::{ + aura::Aura, character::{Character, CharacterId}, diedto::DiedTo, error::GameError, @@ -294,6 +295,7 @@ impl Village { impl RoleTitle { pub fn title_to_role_excl_apprentice(self) -> Role { match self { + RoleTitle::Bloodletter => Role::Bloodletter, RoleTitle::Insomniac => Role::Insomniac, RoleTitle::LoneWolf => Role::LoneWolf, RoleTitle::Villager => Role::Villager, diff --git a/werewolves-proto/src/game/village/apply.rs b/werewolves-proto/src/game/village/apply.rs index 71024e7..5b77563 100644 --- a/werewolves-proto/src/game/village/apply.rs +++ b/werewolves-proto/src/game/village/apply.rs @@ -15,6 +15,7 @@ use core::num::NonZeroU8; use crate::{ + aura::Aura, diedto::DiedTo, error::GameError, game::{ @@ -52,6 +53,10 @@ impl Village { let mut new_village = self.clone(); for change in all_changes { match change { + NightChange::ApplyAura { target, aura, .. } => { + let target = new_village.character_by_id_mut(*target)?; + target.apply_aura(*aura); + } NightChange::ElderReveal { elder } => { new_village.character_by_id_mut(*elder)?.elder_reveal() } diff --git a/werewolves-proto/src/lib.rs b/werewolves-proto/src/lib.rs index 28415f1..07ad5dd 100644 --- a/werewolves-proto/src/lib.rs +++ b/werewolves-proto/src/lib.rs @@ -14,6 +14,7 @@ // along with this program. If not, see . #![allow(clippy::new_without_default)] +pub mod aura; pub mod character; pub mod diedto; pub mod error; @@ -21,7 +22,6 @@ pub mod game; #[cfg(test)] mod game_test; pub mod message; -pub mod modifier; pub mod nonzero; pub mod player; pub mod role; diff --git a/werewolves-proto/src/message/night.rs b/werewolves-proto/src/message/night.rs index 1185c57..9ea2b5b 100644 --- a/werewolves-proto/src/message/night.rs +++ b/werewolves-proto/src/message/night.rs @@ -203,6 +203,12 @@ pub enum ActionPrompt { }, #[checks(ActionType::Insomniac)] Insomniac { character_id: CharacterIdentity }, + #[checks(ActionType::OtherWolf)] + Bloodletter { + character_id: CharacterIdentity, + living_players: Box<[CharacterIdentity]>, + marked: Option, + }, } impl ActionPrompt { @@ -230,6 +236,7 @@ impl ActionPrompt { | ActionPrompt::Empath { character_id, .. } | ActionPrompt::Vindicator { character_id, .. } | ActionPrompt::PyreMaster { character_id, .. } + | ActionPrompt::Bloodletter { character_id, .. } | ActionPrompt::DireWolf { character_id, .. } => Some(character_id.character_id), ActionPrompt::WolvesIntro { .. } @@ -241,6 +248,7 @@ impl ActionPrompt { pub(crate) fn matches_beholding(&self, target: CharacterId) -> bool { match self { ActionPrompt::Insomniac { character_id, .. } + | ActionPrompt::Bloodletter { character_id, .. } | ActionPrompt::Seer { character_id, .. } | ActionPrompt::Arcanist { character_id, .. } | ActionPrompt::Gravedigger { character_id, .. } @@ -344,7 +352,12 @@ impl ActionPrompt { Ok(prompt) } - ActionPrompt::LoneWolfKill { + ActionPrompt::Bloodletter { + living_players: targets, + marked, + .. + } + | ActionPrompt::LoneWolfKill { living_players: targets, marked, .. diff --git a/werewolves-proto/src/modifier.rs b/werewolves-proto/src/modifier.rs deleted file mode 100644 index ad2aae9..0000000 --- a/werewolves-proto/src/modifier.rs +++ /dev/null @@ -1,21 +0,0 @@ -// 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 serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] -pub enum Modifier { - Drunk, - Insane, -} diff --git a/werewolves-proto/src/role.rs b/werewolves-proto/src/role.rs index 4d576dd..b792a33 100644 --- a/werewolves-proto/src/role.rs +++ b/werewolves-proto/src/role.rs @@ -271,6 +271,11 @@ pub enum Role { #[checks(Powerful::Powerful)] #[checks("wolf")] LoneWolf, + #[checks(Alignment::Wolves)] + #[checks(Killer::Killer)] + #[checks(Powerful::Powerful)] + #[checks("wolf")] + Bloodletter, } impl Role { @@ -313,6 +318,7 @@ impl Role { Role::Werewolf => KillingWolfOrder::Werewolf, Role::AlphaWolf { .. } => KillingWolfOrder::AlphaWolf, + Role::BloodLetter { .. } => KillingWolfOrder::Bloodletter, Role::DireWolf { .. } => KillingWolfOrder::DireWolf, Role::Shapeshifter { .. } => KillingWolfOrder::Shapeshifter, Role::LoneWolf => KillingWolfOrder::LoneWolf, @@ -325,7 +331,7 @@ impl Role { | Role::Adjudicator | Role::DireWolf { .. } | Role::Arcanist - | Role::Seer => true, + | Role::Seer | Role::BloodLetter => true, Role::Insomniac // has to at least get one good night of sleep, right? | Role::Beholder @@ -400,6 +406,7 @@ impl Role { | Role::Militia { targeted: None } | Role::MapleWolf { .. } | Role::Guardian { .. } + | Role::BloodLetter { .. } | Role::Seer => true, Role::Apprentice(title) => village