322 lines
13 KiB
Rust
322 lines
13 KiB
Rust
// 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 core::{num::NonZeroU8, ops::Not};
|
|
|
|
use crate::{
|
|
aura::Aura,
|
|
diedto::DiedTo,
|
|
error::GameError,
|
|
game::{
|
|
GameTime, Village,
|
|
kill::{self, KillOutcome},
|
|
night::changes::{ChangesLookup, NightChange},
|
|
story::DayDetail,
|
|
},
|
|
player::Protection,
|
|
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle},
|
|
};
|
|
|
|
type Result<T> = core::result::Result<T, GameError>;
|
|
|
|
impl Village {
|
|
pub fn with_day_changes(&self, day_changes: &[DayDetail]) -> Result<Self> {
|
|
let mut new_village = self.clone();
|
|
let executions = day_changes
|
|
.iter()
|
|
.map(|c| match c {
|
|
DayDetail::Execute(c) => *c,
|
|
})
|
|
.collect::<Box<[_]>>();
|
|
|
|
new_village.execute(&executions)?;
|
|
|
|
Ok(new_village)
|
|
}
|
|
pub fn with_night_changes(
|
|
&self,
|
|
all_changes: &[NightChange],
|
|
) -> Result<(Self, Box<[NightChange]>)> {
|
|
let night = match self.time {
|
|
GameTime::Day { .. } => return Err(GameError::NotNight),
|
|
GameTime::Night { number } => number,
|
|
};
|
|
let changes = ChangesLookup::new(all_changes);
|
|
|
|
let mut new_village = self.clone();
|
|
|
|
// recorded changes: changes sans failed kills, actions failed due to blocks, etc
|
|
let mut recorded_changes = all_changes.to_vec();
|
|
|
|
// dispose of the current drunk token for every drunk in the village
|
|
new_village
|
|
.characters_mut()
|
|
.iter_mut()
|
|
.filter_map(|c| {
|
|
c.auras_mut().iter_mut().find_map(|a| match a {
|
|
Aura::Drunk(bag) => Some(bag),
|
|
_ => None,
|
|
})
|
|
})
|
|
.for_each(|bag| {
|
|
// dispose of a token
|
|
let _ = bag.pull();
|
|
});
|
|
|
|
for change in all_changes {
|
|
match change {
|
|
NightChange::ApplyAura { target, aura, .. } => {
|
|
let target = new_village.character_by_id_mut(*target)?;
|
|
target.apply_aura(aura.clone());
|
|
}
|
|
NightChange::ElderReveal { elder } => {
|
|
new_village.character_by_id_mut(*elder)?.elder_reveal()
|
|
}
|
|
NightChange::RoleChange(character_id, role_title) => new_village
|
|
.character_by_id_mut(*character_id)?
|
|
.role_change(*role_title, GameTime::Night { number: night })?,
|
|
NightChange::HunterTarget { source, target } => {
|
|
let hunter_character = new_village.character_by_id_mut(*source).unwrap();
|
|
hunter_character.hunter_mut()?.target.replace(*target);
|
|
if changes
|
|
.died_to(hunter_character.character_id(), night, self)?
|
|
.is_some()
|
|
&& let Some(kill) = kill::resolve_kill(
|
|
&changes,
|
|
*target,
|
|
&DiedTo::Hunter {
|
|
killer: *source,
|
|
night: NonZeroU8::new(night)
|
|
.ok_or(GameError::CannotHappenOnNightZero)?,
|
|
},
|
|
night,
|
|
&new_village,
|
|
)?
|
|
{
|
|
kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?;
|
|
}
|
|
}
|
|
NightChange::Kill { target, died_to } => {
|
|
if let Some(kill) = kill::resolve_kill(&changes, *target, died_to, night, self)?
|
|
{
|
|
if let KillOutcome::Guarding {
|
|
guardian,
|
|
original_kill,
|
|
..
|
|
} = &kill
|
|
{
|
|
recorded_changes.retain(|c| c != change);
|
|
recorded_changes.push(NightChange::Kill {
|
|
target: *guardian,
|
|
died_to: original_kill.clone(),
|
|
});
|
|
}
|
|
kill.apply_to_village(&mut new_village, Some(&mut recorded_changes))?;
|
|
if let DiedTo::MapleWolf { source, .. } = died_to
|
|
&& let Ok(maple) = new_village.character_by_id_mut(*source)
|
|
{
|
|
*maple.maple_wolf_mut()?.last_kill_on_night = night;
|
|
}
|
|
} else {
|
|
recorded_changes.retain(|c| c != change);
|
|
}
|
|
match died_to {
|
|
DiedTo::Militia { killer, .. } => {
|
|
new_village
|
|
.character_by_id_mut(*killer)?
|
|
.militia_mut()?
|
|
.targeted
|
|
.replace(*target);
|
|
}
|
|
DiedTo::AlphaWolf { killer, .. } => {
|
|
new_village
|
|
.character_by_id_mut(*killer)?
|
|
.alpha_wolf_mut()?
|
|
.killed
|
|
.replace(*target);
|
|
}
|
|
_ => {}
|
|
};
|
|
}
|
|
NightChange::Shapeshift { source, into } => {
|
|
if let Some(target) = changes.wolf_pack_kill_target()
|
|
&& changes.protected(*target).is_none()
|
|
{
|
|
if *target != *into {
|
|
log::error!("shapeshift into({into}) != target({target})");
|
|
continue;
|
|
}
|
|
let ss = new_village.character_by_id_mut(*source).unwrap();
|
|
ss.shapeshifter_mut().unwrap().shifted_into.replace(*target);
|
|
ss.kill(DiedTo::Shapeshift {
|
|
into: *target,
|
|
night: NonZeroU8::new(night).unwrap(),
|
|
});
|
|
// role change pushed in [apply_shapeshift]
|
|
} else {
|
|
recorded_changes.retain(|c| c != change);
|
|
}
|
|
}
|
|
|
|
NightChange::Protection {
|
|
target,
|
|
protection: Protection::Guardian { source, guarding },
|
|
} => {
|
|
let target = new_village.character_by_id(*target)?.identity();
|
|
new_village
|
|
.character_by_id_mut(*source)?
|
|
.guardian_mut()?
|
|
.last_protected
|
|
.replace(if *guarding {
|
|
PreviousGuardianAction::Guard(target)
|
|
} else {
|
|
PreviousGuardianAction::Protect(target)
|
|
});
|
|
}
|
|
|
|
NightChange::RoleBlock {
|
|
source,
|
|
target,
|
|
block_type: RoleBlock::Direwolf,
|
|
} => {
|
|
new_village
|
|
.character_by_id_mut(*source)?
|
|
.dire_wolf_mut()?
|
|
.last_blocked
|
|
.replace(*target);
|
|
|
|
recorded_changes.retain(|c| {
|
|
matches!(c, NightChange::RoleBlock { .. })
|
|
|| c.target().map(|t| t == *target).unwrap_or_default().not()
|
|
});
|
|
}
|
|
|
|
NightChange::MasonRecruit {
|
|
mason_leader,
|
|
recruiting,
|
|
} => {
|
|
if new_village.character_by_id(*recruiting)?.is_wolf() {
|
|
new_village.character_by_id_mut(*mason_leader)?.kill(
|
|
DiedTo::MasonLeaderRecruitFail {
|
|
night,
|
|
tried_recruiting: *recruiting,
|
|
},
|
|
);
|
|
recorded_changes.retain(|c| c != change);
|
|
} else {
|
|
new_village
|
|
.character_by_id_mut(*mason_leader)?
|
|
.mason_leader_mut()?
|
|
.recruit(*recruiting);
|
|
}
|
|
}
|
|
NightChange::EmpathFoundScapegoat { empath, scapegoat } => {
|
|
new_village
|
|
.character_by_id_mut(*scapegoat)?
|
|
.role_change(RoleTitle::Villager, GameTime::Night { number: night })?;
|
|
*new_village
|
|
.character_by_id_mut(*empath)?
|
|
.empath_mut()?
|
|
.cursed = true;
|
|
}
|
|
NightChange::LostAura { character, aura } => {
|
|
new_village
|
|
.character_by_id_mut(*character)?
|
|
.remove_aura(aura.title());
|
|
}
|
|
NightChange::Protection { protection, target } => {
|
|
let target_ident = new_village.character_by_id(*target)?.identity();
|
|
match protection {
|
|
Protection::Guardian {
|
|
source,
|
|
guarding: true,
|
|
} => {
|
|
new_village
|
|
.character_by_id_mut(*source)?
|
|
.guardian_mut()?
|
|
.last_protected
|
|
.replace(PreviousGuardianAction::Guard(target_ident));
|
|
}
|
|
Protection::Guardian {
|
|
source,
|
|
guarding: false,
|
|
} => {
|
|
new_village
|
|
.character_by_id_mut(*source)?
|
|
.guardian_mut()?
|
|
.last_protected
|
|
.replace(PreviousGuardianAction::Protect(target_ident));
|
|
}
|
|
Protection::Protector { source } => {
|
|
new_village
|
|
.character_by_id_mut(*source)?
|
|
.protector_mut()?
|
|
.last_protected
|
|
.replace(*target);
|
|
}
|
|
Protection::Vindicator { .. } => {}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// black knights death
|
|
for knight in new_village
|
|
.characters_mut()
|
|
.into_iter()
|
|
.filter(|k| k.alive())
|
|
.filter(|k| {
|
|
k.black_knight_ref()
|
|
.ok()
|
|
.and_then(|t| (*t.attacked).clone())
|
|
.is_some()
|
|
})
|
|
.filter(|k| changes.killed(k.character_id()).is_none())
|
|
{
|
|
knight.black_knight_kill()?;
|
|
}
|
|
|
|
// pyre masters death
|
|
let village_dead = new_village
|
|
.characters()
|
|
.into_iter()
|
|
.filter(|c| c.is_village())
|
|
.filter_map(|c| c.died_to().cloned())
|
|
.filter_map(|c| c.killer().map(|k| (k, c)))
|
|
.collect::<Box<[_]>>();
|
|
for pyremaster in new_village
|
|
.living_characters_by_role_mut(RoleTitle::PyreMaster)
|
|
.into_iter()
|
|
.filter(|p| {
|
|
village_dead
|
|
.iter()
|
|
.filter(|(k, _)| *k == p.character_id())
|
|
.count()
|
|
>= PYREMASTER_VILLAGER_KILLS_TO_DIE.get() as usize
|
|
})
|
|
{
|
|
if let Some(night) = NonZeroU8::new(night) {
|
|
pyremaster.kill(DiedTo::PyreMasterLynchMob {
|
|
night,
|
|
source: pyremaster.character_id(),
|
|
});
|
|
}
|
|
}
|
|
|
|
if new_village.is_game_over().is_none() {
|
|
new_village.to_day()?;
|
|
}
|
|
Ok((new_village, recorded_changes.into_boxed_slice()))
|
|
}
|
|
}
|