diff options
| author | nokoe <nokoe@mailbox.org> | 2025-12-16 04:45:47 +0100 |
|---|---|---|
| committer | nokoe <nokoe@mailbox.org> | 2025-12-16 04:45:47 +0100 |
| commit | 3d49838e934848bd66411ca55f5b58eae99cb728 (patch) | |
| tree | 80b95f93341ea8a97c23d95b07bec822aea767a5 /server/src | |
| parent | e99a71f4f5918e9e43c5f8ff01ce348021f925ea (diff) | |
| download | hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar.bz2 hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar.zst | |
capture the curry
Diffstat (limited to 'server/src')
| -rw-r--r-- | server/src/entity/ctf_minigame.rs | 296 | ||||
| -rw-r--r-- | server/src/entity/mod.rs | 10 |
2 files changed, 304 insertions, 2 deletions
diff --git a/server/src/entity/ctf_minigame.rs b/server/src/entity/ctf_minigame.rs new file mode 100644 index 00000000..18473fa5 --- /dev/null +++ b/server/src/entity/ctf_minigame.rs @@ -0,0 +1,296 @@ +/* + 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 <https://www.gnu.org/licenses/>. + +*/ +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<ItemIndex, TeamData>, +} +#[derive(Debug, Clone)] +struct TeamData { + spawn: IVec2, + players: HashSet<PlayerID>, + score: isize, +} + +impl TeamData { + fn clear_hands(&mut self, c: &mut EntityContext) -> Result<(), TrError> { + for pid in self.players.iter() { + let player = c + .game + .players + .get_mut(pid) + .ok_or(TrError::Plain("Player is missing".to_string()))?; + for (hand, item) in player + .items + .iter_mut() + .enumerate() + .filter(|(_, i)| i.is_some()) + { + *item = None; + c.packet_out.push_back(PacketC::SetItem { + location: ItemLocation::Player(*pid, hurrycurry_protocol::Hand(hand)), + item: None, + }); + } + } + Ok(()) + } + fn return_players(&mut self, c: &mut EntityContext) -> Result<(), TrError> { + for pid in self.players.iter() { + c.game + .players + .get_mut(&pid) + .ok_or(TrError::Plain("Player is missing".to_string()))? + .movement + .position = self.spawn.as_vec2(); + } + 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 new_round(&mut self, c: &mut EntityContext) -> Result<(), TrError> { + for (&pos, tile) in c.game.tiles.iter_mut() { + if tile.item.is_some() { + tile.item = None; + c.packet_out.push_back(PacketC::SetItem { + location: ItemLocation::Tile(pos), + item: None, + }); + } + } + for (item, team) in self.teams.iter_mut() { + team.clear_hands(c)?; + team.return_players(c)?; + c.game.set_item(team.spawn, Some(*item)); + } + Ok(()) + } + + fn get_team_mut(&mut self, pid: PlayerID) -> Result<(&ItemIndex, &mut TeamData), TrError> { + 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()))?) + } + + fn return_flag(&mut self, c: &mut EntityContext, team_idx: ItemIndex) -> Result<(), TrError> { + let team = self.teams.get_mut(&team_idx).unwrap(); + 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 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::<Vec<_>>()))); + 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(); + } + } + 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<ItemLocation>, + from: PlayerID, + ) -> Result<bool, TrError> { + 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, from_team) = self.get_team_mut(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) + { + from_team.score += 10; + self.send_table(&mut c)?; + self.new_round(&mut c)?; + } + Ok(true) + } + } else { + Ok(false) + } + } + ItemLocation::Player(to, _) => { + let (&from_team_idx, _) = self.get_team_mut(from)?; + let (&to_team_idx, to_team) = self.get_team_mut(to)?; + if from_team_idx != to_team_idx { + if c.game + .players + .get(&to) + .ok_or(TrError::Plain("Player is missing".to_string()))? + .items + .iter() + .flatten() + .find(|i| i.kind == from_team_idx) + .is_some() + { + to_team.clear_hands(&mut c)?; + self.return_flag(&mut c, from_team_idx)?; + } + self.return_player(&mut c, to)?; + Ok(true) + } else { + Ok(false) + } + } + } + } else { + Ok(false) + } + } +} diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index c10e1dbd..d10bee0c 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -19,6 +19,7 @@ mod book; pub mod bot; mod campaign; mod conveyor; +mod ctf_minigame; mod customers; mod demand_sink; mod environment_effect; @@ -32,8 +33,8 @@ pub mod tutorial; use crate::{ entity::{ - demand_sink::DemandSink, pedestrians::Pedestrians, player_portal_pair::PlayerPortalPair, - tag_minigame::TagMinigame, + ctf_minigame::CtfMinigame, demand_sink::DemandSink, pedestrians::Pedestrians, + player_portal_pair::PlayerPortalPair, tag_minigame::TagMinigame, }, scoreboard::ScoreboardStore, }; @@ -174,6 +175,11 @@ pub fn construct_entity(decl: &EntityDecl) -> DynEntity { speed: speed.unwrap_or(0.6), }), EntityDecl::DemandSink { pos } => Box::new(DemandSink { pos }), + EntityDecl::CtfMinigame { + spawnpoints, + item_indices, + .. + } => Box::new(CtfMinigame::new(&spawnpoints, &item_indices)), EntityDecl::PlayerPortalPair { a, b, |