bloodletter: wip

This commit is contained in:
emilis 2025-11-09 16:40:50 +00:00
parent ad29c3d59c
commit bdfd4034b9
No known key found for this signature in database
13 changed files with 183 additions and 47 deletions

View File

@ -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 <https://www.gnu.org/licenses/>.
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,
_ => {}
}
}
}

View File

@ -22,11 +22,11 @@ use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::{ use crate::{
aura::Aura,
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: Vec<Aura>,
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,
auras,
player_id,
died_to: None,
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(),
}) })
} }
@ -298,6 +299,14 @@ impl Character {
AsCharacter(char) 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<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 +354,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()),

View File

@ -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, .. }
@ -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<Character> {
todo!()
}
fn changes_from_actions(&self) -> Box<[NightChange]> { fn changes_from_actions(&self) -> Box<[NightChange]> {
self.used_actions self.used_actions
.iter() .iter()
@ -1109,7 +1120,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 +1216,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, .. }

View File

@ -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,11 @@ pub enum NightChange {
empath: CharacterId, empath: CharacterId,
scapegoat: CharacterId, scapegoat: CharacterId,
}, },
ApplyAura {
source: CharacterId,
target: CharacterId,
aura: Aura,
},
} }
pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>); pub struct ChangesLookup<'a>(&'a [NightChange], Vec<usize>);

View File

@ -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, .. }

View File

@ -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()))
} }
} }

View File

@ -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 { .. }

View File

@ -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,

View File

@ -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()
} }

View File

@ -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,6 @@ 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;

View File

@ -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,
.. ..

View File

@ -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 <https://www.gnu.org/licenses/>.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub enum Modifier {
Drunk,
Insane,
}

View File

@ -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