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 | |
| parent | e99a71f4f5918e9e43c5f8ff01ce348021f925ea (diff) | |
| download | hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar.bz2 hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar.zst | |
capture the curry
| -rw-r--r-- | data/index.yaml | 1 | ||||
| -rw-r--r-- | data/maps/ctf.yaml | 61 | ||||
| -rw-r--r-- | server/data/src/entities.rs | 6 | ||||
| -rw-r--r-- | server/data/src/lib.rs | 7 | ||||
| -rw-r--r-- | server/data/src/registry.rs | 6 | ||||
| -rw-r--r-- | server/src/entity/ctf_minigame.rs | 296 | ||||
| -rw-r--r-- | server/src/entity/mod.rs | 10 |
7 files changed, 385 insertions, 2 deletions
diff --git a/data/index.yaml b/data/index.yaml index 52c38e26..947f6b5f 100644 --- a/data/index.yaml +++ b/data/index.yaml @@ -30,6 +30,7 @@ maps: bbq: { name: "BBQ", players: 2, difficulty: 1 } burgers_inc: { name: "Burgers, Inc.", players: 2, difficulty: 2 } bus: { name: "Bus", players: 5, difficulty: 3 } + ctf: { name: "Capture the Curry!", players: 4, difficulty: 3 } duplex: { name: "Duplex", players: 2, difficulty: 3 } junior: { name: "Junior", players: 3, difficulty: 1 } line: { name: "Line", players: 2, difficulty: 1 } diff --git a/data/maps/ctf.yaml b/data/maps/ctf.yaml new file mode 100644 index 00000000..24b22990 --- /dev/null +++ b/data/maps/ctf.yaml @@ -0,0 +1,61 @@ +# 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/>. +# +score_baseline: 100000 +default_timer: 120 +recipes: none +map: + - "BBBBBBB......^o^..^." + - "B.....e...^^^.^..^o^" + - "B.....e..^ooo^..^.^." + - "B..Ø..d...^^^..^o^.." + - "B.....d......^..^..^" + - "B.......^o^.^o^....o" + - "B............^.....^" + - "CC.CC...xccxx..CC.CC" + - "C...C..........C...C" + - "C.x.C.xaa.~....C...C" + - "C.x.C......bbx.Cxx.C" + - "C...C..........C...C" + - "CC.CC..xccxx...CC.CC" + - ".....^^............A" + - "....^oo^..^^.......A" + - "..^..^^..^oo^......A" + - ".^o^..^...^^....ø..A" + - "..^..^o^.^.........A" + - "......^.^o^........A" + - "..^o^....^...AAAAAAA" + +tiles: + "x": counter -c + "a": cutting-board -c + "b": rolling-board -c + "c": stove -c + "d": oven -c + "e": freezer -c + "Ø": conveyor -c + "ø": conveyor -c + "~": floor -w --chef-spawn --customer-spawn + ".": floor -w + "A": wall:blue -c + "B": wall:red -c + "C": wall:green -c + "o": table -c + "^": chair -w + +entities: + - !ctf_minigame + items: [lettuce, pizza] + spawnpoints: [[3, 3], [16, 16]] diff --git a/server/data/src/entities.rs b/server/data/src/entities.rs index cd1ba18d..5c874441 100644 --- a/server/data/src/entities.rs +++ b/server/data/src/entities.rs @@ -90,6 +90,12 @@ pub enum EntityDecl { DemandSink { pos: IVec2, }, + CtfMinigame { + items: Vec<String>, + spawnpoints: Vec<IVec2>, + #[serde(default)] + item_indices: Vec<ItemIndex>, + }, } #[derive(Debug, Clone, Deserialize, Serialize)] diff --git a/server/data/src/lib.rs b/server/data/src/lib.rs index b490fbc2..d4764f7d 100644 --- a/server/data/src/lib.rs +++ b/server/data/src/lib.rs @@ -235,6 +235,13 @@ fn build_data( *out_tile = reg.register_tile("white-hole".to_owned()); tile_walkable.extend([*in_tile, *neutral_tile, *out_tile]); } + EntityDecl::CtfMinigame { + items, + item_indices, + .. + } => { + item_indices.extend(items.iter().cloned().map(|name| reg.register_item(name))); + } _ => (), } entities.push(e); diff --git a/server/data/src/registry.rs b/server/data/src/registry.rs index f405489b..14a06b93 100644 --- a/server/data/src/registry.rs +++ b/server/data/src/registry.rs @@ -98,6 +98,9 @@ pub(crate) fn filter_unused_tiles_and_items(data: &mut Gamedata, serverdata: &mu neutral_tile, .. } => used_tiles.extend([*in_tile, *out_tile, *neutral_tile]), + EntityDecl::CtfMinigame { item_indices, .. } => { + used_items.extend(item_indices.iter().cloned()); + } _ => (), }; } @@ -209,6 +212,9 @@ pub(crate) fn filter_unused_tiles_and_items(data: &mut Gamedata, serverdata: &mu *neutral_tile = tile_map[neutral_tile]; *out_tile = tile_map[out_tile]; } + EntityDecl::CtfMinigame { item_indices, .. } => { + item_indices.iter_mut().for_each(|e| *e = item_map[e]); + } _ => (), }; } 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, |