2025-11-05 20:24:51 +00:00
|
|
|
// 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/>.
|
2025-10-12 23:48:52 +01:00
|
|
|
use core::num::NonZeroU8;
|
|
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
|
diedto::DiedTo,
|
|
|
|
|
error::GameError,
|
|
|
|
|
game::{
|
|
|
|
|
GameTime, Village, kill,
|
|
|
|
|
night::changes::{ChangesLookup, NightChange},
|
|
|
|
|
story::DayDetail,
|
|
|
|
|
},
|
|
|
|
|
player::Protection,
|
2025-10-13 23:29:10 +01:00
|
|
|
role::{PYREMASTER_VILLAGER_KILLS_TO_DIE, PreviousGuardianAction, RoleBlock, RoleTitle},
|
2025-10-12 23:48:52 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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> {
|
|
|
|
|
let night = match self.time {
|
|
|
|
|
GameTime::Day { .. } => return Err(GameError::NotNight),
|
|
|
|
|
GameTime::Night { number } => number,
|
|
|
|
|
};
|
|
|
|
|
let mut changes = ChangesLookup::new(all_changes);
|
|
|
|
|
|
|
|
|
|
let mut new_village = self.clone();
|
|
|
|
|
for change in all_changes {
|
|
|
|
|
match change {
|
|
|
|
|
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()?.replace(*target);
|
|
|
|
|
if changes.killed(*source).is_some()
|
|
|
|
|
&& changes.protected(source).is_none()
|
|
|
|
|
&& changes.protected(target).is_none()
|
|
|
|
|
{
|
|
|
|
|
new_village
|
|
|
|
|
.character_by_id_mut(*target)
|
|
|
|
|
.unwrap()
|
|
|
|
|
.kill(DiedTo::Hunter {
|
|
|
|
|
killer: *source,
|
|
|
|
|
night: NonZeroU8::new(night).unwrap(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
NightChange::Kill { target, died_to } => {
|
|
|
|
|
if let Some(kill) =
|
|
|
|
|
kill::resolve_kill(&mut changes, *target, died_to, night, self)?
|
|
|
|
|
{
|
|
|
|
|
kill.apply_to_village(&mut new_village)?;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
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().replace(*target);
|
|
|
|
|
ss.kill(DiedTo::Shapeshift {
|
|
|
|
|
into: *target,
|
|
|
|
|
night: NonZeroU8::new(night).unwrap(),
|
|
|
|
|
});
|
|
|
|
|
// role change pushed in [apply_shapeshift]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()?
|
|
|
|
|
.replace(if *guarding {
|
|
|
|
|
PreviousGuardianAction::Guard(target)
|
|
|
|
|
} else {
|
|
|
|
|
PreviousGuardianAction::Protect(target)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2025-10-13 23:29:10 +01:00
|
|
|
NightChange::RoleBlock {
|
|
|
|
|
source,
|
|
|
|
|
target,
|
|
|
|
|
block_type: RoleBlock::Direwolf,
|
|
|
|
|
} => {
|
|
|
|
|
new_village
|
|
|
|
|
.character_by_id_mut(*source)?
|
|
|
|
|
.direwolf_mut()?
|
|
|
|
|
.replace(*target);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NightChange::Protection { .. } => {}
|
2025-10-12 23:48:52 +01:00
|
|
|
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,
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
} 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()? = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// black knights death
|
|
|
|
|
for knight in new_village
|
|
|
|
|
.characters_mut()
|
|
|
|
|
.into_iter()
|
|
|
|
|
.filter(|k| k.alive())
|
|
|
|
|
.filter(|k| k.black_knight().ok().and_then(|t| (*t).clone()).is_some())
|
|
|
|
|
.filter(|k| changes.killed(k.character_id()).is_none())
|
|
|
|
|
{
|
|
|
|
|
knight.black_knight_kill()?.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)
|
|
|
|
|
}
|
|
|
|
|
}
|