aboutsummaryrefslogtreecommitdiff
path: root/server/src
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 /server/src
parente99a71f4f5918e9e43c5f8ff01ce348021f925ea (diff)
downloadhurrycurry-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.rs296
-rw-r--r--server/src/entity/mod.rs10
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,