/* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors 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, version 3 of the License only. 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 super::{Entity, EntityContext}; use anyhow::Result; use hurrycurry_locale::TrError; use hurrycurry_protocol::{ItemIndex, ItemLocation, Message, PacketC, PlayerID, glam::IVec2}; use std::collections::{HashMap, HashSet}; use std::fmt::Write; #[derive(Debug, Clone)] pub struct CtfMinigame { ready: bool, time: f32, teams: HashMap, } #[derive(Debug, Clone)] struct TeamData { spawn: IVec2, players: HashSet, score: isize, } impl TeamData { fn effect(&self, c: &mut EntityContext, name: &str) -> Result<(), TrError> { for pid in self.players.iter() { c.packet_out.push_back(PacketC::Effect2 { name: name.to_string(), location: ItemLocation::Player(*pid, hurrycurry_protocol::Hand(0)), }); } Ok(()) } } impl CtfMinigame { pub fn new(spawnpoints: &[IVec2], items: &[ItemIndex]) -> Self { Self { ready: false, teams: items .iter() .zip(spawnpoints.iter()) .map(|(&item, &spawn)| { ( item, TeamData { spawn, players: HashSet::new(), score: 0, }, ) }) .collect(), time: 0., } } fn setup(&mut self, c: &mut EntityContext) -> Result<(), TrError> { let mut players = c.game.players.iter_mut().map(|p| p); 'a: loop { for (_, data) in self.teams.iter_mut() { if let Some((&pid, player)) = players.next() { data.players.insert(pid); player.movement.position = data.spawn.as_vec2(); } else { break 'a; } } } for (i, team) in self.teams.iter() { c.game.set_item(team.spawn, Some(*i)); } self.ready = true; Ok(()) } fn get_team(&mut self, pid: PlayerID) -> Result { Ok(*self .teams .iter_mut() .find(|(_, d)| d.players.get(&pid).is_some()) .ok_or(TrError::Plain("Player is not in any team".to_string()))? .0) } fn return_flag(&mut self, c: &mut EntityContext, team_idx: ItemIndex) -> Result<(), TrError> { let team = self.teams.get_mut(&team_idx).unwrap(); for (&pos, tile) in c.game.tiles.iter_mut() { if tile.item.as_ref().is_some_and(|a| a.kind == team_idx) { tile.item = None; c.packet_out.push_back(PacketC::SetItem { location: ItemLocation::Tile(pos), item: None, }); } } for (pid, player) in c.game.players.iter_mut() { for (hand, item) in player .items .iter_mut() .enumerate() .filter(|(_, i)| i.as_ref().is_some_and(|a| a.kind == team_idx)) { *item = None; c.packet_out.push_back(PacketC::SetItem { location: ItemLocation::Player(*pid, hurrycurry_protocol::Hand(hand)), item: None, }); } } c.game.set_item(team.spawn, Some(team_idx)); Ok(()) } fn return_player(&mut self, c: &mut EntityContext, pid: PlayerID) -> Result<(), TrError> { let (_, team) = self .teams .iter() .find(|(_, d)| d.players.get(&pid).is_some()) .ok_or(TrError::Plain("Player is not in any team".to_string()))?; c.game .players .get_mut(&pid) .ok_or(TrError::Plain("Player is missing".to_string()))? .movement .position = team.spawn.as_vec2(); Ok(()) } fn player_flags( &mut self, c: &mut EntityContext, pid: PlayerID, ) -> Result, TrError> { Ok(c.game .players .get_mut(&pid) .ok_or(TrError::Plain("Player is missing".to_string()))? .items .iter() .flatten() .map(|a| a.kind) .filter(|a| self.teams.contains_key(a)) .collect::>()) } fn send_table(&self, c: &mut EntityContext) -> Result<(), TrError> { let mut o = String::new(); let ti = self .teams .iter() .map(|(i, d)| (i, (d.score, d.players.iter().collect::>()))); writeln!(o, "Capture the Curry! — Get the other team's item!").unwrap(); write!(o, "|").unwrap(); for (item, _) in ti.clone() { write!(o, "{:<20}|", c.game.data.item_name(*item)).unwrap(); } writeln!(o, "").unwrap(); write!(o, "|").unwrap(); for (_, (s, _)) in ti.clone() { write!(o, "{:<20}|", s).unwrap(); } writeln!(o, "").unwrap(); write!(o, "|").unwrap(); for _ in ti.clone() { write!(o, "{:-<20}|", "").unwrap(); } writeln!(o, "").unwrap(); let max = self .teams .iter() .map(|a| a.1.players.len()) .max() .ok_or(TrError::Plain( "There must be at least one team".to_string(), ))?; for i in 0..max { write!(o, "|").unwrap(); for (_, (_, team)) in ti.clone() { if let Some(p) = team.get(i).and_then(|&i| c.game.players.get(i)) { write!(o, "{:<20}|", p.name).unwrap(); } else { write!(o, "{:<20}|", "").unwrap(); } } writeln!(o, "").unwrap(); } c.packet_out.push_back(PacketC::ServerMessage { error: false, message: Message::Text(o), }); Ok(()) } } impl Entity for CtfMinigame { fn tick(&mut self, mut c: EntityContext) -> Result<(), TrError> { if !self.ready { self.setup(&mut c)?; } for (_, team) in self.teams.iter_mut() { team.players.retain(|pid| c.game.players.get(pid).is_some()); } self.time += c.dt; while self.time > 0. { self.send_table(&mut c).unwrap(); self.time -= 1. } Ok(()) } fn interact( &mut self, mut c: EntityContext<'_>, target: Option, from: PlayerID, ) -> Result { if let Some(target) = target { match target { ItemLocation::Tile(pos) => { if let Some(to_team_idx) = self .teams .iter() .find(|(_, d)| d.spawn == pos) .map(|a| *a.0) { let from_team_idx = self.get_team(from)?; if to_team_idx != from_team_idx { Ok(false) } else { if c.game .players .get(&from) .unwrap() .items .iter() .flatten() .find(|i| i.kind != from_team_idx) .is_some() && c.game .tiles .get(&pos) .and_then(|a| a.item.as_ref().map(|a| a.kind)) .is_some_and(|a| a == from_team_idx) { for i in self.player_flags(&mut c, from)? { self.teams.get(&i).unwrap().effect(&mut c, "angry")?; self.return_flag(&mut c, i)?; self.teams.get_mut(&from_team_idx).unwrap().score += 10; } self.teams .get(&from_team_idx) .unwrap() .effect(&mut c, "satisfied")?; self.send_table(&mut c)?; } Ok(true) } } else { Ok(false) } } ItemLocation::Player(to, _) => { let from_team_idx = self.get_team(from)?; let to_team_idx = self.get_team(to)?; if from_team_idx != to_team_idx { for i in self.player_flags(&mut c, to)? { self.return_flag(&mut c, i)?; self.teams.get_mut(&from_team_idx).unwrap().score += 3; } self.send_table(&mut c)?; self.return_player(&mut c, to)?; Ok(true) } else { Ok(false) } } } } else { Ok(false) } } }