aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authornokoe <nokoe@mailbox.org>2025-12-16 04:45:47 +0100
committernokoe <nokoe@mailbox.org>2025-12-16 04:45:47 +0100
commit3d49838e934848bd66411ca55f5b58eae99cb728 (patch)
tree80b95f93341ea8a97c23d95b07bec822aea767a5
parente99a71f4f5918e9e43c5f8ff01ce348021f925ea (diff)
downloadhurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar
hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar.bz2
hurrycurry-3d49838e934848bd66411ca55f5b58eae99cb728.tar.zst
capture the curry
-rw-r--r--data/index.yaml1
-rw-r--r--data/maps/ctf.yaml61
-rw-r--r--server/data/src/entities.rs6
-rw-r--r--server/data/src/lib.rs7
-rw-r--r--server/data/src/registry.rs6
-rw-r--r--server/src/entity/ctf_minigame.rs296
-rw-r--r--server/src/entity/mod.rs10
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,