// Copyright (C) 2025 Emilis Bliūdžius // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as // published by the Free Software Foundation, either version 3 of the // License, or (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use 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 = core::result::Result; impl Village { pub fn with_day_changes(&self, day_changes: &[DayDetail]) -> Result { let mut new_village = self.clone(); let executions = day_changes .iter() .map(|c| match c { DayDetail::Execute(c) => *c, }) .collect::>(); 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::>(); 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())) } }