// 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; use crate::{ character::CharacterId, diedto::DiedTo, error::GameError, game::night::{CurrentResult, Night, NightState, changes::NightChange}, message::night::{ActionPrompt, ActionResult}, role::RoleBlock, }; use super::Result; impl Night { #[allow(clippy::should_implement_trait)] pub fn next(&mut self) -> Result<()> { self.retroactive_role_blocks()?; self.next_state_process_maple_starving()?; match &self.night_state { NightState::Active { current_prompt, current_result: CurrentResult::Result(ActionResult::Continue), current_changes, .. } => { self.used_actions.push(( current_prompt.clone(), ActionResult::Continue, current_changes.clone(), )); } NightState::Active { current_prompt, current_result: CurrentResult::Result(ActionResult::GoBackToSleep), current_changes, .. } => { self.used_actions.push(( current_prompt.clone(), ActionResult::GoBackToSleep, current_changes.clone(), )); } NightState::Active { current_result: CurrentResult::Result(_), .. } => { // needs Continue, not Next return Err(GameError::AwaitingResponse); } NightState::Active { current_prompt, current_result: CurrentResult::GoBackToSleepAfterShown { result_with_data }, current_changes, .. } => { self.used_actions.push(( current_prompt.clone(), result_with_data.clone(), current_changes.clone(), )); } NightState::Active { current_prompt: _, current_result: CurrentResult::None, .. } => return Err(GameError::AwaitingResponse), NightState::Complete => return Err(GameError::NightOver), } if let Some(prompt) = self.action_queue.pop_front() { if let ActionPrompt::Insomniac { character_id } = &prompt && self.get_visits_for(character_id.character_id).is_empty() { // skip! self.used_actions.pop(); // it will be re-added return self.next(); } self.night_state = NightState::Active { current_prompt: prompt, current_result: CurrentResult::None, current_changes: Vec::new(), current_page: 0, }; } else { self.night_state = NightState::Complete; } Ok(()) } fn next_state_process_maple_starving(&mut self) -> Result<()> { let (maple_id, target) = match self.current_prompt() { Some(( ActionPrompt::MapleWolf { character_id, kill_or_die, marked, .. }, _, )) => { if *kill_or_die { (character_id.character_id, *marked) } else { return Ok(()); } } Some(_) | None => return Ok(()), }; let starve_change = if let Some(night) = NonZeroU8::new(self.night) { NightChange::Kill { target: maple_id, died_to: DiedTo::MapleWolfStarved { night }, } } else { return Ok(()); }; let Some(target) = target else { return self.append_change(starve_change); }; match self.died_to_tonight(target)? { Some(DiedTo::MapleWolf { source, .. }) => { if source != maple_id { self.append_change(starve_change)?; } } Some(_) | None => { self.append_change(starve_change)?; } } Ok(()) } fn retroactive_role_blocks(&mut self) -> Result<()> { let blocks = match &self.night_state { NightState::Active { current_changes, .. } => current_changes .iter() .filter_map(|c| match c { NightChange::RoleBlock { target, block_type, .. } => Some((*target, *block_type)), _ => None, }) .collect::>(), NightState::Complete => return Err(GameError::NightOver), }; for (target, block_type) in blocks { match block_type { RoleBlock::Direwolf => self.apply_direwolf_block_retroactively(target), } } Ok(()) } fn apply_direwolf_block_retroactively(&mut self, target: CharacterId) { self.used_actions .iter_mut() .filter_map(|(prompt, res, changes)| match prompt.marked() { Some((marked, None)) => (marked == target).then_some((res, changes)), Some((marked1, Some(marked2))) => { (marked1 == target || marked2 == target).then_some((res, changes)) } None => None, }) .for_each(|(result, changes)| { changes.clear(); *result = ActionResult::RoleBlocked; }); } }