scapegoat auras
This commit is contained in:
parent
6f0a7bf9f4
commit
38bd545862
|
|
@ -36,6 +36,25 @@ pub enum Aura {
|
||||||
Insane,
|
Insane,
|
||||||
#[checks("cleansible")]
|
#[checks("cleansible")]
|
||||||
Bloodlet { night: u8 },
|
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 {
|
impl Display for Aura {
|
||||||
|
|
@ -45,6 +64,12 @@ impl Display for Aura {
|
||||||
Aura::Drunk(_) => "Drunk",
|
Aura::Drunk(_) => "Drunk",
|
||||||
Aura::Insane => "Insane",
|
Aura::Insane => "Insane",
|
||||||
Aura::Bloodlet { .. } => "Bloodlet",
|
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",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -52,7 +77,15 @@ impl Display for Aura {
|
||||||
impl Aura {
|
impl Aura {
|
||||||
pub const fn expired(&self, village: &Village) -> bool {
|
pub const fn expired(&self, village: &Village) -> bool {
|
||||||
match self {
|
match self {
|
||||||
Aura::Traitor | Aura::Drunk(_) | Aura::Insane => false,
|
Aura::Scapegoat
|
||||||
|
| Aura::RedeemableScapegoat
|
||||||
|
| Aura::VindictiveScapegoat
|
||||||
|
| Aura::SpitefulScapegoat
|
||||||
|
| Aura::InevitableScapegoat
|
||||||
|
| Aura::Notorious
|
||||||
|
| Aura::Traitor
|
||||||
|
| Aura::Drunk(_)
|
||||||
|
| Aura::Insane => false,
|
||||||
Aura::Bloodlet {
|
Aura::Bloodlet {
|
||||||
night: applied_night,
|
night: applied_night,
|
||||||
} => match village.time() {
|
} => match village.time() {
|
||||||
|
|
@ -138,8 +171,13 @@ impl Auras {
|
||||||
for aura in self.0.iter() {
|
for aura in self.0.iter() {
|
||||||
match aura {
|
match aura {
|
||||||
Aura::Traitor => return Some(Alignment::Traitor),
|
Aura::Traitor => return Some(Alignment::Traitor),
|
||||||
Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
Aura::Notorious
|
||||||
Aura::Drunk(_) | Aura::Insane => {}
|
| Aura::RedeemableScapegoat
|
||||||
|
| Aura::VindictiveScapegoat
|
||||||
|
| Aura::SpitefulScapegoat
|
||||||
|
| Aura::Scapegoat
|
||||||
|
| Aura::Bloodlet { .. } => return Some(Alignment::Wolves),
|
||||||
|
Aura::InevitableScapegoat | Aura::Drunk(_) | Aura::Insane => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
|
|
@ -169,6 +207,12 @@ impl AuraTitle {
|
||||||
AuraTitle::Drunk => Aura::Drunk(DrunkBag::default()),
|
AuraTitle::Drunk => Aura::Drunk(DrunkBag::default()),
|
||||||
AuraTitle::Insane => Aura::Insane,
|
AuraTitle::Insane => Aura::Insane,
|
||||||
AuraTitle::Bloodlet => Aura::Bloodlet { night: 0 },
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ use crate::{
|
||||||
aura::{Aura, AuraTitle, Auras},
|
aura::{Aura, AuraTitle, Auras},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{GameTime, Village},
|
game::{GameTime, Village, night::changes::NightChange},
|
||||||
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
message::{CharacterIdentity, Identification, PublicIdentity, night::ActionPrompt},
|
||||||
player::{PlayerId, RoleChange},
|
player::{PlayerId, RoleChange},
|
||||||
role::{
|
role::{
|
||||||
|
|
@ -316,6 +316,56 @@ impl Character {
|
||||||
self.auras.remove_aura(aura);
|
self.auras.remove_aura(aura);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn night_action_prompts_from_aura(
|
||||||
|
&self,
|
||||||
|
village: &Village,
|
||||||
|
) -> Result<Option<(ActionPrompt, Vec<NightChange>)>> {
|
||||||
|
if let Some(day) = NonZeroU8::new(match village.time() {
|
||||||
|
GameTime::Day { number: _ } => return Err(GameError::NotNight),
|
||||||
|
GameTime::Night { number } => number,
|
||||||
|
}) && let Some(aura) = self
|
||||||
|
.auras
|
||||||
|
.list()
|
||||||
|
.iter()
|
||||||
|
.find(|a| matches!(a, Aura::RedeemableScapegoat))
|
||||||
|
.cloned()
|
||||||
|
&& let Some(redeem_from) = village.characters().into_iter().find(|c| {
|
||||||
|
c.died_to()
|
||||||
|
.map(|died| match died.date_time() {
|
||||||
|
GameTime::Day { number } => number.get().saturating_add(1) == day.get(),
|
||||||
|
GameTime::Night { number } => number.saturating_add(1) == day.get(),
|
||||||
|
})
|
||||||
|
.unwrap_or_default()
|
||||||
|
&& c.scapegoat_can_redeem_into()
|
||||||
|
})
|
||||||
|
{
|
||||||
|
log::info!(
|
||||||
|
"scapegoat {} is redeeming into {}",
|
||||||
|
self.identity(),
|
||||||
|
redeem_from.role_title()
|
||||||
|
);
|
||||||
|
|
||||||
|
return Ok(Some((
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: self.identity(),
|
||||||
|
new_role: redeem_from.role_title(),
|
||||||
|
},
|
||||||
|
vec![
|
||||||
|
NightChange::LostAura {
|
||||||
|
aura,
|
||||||
|
character: self.character_id(),
|
||||||
|
},
|
||||||
|
NightChange::ApplyAura {
|
||||||
|
source: self.character_id(),
|
||||||
|
target: self.character_id(),
|
||||||
|
aura: Aura::Scapegoat,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
pub fn night_action_prompts(&self, village: &Village) -> Result<Box<[ActionPrompt]>> {
|
||||||
let mut prompts = Vec::new();
|
let mut prompts = Vec::new();
|
||||||
if let Role::MasonLeader { .. } = &self.role {
|
if let Role::MasonLeader { .. } = &self.role {
|
||||||
|
|
|
||||||
|
|
@ -105,4 +105,6 @@ pub enum GameError {
|
||||||
ShapeshiftingIsForShapeshifters,
|
ShapeshiftingIsForShapeshifters,
|
||||||
#[error("must select a target")]
|
#[error("must select a target")]
|
||||||
MustSelectTarget,
|
MustSelectTarget,
|
||||||
|
#[error("no current prompt in aura handling")]
|
||||||
|
NoCurrentPromptForAura,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::Result;
|
use super::Result;
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::{Aura, AuraTitle},
|
||||||
character::{Character, CharacterId},
|
character::{Character, CharacterId},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -37,7 +38,7 @@ use crate::{
|
||||||
|
|
||||||
enum BlockResolvedOutcome {
|
enum BlockResolvedOutcome {
|
||||||
PromptUpdate(ActionPrompt),
|
PromptUpdate(ActionPrompt),
|
||||||
ActionComplete(ActionResult, Option<NightChange>),
|
ActionComplete(ActionResult, Option<NightChange>, Vec<NightChange>),
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ResponseOutcome {
|
enum ResponseOutcome {
|
||||||
|
|
@ -48,6 +49,8 @@ enum ResponseOutcome {
|
||||||
struct ActionComplete {
|
struct ActionComplete {
|
||||||
pub result: ActionResult,
|
pub result: ActionResult,
|
||||||
pub change: Option<NightChange>,
|
pub change: Option<NightChange>,
|
||||||
|
/// hacking in to get scapegoat auras in faster for wednesday
|
||||||
|
pub secondary_changes: Vec<NightChange>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ActionComplete> for ResponseOutcome {
|
impl From<ActionComplete> for ResponseOutcome {
|
||||||
|
|
@ -193,8 +196,9 @@ impl ActionPrompt {
|
||||||
impl Default for ActionComplete {
|
impl Default for ActionComplete {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self {
|
Self {
|
||||||
result: ActionResult::GoBackToSleep,
|
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
|
result: ActionResult::GoBackToSleep,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -296,16 +300,24 @@ impl Night {
|
||||||
filter::no_filter
|
filter::no_filter
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut action_queue = village
|
let (mut action_queue, changes): (Vec<_>, Vec<_>) = village
|
||||||
.characters()
|
.characters()
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(filter)
|
.filter(filter)
|
||||||
.map(|c| c.night_action_prompts(&village))
|
.map(|c| {
|
||||||
|
c.night_action_prompts(&village).map(|acts| {
|
||||||
|
acts.into_iter()
|
||||||
|
.map(|prompt| (prompt, Vec::new()))
|
||||||
|
.chain(c.night_action_prompts_from_aura(&village).ok().flatten())
|
||||||
|
.collect::<Box<[_]>>()
|
||||||
|
})
|
||||||
|
})
|
||||||
.collect::<Result<Box<[_]>>>()?
|
.collect::<Result<Box<[_]>>>()?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flatten()
|
.flatten()
|
||||||
.chain(village.wolf_pack_kill())
|
.chain(village.wolf_pack_kill().map(|prompt| (prompt, Vec::new())))
|
||||||
.collect::<Vec<_>>();
|
.unzip();
|
||||||
|
let current_changes = changes.into_iter().flatten().collect();
|
||||||
action_queue.sort_by(night_sort_order);
|
action_queue.sort_by(night_sort_order);
|
||||||
|
|
||||||
let mut action_queue = VecDeque::from({
|
let mut action_queue = VecDeque::from({
|
||||||
|
|
@ -390,8 +402,8 @@ impl Night {
|
||||||
action_queue.push_front(prompt);
|
action_queue.push_front(prompt);
|
||||||
}
|
}
|
||||||
let night_state = NightState::Active {
|
let night_state = NightState::Active {
|
||||||
|
current_changes,
|
||||||
current_prompt: ActionPrompt::CoverOfDarkness,
|
current_prompt: ActionPrompt::CoverOfDarkness,
|
||||||
current_changes: Vec::new(),
|
|
||||||
current_result: CurrentResult::None,
|
current_result: CurrentResult::None,
|
||||||
current_page: 0,
|
current_page: 0,
|
||||||
};
|
};
|
||||||
|
|
@ -553,11 +565,106 @@ impl Night {
|
||||||
self.action_queue.iter().cloned().collect()
|
self.action_queue.iter().cloned().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn end_of_night_scapegoat_aura_changes(&self) -> Result<Box<[NightChange]>> {
|
||||||
|
let mut changes = Vec::new();
|
||||||
|
let source_target = self
|
||||||
|
.used_actions
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(prompt, _, _)| {
|
||||||
|
prompt
|
||||||
|
.character_id()
|
||||||
|
.and_then(|cid| prompt.marked().map(|marked| (cid, marked.0)))
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
for char in self.village.characters() {
|
||||||
|
if let Some(aura) = char
|
||||||
|
.auras()
|
||||||
|
.iter()
|
||||||
|
.find(|a| matches!(a.title(), AuraTitle::SpitefulScapegoat))
|
||||||
|
&& let Some(picked_by) = source_target.iter().find_map(|(source, target)| {
|
||||||
|
(*target == char.character_id()).then_some(*source)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
changes.push(NightChange::LostAura {
|
||||||
|
character: char.character_id(),
|
||||||
|
aura: aura.clone(),
|
||||||
|
});
|
||||||
|
changes.push(NightChange::ApplyAura {
|
||||||
|
source: char.character_id(),
|
||||||
|
target: picked_by,
|
||||||
|
aura: Aura::Scapegoat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if let Some(DiedTo::Execution { day }) = char.died_to()
|
||||||
|
&& let GameTime::Night { number: night } = self.village.time()
|
||||||
|
&& day.get() == night
|
||||||
|
&& let Some(aura) = char
|
||||||
|
.auras()
|
||||||
|
.iter()
|
||||||
|
.find(|a| matches!(a.title(), AuraTitle::VindictiveScapegoat))
|
||||||
|
{
|
||||||
|
changes.push(NightChange::LostAura {
|
||||||
|
character: char.character_id(),
|
||||||
|
aura: aura.clone(),
|
||||||
|
});
|
||||||
|
let chars = self
|
||||||
|
.village
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| {
|
||||||
|
c.character_id() != char.character_id()
|
||||||
|
&& c.alive()
|
||||||
|
&& c.is_village()
|
||||||
|
&& self // didn't die tonight
|
||||||
|
.died_to_tonight(c.character_id())
|
||||||
|
.ok()
|
||||||
|
.map(|c| c.is_none())
|
||||||
|
.unwrap_or_default()
|
||||||
|
})
|
||||||
|
.collect::<Box<[_]>>();
|
||||||
|
if !chars.is_empty() {
|
||||||
|
let new_scapegoat = chars[rand::random_range(0..chars.len())].character_id();
|
||||||
|
changes.push(NightChange::ApplyAura {
|
||||||
|
source: char.character_id(),
|
||||||
|
target: new_scapegoat,
|
||||||
|
aura: Aura::Scapegoat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(aura) = char
|
||||||
|
.auras()
|
||||||
|
.iter()
|
||||||
|
.find(|a| matches!(a.title(), AuraTitle::RedeemableScapegoat))
|
||||||
|
.cloned()
|
||||||
|
&& changes.iter().any(|c| match c {
|
||||||
|
NightChange::RoleChange(cid, _) => char.character_id() == *cid,
|
||||||
|
_ => false,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
// omg they changed. change the aura to scapegoat
|
||||||
|
changes.push(NightChange::LostAura {
|
||||||
|
aura,
|
||||||
|
character: char.character_id(),
|
||||||
|
});
|
||||||
|
changes.push(NightChange::ApplyAura {
|
||||||
|
source: char.character_id(),
|
||||||
|
target: char.character_id(),
|
||||||
|
aura: Aura::Scapegoat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(changes.into_boxed_slice())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn collect_changes(&self) -> Result<Box<[NightChange]>> {
|
pub fn collect_changes(&self) -> Result<Box<[NightChange]>> {
|
||||||
if !matches!(self.night_state, NightState::Complete) {
|
if !matches!(self.night_state, NightState::Complete) {
|
||||||
return Err(GameError::NotEndOfNight);
|
return Err(GameError::NotEndOfNight);
|
||||||
}
|
}
|
||||||
Ok(self.current_changes())
|
Ok(self
|
||||||
|
.current_changes()
|
||||||
|
.into_iter()
|
||||||
|
.chain(self.end_of_night_scapegoat_aura_changes()?)
|
||||||
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_changes(&self) -> Box<[NightChange]> {
|
pub fn current_changes(&self) -> Box<[NightChange]> {
|
||||||
|
|
@ -732,7 +839,7 @@ impl Night {
|
||||||
}
|
}
|
||||||
NightState::Complete => Err(GameError::NightOver),
|
NightState::Complete => Err(GameError::NightOver),
|
||||||
},
|
},
|
||||||
BlockResolvedOutcome::ActionComplete(mut result, Some(change)) => {
|
BlockResolvedOutcome::ActionComplete(mut result, Some(change), additional) => {
|
||||||
self.set_current_result(result.clone().into())?;
|
self.set_current_result(result.clone().into())?;
|
||||||
if let NightChange::Shapeshift { source, .. } = &change {
|
if let NightChange::Shapeshift { source, .. } = &change {
|
||||||
// needs to be resolved _now_ so that the target can be woken
|
// needs to be resolved _now_ so that the target can be woken
|
||||||
|
|
@ -766,9 +873,16 @@ impl Night {
|
||||||
} => current_changes.push(change),
|
} => current_changes.push(change),
|
||||||
NightState::Complete => return Err(GameError::InvalidMessageForGameState),
|
NightState::Complete => return Err(GameError::InvalidMessageForGameState),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for ch in additional {
|
||||||
|
self.append_change(ch)?;
|
||||||
|
}
|
||||||
Ok(ServerAction::Result(result))
|
Ok(ServerAction::Result(result))
|
||||||
}
|
}
|
||||||
BlockResolvedOutcome::ActionComplete(result, None) => {
|
BlockResolvedOutcome::ActionComplete(result, None, additional) => {
|
||||||
|
for ch in additional {
|
||||||
|
self.append_change(ch)?;
|
||||||
|
}
|
||||||
match &mut self.night_state {
|
match &mut self.night_state {
|
||||||
NightState::Active {
|
NightState::Active {
|
||||||
current_prompt: _,
|
current_prompt: _,
|
||||||
|
|
@ -804,11 +918,13 @@ impl Night {
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change,
|
change,
|
||||||
|
secondary_changes,
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
) => ResponseOutcome::ActionComplete(ActionComplete {
|
) => ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change,
|
change,
|
||||||
|
secondary_changes,
|
||||||
}),
|
}),
|
||||||
(act, _) => act,
|
(act, _) => act,
|
||||||
}
|
}
|
||||||
|
|
@ -847,6 +963,7 @@ impl Night {
|
||||||
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
return Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -860,23 +977,27 @@ impl Night {
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: Some(NightChange::Shapeshift { source, into }),
|
change: Some(NightChange::Shapeshift { source, into }),
|
||||||
|
secondary_changes,
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
_,
|
_,
|
||||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change: Some(NightChange::Shapeshift { source, into }),
|
change: Some(NightChange::Shapeshift { source, into }),
|
||||||
|
secondary_changes,
|
||||||
})),
|
})),
|
||||||
(
|
(
|
||||||
ResponseOutcome::ActionComplete(ActionComplete {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change,
|
change,
|
||||||
|
secondary_changes,
|
||||||
}),
|
}),
|
||||||
true,
|
true,
|
||||||
true,
|
true,
|
||||||
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
) => Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Continue,
|
result: ActionResult::Continue,
|
||||||
change,
|
change,
|
||||||
|
secondary_changes,
|
||||||
})),
|
})),
|
||||||
(outcome, _, _) => Ok(outcome),
|
(outcome, _, _) => Ok(outcome),
|
||||||
}
|
}
|
||||||
|
|
@ -888,7 +1009,14 @@ impl Night {
|
||||||
) -> Result<BlockResolvedOutcome> {
|
) -> Result<BlockResolvedOutcome> {
|
||||||
match self.received_response_consecutive_same_player_no_sleep(resp)? {
|
match self.received_response_consecutive_same_player_no_sleep(resp)? {
|
||||||
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
ResponseOutcome::PromptUpdate(update) => Ok(BlockResolvedOutcome::PromptUpdate(update)),
|
||||||
ResponseOutcome::ActionComplete(ActionComplete { result, change }) => {
|
ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
result,
|
||||||
|
change,
|
||||||
|
secondary_changes,
|
||||||
|
}) => {
|
||||||
|
if !secondary_changes.is_empty() {
|
||||||
|
log::info!("secondary changes: {secondary_changes:?}");
|
||||||
|
}
|
||||||
match self
|
match self
|
||||||
.current_prompt()
|
.current_prompt()
|
||||||
.ok_or(GameError::NightOver)?
|
.ok_or(GameError::NightOver)?
|
||||||
|
|
@ -907,9 +1035,14 @@ impl Night {
|
||||||
Ok(BlockResolvedOutcome::ActionComplete(
|
Ok(BlockResolvedOutcome::ActionComplete(
|
||||||
ActionResult::RoleBlocked,
|
ActionResult::RoleBlocked,
|
||||||
None,
|
None,
|
||||||
|
secondary_changes,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
Ok(BlockResolvedOutcome::ActionComplete(
|
||||||
|
result,
|
||||||
|
change,
|
||||||
|
secondary_changes,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
|
Some(Unless::TargetsBlocked(unless_blocked1, unless_blocked2)) => {
|
||||||
|
|
@ -924,12 +1057,21 @@ impl Night {
|
||||||
Ok(BlockResolvedOutcome::ActionComplete(
|
Ok(BlockResolvedOutcome::ActionComplete(
|
||||||
ActionResult::RoleBlocked,
|
ActionResult::RoleBlocked,
|
||||||
None,
|
None,
|
||||||
|
secondary_changes,
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
Ok(BlockResolvedOutcome::ActionComplete(result, change))
|
Ok(BlockResolvedOutcome::ActionComplete(
|
||||||
|
result,
|
||||||
|
change,
|
||||||
|
secondary_changes,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => Ok(BlockResolvedOutcome::ActionComplete(result, change)),
|
None => Ok(BlockResolvedOutcome::ActionComplete(
|
||||||
|
result,
|
||||||
|
change,
|
||||||
|
secondary_changes,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1022,9 +1164,21 @@ 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 and returns the [DiedTo] cause of death
|
/// state of the night and returns the [DiedTo] cause of death
|
||||||
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
fn died_to_tonight(&self, character_id: CharacterId) -> Result<Option<DiedTo>> {
|
||||||
|
ChangesLookup::new(&self.current_changes()).died_to(character_id, self.night, &self.village)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn currently_dying(&self) -> Box<[DiedTo]> {
|
||||||
let ch = self.current_changes();
|
let ch = self.current_changes();
|
||||||
let mut changes = ChangesLookup::new(&ch);
|
let changes: ChangesLookup<'_> = ChangesLookup::new(&ch);
|
||||||
changes.died_to(character_id, self.night, &self.village)
|
ch.iter()
|
||||||
|
.filter_map(|c| match c {
|
||||||
|
NightChange::Kill { target, .. } => changes
|
||||||
|
.died_to(*target, self.night, &self.village)
|
||||||
|
.ok()
|
||||||
|
.flatten(),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// returns the matching [Character] with the current night's aura changes
|
/// returns the matching [Character] with the current night's aura changes
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
use core::num::NonZeroU8;
|
use core::num::NonZeroU8;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::Aura,
|
aura::{Aura, AuraTitle},
|
||||||
bag::DrunkRoll,
|
bag::DrunkRoll,
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
|
|
@ -24,7 +24,9 @@ use crate::{
|
||||||
},
|
},
|
||||||
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
message::night::{ActionPrompt, ActionResponse, ActionResult},
|
||||||
player::Protection,
|
player::Protection,
|
||||||
role::{AlignmentEq, PreviousGuardianAction, RoleBlock, RoleTitle},
|
role::{
|
||||||
|
Alignment, AlignmentEq, Killer, Powerful, PreviousGuardianAction, RoleBlock, RoleTitle,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Result<T> = core::result::Result<T, GameError>;
|
type Result<T> = core::result::Result<T, GameError>;
|
||||||
|
|
@ -79,6 +81,7 @@ impl Night {
|
||||||
})
|
})
|
||||||
.ok_or(GameError::InvalidTarget)?,
|
.ok_or(GameError::InvalidTarget)?,
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
_ => Err(GameError::ShapeshiftingIsForShapeshifters),
|
_ => Err(GameError::ShapeshiftingIsForShapeshifters),
|
||||||
};
|
};
|
||||||
|
|
@ -90,6 +93,7 @@ impl Night {
|
||||||
self.get_visits_for(character_id.character_id),
|
self.get_visits_for(character_id.character_id),
|
||||||
),
|
),
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
|
@ -104,6 +108,7 @@ impl Night {
|
||||||
character_id.character_id,
|
character_id.character_id,
|
||||||
*new_role,
|
*new_role,
|
||||||
)),
|
)),
|
||||||
|
secondary_changes: vec![],
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -114,6 +119,7 @@ impl Night {
|
||||||
ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
|
ActionPrompt::TraitorIntro { .. } => Ok(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::Bloodletter {
|
ActionPrompt::Bloodletter {
|
||||||
|
|
@ -127,6 +133,7 @@ impl Night {
|
||||||
aura: Aura::Bloodlet { night: self.night },
|
aura: Aura::Bloodlet { night: self.night },
|
||||||
target: *marked,
|
target: *marked,
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::LoneWolfKill {
|
ActionPrompt::LoneWolfKill {
|
||||||
|
|
@ -142,6 +149,7 @@ impl Night {
|
||||||
night: self.night,
|
night: self.night,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::RoleChange { .. }
|
ActionPrompt::RoleChange { .. }
|
||||||
|
|
@ -150,6 +158,7 @@ impl Night {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::ElderReveal { character_id } => {
|
ActionPrompt::ElderReveal { character_id } => {
|
||||||
|
|
@ -158,6 +167,7 @@ impl Night {
|
||||||
change: Some(NightChange::ElderReveal {
|
change: Some(NightChange::ElderReveal {
|
||||||
elder: character_id.character_id,
|
elder: character_id.character_id,
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Seer {
|
ActionPrompt::Seer {
|
||||||
|
|
@ -168,6 +178,7 @@ impl Night {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Seer(alignment),
|
result: ActionResult::Seer(alignment),
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Protector {
|
ActionPrompt::Protector {
|
||||||
|
|
@ -182,6 +193,7 @@ impl Night {
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Arcanist {
|
ActionPrompt::Arcanist {
|
||||||
marked: (Some(marked1), Some(marked2)),
|
marked: (Some(marked1), Some(marked2)),
|
||||||
|
|
@ -193,6 +205,7 @@ impl Night {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
result: ActionResult::Arcanist(AlignmentEq::new(same)),
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Gravedigger {
|
ActionPrompt::Gravedigger {
|
||||||
|
|
@ -205,6 +218,7 @@ impl Night {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
result: ActionResult::GraveDigger(dig_role),
|
result: ActionResult::GraveDigger(dig_role),
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Hunter {
|
ActionPrompt::Hunter {
|
||||||
|
|
@ -217,6 +231,7 @@ impl Night {
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
target: *marked,
|
target: *marked,
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Militia {
|
ActionPrompt::Militia {
|
||||||
character_id,
|
character_id,
|
||||||
|
|
@ -232,6 +247,7 @@ impl Night {
|
||||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Militia { marked: None, .. } => {
|
ActionPrompt::Militia { marked: None, .. } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
|
@ -251,6 +267,7 @@ impl Night {
|
||||||
starves_if_fails: *nights_til_starvation == 0,
|
starves_if_fails: *nights_til_starvation == 0,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::MapleWolf { marked: None, .. } => {
|
ActionPrompt::MapleWolf { marked: None, .. } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
|
@ -269,6 +286,7 @@ impl Night {
|
||||||
guarding: false,
|
guarding: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
character_id,
|
character_id,
|
||||||
|
|
@ -288,6 +306,7 @@ impl Night {
|
||||||
guarding: false,
|
guarding: false,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::Guardian {
|
ActionPrompt::Guardian {
|
||||||
|
|
@ -304,6 +323,7 @@ impl Night {
|
||||||
guarding: prev_protect.character_id == *marked,
|
guarding: prev_protect.character_id == *marked,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::WolfPackKill {
|
ActionPrompt::WolfPackKill {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -322,6 +342,7 @@ impl Night {
|
||||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Shapeshifter { character_id } => {
|
ActionPrompt::Shapeshifter { character_id } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
Ok(ResponseOutcome::ActionComplete(ActionComplete {
|
||||||
|
|
@ -344,6 +365,7 @@ impl Night {
|
||||||
}),
|
}),
|
||||||
_ => return Err(GameError::ShapeshiftingIsForShapeshifters),
|
_ => return Err(GameError::ShapeshiftingIsForShapeshifters),
|
||||||
},
|
},
|
||||||
|
secondary_changes: vec![],
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
ActionPrompt::AlphaWolf {
|
ActionPrompt::AlphaWolf {
|
||||||
|
|
@ -360,6 +382,7 @@ impl Night {
|
||||||
.ok_or(GameError::CannotHappenOnNightZero)?,
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
ActionPrompt::AlphaWolf { marked: None, .. } => {
|
||||||
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
Ok(ResponseOutcome::ActionComplete(Default::default()))
|
||||||
|
|
@ -375,6 +398,7 @@ impl Night {
|
||||||
target: *marked,
|
target: *marked,
|
||||||
block_type: RoleBlock::Direwolf,
|
block_type: RoleBlock::Direwolf,
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
})),
|
})),
|
||||||
ActionPrompt::Adjudicator {
|
ActionPrompt::Adjudicator {
|
||||||
marked: Some(marked),
|
marked: Some(marked),
|
||||||
|
|
@ -384,6 +408,7 @@ impl Night {
|
||||||
killer: self.character_with_current_auras(*marked)?.killer(),
|
killer: self.character_with_current_auras(*marked)?.killer(),
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::PowerSeer {
|
ActionPrompt::PowerSeer {
|
||||||
|
|
@ -394,6 +419,7 @@ impl Night {
|
||||||
powerful: self.character_with_current_auras(*marked)?.powerful(),
|
powerful: self.character_with_current_auras(*marked)?.powerful(),
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::Mortician {
|
ActionPrompt::Mortician {
|
||||||
|
|
@ -408,6 +434,7 @@ impl Night {
|
||||||
.title(),
|
.title(),
|
||||||
),
|
),
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::Beholder {
|
ActionPrompt::Beholder {
|
||||||
|
|
@ -426,12 +453,14 @@ impl Night {
|
||||||
result.clone()
|
result.clone()
|
||||||
},
|
},
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into())
|
.into())
|
||||||
} else {
|
} else {
|
||||||
Ok(ActionComplete {
|
Ok(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
@ -439,6 +468,7 @@ impl Night {
|
||||||
ActionPrompt::MasonsWake { .. } => Ok(ActionComplete {
|
ActionPrompt::MasonsWake { .. } => Ok(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::MasonLeaderRecruit {
|
ActionPrompt::MasonLeaderRecruit {
|
||||||
|
|
@ -451,6 +481,7 @@ impl Night {
|
||||||
mason_leader: character_id.character_id,
|
mason_leader: character_id.character_id,
|
||||||
recruiting: *marked,
|
recruiting: *marked,
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::Empath {
|
ActionPrompt::Empath {
|
||||||
|
|
@ -459,7 +490,23 @@ impl Night {
|
||||||
..
|
..
|
||||||
} => {
|
} => {
|
||||||
let marked = self.village.character_by_id(*marked)?;
|
let marked = self.village.character_by_id(*marked)?;
|
||||||
let scapegoat = marked.role_title() == RoleTitle::Scapegoat;
|
let scapegoat_aura = marked
|
||||||
|
.auras()
|
||||||
|
.iter()
|
||||||
|
.find(|a| {
|
||||||
|
matches!(
|
||||||
|
a,
|
||||||
|
Aura::Scapegoat
|
||||||
|
| Aura::SpitefulScapegoat
|
||||||
|
| Aura::InevitableScapegoat
|
||||||
|
| Aura::RedeemableScapegoat
|
||||||
|
| Aura::VindictiveScapegoat
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.cloned();
|
||||||
|
let is_scapegoat_aura =
|
||||||
|
marked.role_title() != RoleTitle::Scapegoat && scapegoat_aura.is_some();
|
||||||
|
let scapegoat = marked.role_title() == RoleTitle::Scapegoat || is_scapegoat_aura;
|
||||||
|
|
||||||
Ok(ActionComplete {
|
Ok(ActionComplete {
|
||||||
result: ActionResult::Empath { scapegoat },
|
result: ActionResult::Empath { scapegoat },
|
||||||
|
|
@ -467,6 +514,21 @@ impl Night {
|
||||||
empath: character_id.character_id,
|
empath: character_id.character_id,
|
||||||
scapegoat: marked.character_id(),
|
scapegoat: marked.character_id(),
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: if let Some(aura) = scapegoat_aura {
|
||||||
|
vec![
|
||||||
|
NightChange::LostAura {
|
||||||
|
aura,
|
||||||
|
character: marked.character_id(),
|
||||||
|
},
|
||||||
|
NightChange::ApplyAura {
|
||||||
|
source: marked.character_id(),
|
||||||
|
target: character_id.character_id,
|
||||||
|
aura: Aura::Scapegoat,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
vec![]
|
||||||
|
},
|
||||||
}
|
}
|
||||||
.into())
|
.into())
|
||||||
}
|
}
|
||||||
|
|
@ -482,11 +544,13 @@ impl Night {
|
||||||
source: character_id.character_id,
|
source: character_id.character_id,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::Insomniac { .. } => Ok(ActionComplete {
|
ActionPrompt::Insomniac { .. } => Ok(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
ActionPrompt::PyreMaster {
|
ActionPrompt::PyreMaster {
|
||||||
|
|
@ -502,6 +566,7 @@ impl Night {
|
||||||
night,
|
night,
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
|
|
||||||
|
|
@ -509,6 +574,7 @@ impl Night {
|
||||||
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete {
|
| ActionPrompt::MasonLeaderRecruit { marked: None, .. } => Ok(ActionComplete {
|
||||||
result: ActionResult::GoBackToSleep,
|
result: ActionResult::GoBackToSleep,
|
||||||
change: None,
|
change: None,
|
||||||
|
secondary_changes: vec![],
|
||||||
}
|
}
|
||||||
.into()),
|
.into()),
|
||||||
|
|
||||||
|
|
@ -574,7 +640,13 @@ impl Night {
|
||||||
};
|
};
|
||||||
for aura in char.auras() {
|
for aura in char.auras() {
|
||||||
match aura {
|
match aura {
|
||||||
Aura::Traitor | Aura::Bloodlet { .. } => continue,
|
Aura::RedeemableScapegoat
|
||||||
|
| Aura::VindictiveScapegoat
|
||||||
|
| Aura::SpitefulScapegoat
|
||||||
|
| Aura::Notorious
|
||||||
|
| Aura::Scapegoat
|
||||||
|
| Aura::Traitor
|
||||||
|
| Aura::Bloodlet { .. } => continue,
|
||||||
Aura::Drunk(bag) => {
|
Aura::Drunk(bag) => {
|
||||||
if bag.peek() == DrunkRoll::Drunk {
|
if bag.peek() == DrunkRoll::Drunk {
|
||||||
act.change = None;
|
act.change = None;
|
||||||
|
|
@ -587,6 +659,57 @@ impl Night {
|
||||||
act.result = insane_result;
|
act.result = insane_result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Aura::InevitableScapegoat => {
|
||||||
|
match &act.result {
|
||||||
|
ActionResult::Adjudicator { .. } => {
|
||||||
|
act.result = ActionResult::Adjudicator {
|
||||||
|
killer: Killer::Killer,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ActionResult::PowerSeer { .. } => {
|
||||||
|
act.result = ActionResult::PowerSeer {
|
||||||
|
powerful: Powerful::Powerful,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ActionResult::Arcanist(_) => {
|
||||||
|
let (_, target2) = self
|
||||||
|
.current_prompt()
|
||||||
|
.ok_or(GameError::NoCurrentPromptForAura)?
|
||||||
|
.0
|
||||||
|
.marked()
|
||||||
|
.ok_or(GameError::MustSelectTarget)?;
|
||||||
|
let target2_align = self
|
||||||
|
.village
|
||||||
|
.character_by_id(target2.ok_or(GameError::MustSelectTarget)?)?
|
||||||
|
.alignment();
|
||||||
|
let target1_align = Alignment::Wolves;
|
||||||
|
act.result =
|
||||||
|
ActionResult::Arcanist(if target1_align == target2_align {
|
||||||
|
AlignmentEq::Same
|
||||||
|
} else {
|
||||||
|
AlignmentEq::Different
|
||||||
|
});
|
||||||
|
}
|
||||||
|
ActionResult::Seer(_) => act.result = ActionResult::Seer(Alignment::Wolves),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
if let Some((marked, _)) = self
|
||||||
|
.current_prompt()
|
||||||
|
.ok_or(GameError::NoCurrentPromptForAura)?
|
||||||
|
.0
|
||||||
|
.marked()
|
||||||
|
{
|
||||||
|
act.secondary_changes.push(NightChange::ApplyAura {
|
||||||
|
source: char.character_id(),
|
||||||
|
target: marked,
|
||||||
|
aura: Aura::Scapegoat,
|
||||||
|
});
|
||||||
|
act.secondary_changes.push(NightChange::LostAura {
|
||||||
|
character: char.character_id(),
|
||||||
|
aura: Aura::InevitableScapegoat,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(ResponseOutcome::ActionComplete(act))
|
Ok(ResponseOutcome::ActionComplete(act))
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,15 @@ impl SetupRoleTitle {
|
||||||
pub fn can_assign_aura(&self, aura: AuraTitle) -> bool {
|
pub fn can_assign_aura(&self, aura: AuraTitle) -> bool {
|
||||||
if self.into_role().title().wolf() {
|
if self.into_role().title().wolf() {
|
||||||
return match aura {
|
return match aura {
|
||||||
AuraTitle::Traitor | AuraTitle::Bloodlet | AuraTitle::Insane => false,
|
AuraTitle::Scapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat
|
||||||
|
| AuraTitle::Notorious
|
||||||
|
| AuraTitle::Traitor
|
||||||
|
| AuraTitle::Bloodlet
|
||||||
|
| AuraTitle::Insane => false,
|
||||||
AuraTitle::Drunk => !matches!(self, SetupRoleTitle::Werewolf),
|
AuraTitle::Drunk => !matches!(self, SetupRoleTitle::Werewolf),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -195,6 +203,22 @@ impl SetupRoleTitle {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AuraTitle::Bloodlet => false,
|
AuraTitle::Bloodlet => false,
|
||||||
|
AuraTitle::InevitableScapegoat => {
|
||||||
|
matches!(self.category(), Category::Intel)
|
||||||
|
&& !matches!(
|
||||||
|
self,
|
||||||
|
SetupRoleTitle::Mortician
|
||||||
|
| SetupRoleTitle::Gravedigger
|
||||||
|
| SetupRoleTitle::Empath
|
||||||
|
| SetupRoleTitle::Insomniac
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AuraTitle::Notorious => {
|
||||||
|
!matches!(self, SetupRoleTitle::Villager | SetupRoleTitle::Scapegoat)
|
||||||
|
}
|
||||||
|
AuraTitle::RedeemableScapegoat => matches!(self, SetupRoleTitle::Villager),
|
||||||
|
AuraTitle::VindictiveScapegoat | AuraTitle::SpitefulScapegoat => true,
|
||||||
|
AuraTitle::Scapegoat => matches!(self, SetupRoleTitle::Villager),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn into_role(self) -> Role {
|
pub fn into_role(self) -> Role {
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@
|
||||||
use core::{num::NonZeroU8, ops::Not};
|
use core::{num::NonZeroU8, ops::Not};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
aura::Aura,
|
aura::{Aura, AuraTitle},
|
||||||
diedto::DiedTo,
|
diedto::DiedTo,
|
||||||
error::GameError,
|
error::GameError,
|
||||||
game::{
|
game::{
|
||||||
|
|
@ -222,9 +222,15 @@ impl Village {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
NightChange::EmpathFoundScapegoat { empath, scapegoat } => {
|
NightChange::EmpathFoundScapegoat { empath, scapegoat } => {
|
||||||
new_village
|
{
|
||||||
.character_by_id_mut(*scapegoat)?
|
let scapegoat = new_village.character_by_id_mut(*scapegoat)?;
|
||||||
.role_change(RoleTitle::Villager, GameTime::Night { number: night })?;
|
if scapegoat.role_title() == RoleTitle::Scapegoat {
|
||||||
|
scapegoat.role_change(
|
||||||
|
RoleTitle::Villager,
|
||||||
|
GameTime::Night { number: night },
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
*new_village
|
*new_village
|
||||||
.character_by_id_mut(*empath)?
|
.character_by_id_mut(*empath)?
|
||||||
.empath_mut()?
|
.empath_mut()?
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
// 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/>.
|
||||||
|
|
||||||
|
mod scapegoat;
|
||||||
|
|
@ -0,0 +1,325 @@
|
||||||
|
// 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, AuraTitle},
|
||||||
|
game::{Game, GameSettings, SetupRole},
|
||||||
|
game_test::{
|
||||||
|
ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players, init_log,
|
||||||
|
},
|
||||||
|
message::night::{ActionPrompt, ActionPromptTitle},
|
||||||
|
player::RoleChange,
|
||||||
|
role::{Alignment, Killer, Role, RoleTitle},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn redeemable_scapegoat() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let seer = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
settings.add_and_assign(SetupRole::Seer, seer);
|
||||||
|
let mut scapegoat_slot = settings.add_and_assign(SetupRole::Villager, scapegoat);
|
||||||
|
scapegoat_slot.auras.push(AuraTitle::RedeemableScapegoat);
|
||||||
|
settings.update_slot(scapegoat_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
assert!(game.r#continue().seer().wolves());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(scapegoat).auras(),
|
||||||
|
&[Aura::RedeemableScapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.mark_for_execution(game.character_by_player_id(seer).character_id());
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(scapegoat).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(scapegoat).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.next(),
|
||||||
|
ActionPrompt::RoleChange {
|
||||||
|
character_id: game.character_by_player_id(scapegoat).identity(),
|
||||||
|
new_role: RoleTitle::Seer
|
||||||
|
}
|
||||||
|
);
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(wolf).character_id());
|
||||||
|
assert!(game.r#continue().seer().wolves());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
let scapegoat = game.character_by_player_id(scapegoat);
|
||||||
|
assert_eq!(
|
||||||
|
scapegoat.role_changes(),
|
||||||
|
&[RoleChange {
|
||||||
|
role: Role::Villager,
|
||||||
|
new_role: RoleTitle::Seer,
|
||||||
|
changed_on_night: 2,
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
assert_eq!(scapegoat.auras(), &[Aura::Scapegoat]);
|
||||||
|
assert_eq!(scapegoat.role(), &Role::Seer);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inevitable_scapegoat_targeting_black_knight() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let seer = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let black_knight = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
|
||||||
|
settings.add_and_assign(SetupRole::BlackKnight, black_knight);
|
||||||
|
let mut seer_slot = settings.add_and_assign(SetupRole::Seer, seer);
|
||||||
|
seer_slot.auras.push(AuraTitle::InevitableScapegoat);
|
||||||
|
settings.update_slot(seer_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(seer).auras(),
|
||||||
|
&[Aura::InevitableScapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(black_knight).character_id());
|
||||||
|
assert!(game.r#continue().seer().wolves());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(black_knight).auras(),
|
||||||
|
&[Aura::Scapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(seer).auras(), &[]);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
let next_target = game.village().characters().into_iter().last().unwrap();
|
||||||
|
game.mark(
|
||||||
|
game.living_villager_excl(next_target.player_id())
|
||||||
|
.character_id(),
|
||||||
|
);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(next_target.character_id());
|
||||||
|
assert!(game.r#continue().seer().village());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(seer).auras(), &[]);
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(next_target.player_id()).auras(),
|
||||||
|
&[]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vindictive_scapegoat() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
let mut scapegoat_slot = settings.add_and_assign(SetupRole::Villager, scapegoat);
|
||||||
|
scapegoat_slot.auras.push(AuraTitle::VindictiveScapegoat);
|
||||||
|
settings.update_slot(scapegoat_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
game.mark_for_execution(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(scapegoat).auras(), &[]);
|
||||||
|
assert!(
|
||||||
|
game.village()
|
||||||
|
.characters()
|
||||||
|
.into_iter()
|
||||||
|
.filter(|c| c.alive() && c.is_village())
|
||||||
|
.any(|c| c.auras() == [Aura::Scapegoat])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spiteful_scapegoat_chosen_by_adjudicator() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let seer = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let adjudicator = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
|
||||||
|
settings.add_and_assign(SetupRole::Adjudicator, adjudicator);
|
||||||
|
let mut seer_slot = settings.add_and_assign(SetupRole::Seer, seer);
|
||||||
|
seer_slot.auras.push(AuraTitle::SpitefulScapegoat);
|
||||||
|
settings.update_slot(seer_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(seer).auras(),
|
||||||
|
&[Aura::SpitefulScapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(adjudicator).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(seer).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::NotKiller);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(seer).auras(), &[]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(adjudicator).auras(),
|
||||||
|
&[Aura::Scapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(adjudicator).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(seer).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::NotKiller);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inevitable_scapegoat_chosen_by_adjudicator() {
|
||||||
|
init_log();
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let seer = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let adjudicator = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
|
||||||
|
settings.add_and_assign(SetupRole::Adjudicator, adjudicator);
|
||||||
|
let mut seer_slot = settings.add_and_assign(SetupRole::Seer, seer);
|
||||||
|
seer_slot.auras.push(AuraTitle::InevitableScapegoat);
|
||||||
|
settings.update_slot(seer_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(seer).auras(),
|
||||||
|
&[Aura::InevitableScapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(adjudicator).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(seer).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::NotKiller);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(seer).auras(), &[]);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(adjudicator).auras(),
|
||||||
|
&[Aura::Scapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark_villager();
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().seer();
|
||||||
|
game.mark(game.character_by_player_id(adjudicator).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(seer).character_id());
|
||||||
|
assert_eq!(game.r#continue().adjudicator(), Killer::NotKiller);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
}
|
||||||
|
|
@ -12,6 +12,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/>.
|
||||||
|
mod aura;
|
||||||
mod changes;
|
mod changes;
|
||||||
mod night_order;
|
mod night_order;
|
||||||
mod previous;
|
mod previous;
|
||||||
|
|
@ -38,21 +39,22 @@ use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
pub trait SettingsExt {
|
pub trait SettingsExt {
|
||||||
fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot));
|
fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot)) -> SetupSlot;
|
||||||
fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId);
|
fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId) -> SetupSlot;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SettingsExt for GameSettings {
|
impl SettingsExt for GameSettings {
|
||||||
fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot)) {
|
fn add_role(&mut self, role: SetupRole, modify: impl FnOnce(&mut SetupSlot)) -> SetupSlot {
|
||||||
let slot_id = self.new_slot(role.clone().into());
|
let slot_id = self.new_slot(role.clone().into());
|
||||||
let mut slot = self.get_slot_by_id(slot_id).unwrap().clone();
|
let mut slot = self.get_slot_by_id(slot_id).unwrap().clone();
|
||||||
slot.role = role;
|
slot.role = role;
|
||||||
modify(&mut slot);
|
modify(&mut slot);
|
||||||
self.update_slot(slot);
|
self.update_slot(slot.clone());
|
||||||
|
slot
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId) {
|
fn add_and_assign(&mut self, role: SetupRole, assignee: PlayerId) -> SetupSlot {
|
||||||
self.add_role(role, |slot| slot.assign_to = Some(assignee));
|
self.add_role(role, |slot| slot.assign_to = Some(assignee))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,11 @@
|
||||||
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
use pretty_assertions::{assert_eq, assert_ne, assert_str_eq};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
aura::{Aura, AuraTitle},
|
||||||
game::{Game, GameSettings, OrRandom, SetupRole},
|
game::{Game, GameSettings, OrRandom, SetupRole},
|
||||||
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
game_test::{ActionPromptTitleExt, ActionResultExt, GameExt, SettingsExt, gen_players},
|
||||||
message::night::{ActionPromptTitle, ActionResult},
|
message::night::{ActionPromptTitle, ActionResult},
|
||||||
role::Role,
|
role::{Role, RoleTitle},
|
||||||
};
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -151,3 +152,90 @@ fn takes_on_scapegoats_curse() {
|
||||||
|
|
||||||
game.next_expect_day();
|
game.next_expect_day();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn takes_on_scapegoat_aura_curse() {
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let empath = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Empath, empath);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
let mut scapegoat_slot = settings.add_and_assign(SetupRole::Villager, scapegoat);
|
||||||
|
scapegoat_slot.auras.push(AuraTitle::Scapegoat);
|
||||||
|
settings.update_slot(scapegoat_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(scapegoat).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().empath();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
assert_eq!(game.r#continue(), ActionResult::Empath { scapegoat: true });
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(empath).auras(),
|
||||||
|
&[Aura::Scapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(scapegoat).auras(), &[]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn takes_on_scapegoat_aura_curse_no_role_change() {
|
||||||
|
let players = gen_players(1..21);
|
||||||
|
let mut player_ids = players.iter().map(|p| p.player_id);
|
||||||
|
let empath = player_ids.next().unwrap();
|
||||||
|
let wolf = player_ids.next().unwrap();
|
||||||
|
let scapegoat = player_ids.next().unwrap();
|
||||||
|
let mut settings = GameSettings::empty();
|
||||||
|
settings.add_and_assign(SetupRole::Empath, empath);
|
||||||
|
settings.add_and_assign(SetupRole::Werewolf, wolf);
|
||||||
|
let mut scapegoat_slot = settings.add_and_assign(SetupRole::BlackKnight, scapegoat);
|
||||||
|
scapegoat_slot.auras.push(AuraTitle::Scapegoat);
|
||||||
|
settings.update_slot(scapegoat_slot);
|
||||||
|
|
||||||
|
settings.fill_remaining_slots_with_villagers(players.len());
|
||||||
|
let mut game = Game::new(&players, settings).unwrap();
|
||||||
|
game.r#continue().r#continue();
|
||||||
|
assert_eq!(game.next().title(), ActionPromptTitle::WolvesIntro);
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
game.execute().title().wolf_pack_kill();
|
||||||
|
game.mark(game.living_villager_excl(scapegoat).character_id());
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next().title().empath();
|
||||||
|
game.mark(game.character_by_player_id(scapegoat).character_id());
|
||||||
|
assert_eq!(game.r#continue(), ActionResult::Empath { scapegoat: true });
|
||||||
|
game.r#continue().sleep();
|
||||||
|
|
||||||
|
game.next_expect_day();
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(empath).auras(),
|
||||||
|
&[Aura::Scapegoat]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(game.character_by_player_id(scapegoat).auras(), &[]);
|
||||||
|
assert_eq!(game.character_by_player_id(scapegoat).role_changes(), &[]);
|
||||||
|
assert_eq!(
|
||||||
|
game.character_by_player_id(scapegoat).role_title(),
|
||||||
|
RoleTitle::BlackKnight
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1368,6 +1368,10 @@ input {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scapegoat {
|
||||||
|
@extend .wolves;
|
||||||
|
}
|
||||||
|
|
||||||
.assignments {
|
.assignments {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
|
|
@ -1420,6 +1424,11 @@ input {
|
||||||
gap: 5%;
|
gap: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.defensive {
|
||||||
|
background-color: color.change($defensive_color, $lightness: 30%);
|
||||||
|
border: 1px solid color.change($defensive_border, $lightness: 40%);
|
||||||
|
}
|
||||||
|
|
||||||
.category {
|
.category {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
width: 30%;
|
width: 30%;
|
||||||
|
|
@ -1444,6 +1453,12 @@ input {
|
||||||
|
|
||||||
width: 0;
|
width: 0;
|
||||||
height: 0;
|
height: 0;
|
||||||
|
|
||||||
|
.scapegoats {
|
||||||
|
color: rgba(255, 0, 255, 0.7);
|
||||||
|
font-size: 2em;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-list {
|
.category-list {
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,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::aura::{self, Aura, AuraTitle};
|
use werewolves_proto::aura::{Aura, AuraTitle};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -23,10 +23,16 @@ use crate::{
|
||||||
impl Class for AuraTitle {
|
impl Class for AuraTitle {
|
||||||
fn class(&self) -> Option<&'static str> {
|
fn class(&self) -> Option<&'static str> {
|
||||||
Some(match self {
|
Some(match self {
|
||||||
aura::AuraTitle::Traitor => "traitor",
|
AuraTitle::Traitor => "traitor",
|
||||||
aura::AuraTitle::Drunk => "drunk",
|
AuraTitle::Drunk => "drunk",
|
||||||
aura::AuraTitle::Insane => "insane",
|
AuraTitle::Insane => "insane",
|
||||||
aura::AuraTitle::Bloodlet => "wolves",
|
AuraTitle::Bloodlet => "wolves",
|
||||||
|
AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::Notorious
|
||||||
|
| AuraTitle::InevitableScapegoat
|
||||||
|
| AuraTitle::Scapegoat => "scapegoat",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -39,7 +45,7 @@ impl Class for Aura {
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Properties)]
|
#[derive(Debug, Clone, PartialEq, Properties)]
|
||||||
pub struct AuraSpanProps {
|
pub struct AuraSpanProps {
|
||||||
pub aura: aura::AuraTitle,
|
pub aura: AuraTitle,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,10 @@ use core::ops::Not;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
use convert_case::{Case, Casing};
|
use convert_case::{Case, Casing};
|
||||||
use werewolves_proto::game::{Category, GameSettings, SetupRole, SetupRoleTitle};
|
use werewolves_proto::{
|
||||||
|
aura::AuraTitle,
|
||||||
|
game::{Category, GameSettings, SetupRole, SetupRoleTitle},
|
||||||
|
};
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
use crate::components::{AssociatedIcon, Icon, IconSource, IconType};
|
||||||
|
|
@ -49,7 +52,7 @@ pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
|
||||||
|
|
||||||
let categories = categories
|
let categories = categories
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(cat, members)| {
|
.map(|(cat, mut members)| {
|
||||||
let hide = match cat {
|
let hide = match cat {
|
||||||
Category::Wolves => CategoryMode::ShowExactRoleCount,
|
Category::Wolves => CategoryMode::ShowExactRoleCount,
|
||||||
Category::Villager => CategoryMode::ShowExactRoleCount,
|
Category::Villager => CategoryMode::ShowExactRoleCount,
|
||||||
|
|
@ -58,11 +61,29 @@ pub fn Setup(SetupProps { settings }: &SetupProps) -> Html {
|
||||||
| Category::Offensive
|
| Category::Offensive
|
||||||
| Category::StartsAsVillager => CategoryMode::HideAllInfo,
|
| Category::StartsAsVillager => CategoryMode::HideAllInfo,
|
||||||
};
|
};
|
||||||
|
let scapegoat_auras = settings
|
||||||
|
.slots()
|
||||||
|
.iter()
|
||||||
|
.filter(|s| {
|
||||||
|
!matches!(s.role, SetupRole::Scapegoat { .. })
|
||||||
|
&& s.auras.iter().any(|a| {
|
||||||
|
matches!(
|
||||||
|
a,
|
||||||
|
AuraTitle::Scapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.count();
|
||||||
html! {
|
html! {
|
||||||
<SetupCategory
|
<SetupCategory
|
||||||
category={cat}
|
category={cat}
|
||||||
roles={members.into_boxed_slice()}
|
roles={members.into_boxed_slice()}
|
||||||
mode={hide}
|
mode={hide}
|
||||||
|
scapegoat_auras={scapegoat_auras}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
@ -102,6 +123,8 @@ pub struct SetupCategoryProps {
|
||||||
pub roles: Box<[SetupRole]>,
|
pub roles: Box<[SetupRole]>,
|
||||||
#[prop_or_default]
|
#[prop_or_default]
|
||||||
pub mode: CategoryMode,
|
pub mode: CategoryMode,
|
||||||
|
#[prop_or_default]
|
||||||
|
pub scapegoat_auras: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component]
|
#[function_component]
|
||||||
|
|
@ -110,6 +133,7 @@ pub fn SetupCategory(
|
||||||
category,
|
category,
|
||||||
roles,
|
roles,
|
||||||
mode,
|
mode,
|
||||||
|
scapegoat_auras,
|
||||||
}: &SetupCategoryProps,
|
}: &SetupCategoryProps,
|
||||||
) -> Html {
|
) -> Html {
|
||||||
let roles_count = match mode {
|
let roles_count = match mode {
|
||||||
|
|
@ -131,15 +155,37 @@ pub fn SetupCategory(
|
||||||
.map(|(r, count)| {
|
.map(|(r, count)| {
|
||||||
let as_role = r.into_role();
|
let as_role = r.into_role();
|
||||||
let wakes = as_role.wakes_night_zero().then_some("wakes");
|
let wakes = as_role.wakes_night_zero().then_some("wakes");
|
||||||
let count =
|
let count = if matches!(r, SetupRoleTitle::Scapegoat) {
|
||||||
|
(matches!(mode, CategoryMode::ShowExactRoleCount) && count + *scapegoat_auras > 0)
|
||||||
|
.then(|| {
|
||||||
|
let auras = if *scapegoat_auras != 0 {
|
||||||
|
html! {
|
||||||
|
<span class="scapegoats">{"?"}</span>
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
html! {}
|
||||||
|
};
|
||||||
|
html! {
|
||||||
|
<span class="count">{count + *scapegoat_auras}{auras}</span>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
(matches!(mode, CategoryMode::ShowExactRoleCount) && count > 0).then(|| {
|
(matches!(mode, CategoryMode::ShowExactRoleCount) && count > 0).then(|| {
|
||||||
html! {
|
html! {
|
||||||
<span class="count">{count}</span>
|
<span class="count">{count}</span>
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
};
|
||||||
|
// let count =
|
||||||
|
// (matches!(mode, CategoryMode::ShowExactRoleCount) && count > 0).then(|| {
|
||||||
|
// html! {
|
||||||
|
// <span class="count">{count}</span>
|
||||||
|
// }
|
||||||
|
// });
|
||||||
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 = as_role.alignment().icon();
|
let alignment = as_role.alignment().icon();
|
||||||
|
|
||||||
html! {
|
html! {
|
||||||
<div class={classes!("slot")}>
|
<div class={classes!("slot")}>
|
||||||
{count}
|
{count}
|
||||||
|
|
|
||||||
|
|
@ -237,6 +237,12 @@ impl PartialAssociatedIcon for AuraTitle {
|
||||||
AuraTitle::Drunk => Some(IconSource::Drunk),
|
AuraTitle::Drunk => Some(IconSource::Drunk),
|
||||||
AuraTitle::Insane => Some(IconSource::Insane),
|
AuraTitle::Insane => Some(IconSource::Insane),
|
||||||
AuraTitle::Bloodlet => Some(IconSource::Bloodlet),
|
AuraTitle::Bloodlet => Some(IconSource::Bloodlet),
|
||||||
|
AuraTitle::Scapegoat
|
||||||
|
| AuraTitle::RedeemableScapegoat
|
||||||
|
| AuraTitle::VindictiveScapegoat
|
||||||
|
| AuraTitle::SpitefulScapegoat
|
||||||
|
| AuraTitle::InevitableScapegoat
|
||||||
|
| AuraTitle::Notorious => Some(IconSource::Scapegoat),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -533,7 +533,7 @@ fn setup_options_for_slot(
|
||||||
>
|
>
|
||||||
<span class="aura-title">
|
<span class="aura-title">
|
||||||
{icon}
|
{icon}
|
||||||
<span class="title">{aura.to_string()}</span>
|
<span class="title">{aura.to_string().to_case(Case::Title)}</span>
|
||||||
<div class="icon inactive"/>
|
<div class="icon inactive"/>
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue