/*
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)
}
}
}