/* Hurry Curry! - a game about cooking Copyright 2024 metamuffin 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 crate::{ data::Gamedata, entity::{Entity, EntityT}, interaction::{interact, tick_slot, InteractEffect, TickEffect}, spatial_index::SpatialIndex, }; use anyhow::{anyhow, bail, Result}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, movement::MovementBase, ClientGamedata, ItemIndex, ItemLocation, Menu, Message, PacketC, PacketS, PlayerID, RecipeIndex, Score, TileIndex, }; use log::{info, warn}; use std::{ collections::{HashMap, HashSet, VecDeque}, sync::{Arc, RwLock}, time::{Duration, Instant}, }; #[derive(Debug, PartialEq)] pub struct Involvement { pub recipe: RecipeIndex, pub progress: f32, pub working: usize, } #[derive(Debug, PartialEq)] pub struct Item { pub kind: ItemIndex, pub active: Option, } pub struct Tile { pub kind: TileIndex, pub item: Option, } pub struct Player { pub name: String, pub character: i32, pub interacting: Option, pub item: Option, pub communicate_persist: Option, movement: MovementBase, pub direction: Vec2, pub boost: bool, pub last_position_update: Instant, } pub struct Game { pub data: Arc, pub tiles: HashMap, pub walkable: HashSet, pub players: HashMap, players_spatial_index: SpatialIndex, entities: Arc>>, end: Option, pub lobby: bool, pub score_changed: bool, pub score: Score, } impl Default for Game { fn default() -> Self { Self::new() } } impl Game { pub fn new() -> Self { Self { lobby: false, data: Gamedata::default().into(), players: HashMap::new(), tiles: HashMap::new(), walkable: HashSet::new(), end: None, entities: Arc::new(RwLock::new(vec![])), players_spatial_index: SpatialIndex::default(), score: Score::default(), score_changed: false, } } fn unload(&mut self, packet_out: &mut VecDeque) { packet_out.push_back(PacketC::SetIngame { state: false, lobby: false, }); for (id, _) in self.players.drain() { packet_out.push_back(PacketC::RemovePlayer { id }) } for (pos, _) in self.tiles.drain() { packet_out.push_back(PacketC::UpdateMap { tile: pos, kind: None, neighbors: [None, None, None, None], }) } self.walkable.clear(); } pub fn load( &mut self, gamedata: Gamedata, timer: Option, packet_out: &mut VecDeque, ) { let players = self .players .iter() .filter(|(id, _)| id.0 >= 0) .map(|(id, p)| (*id, (p.name.to_owned(), p.character))) .collect::>(); self.unload(packet_out); self.lobby = gamedata.map_name == "lobby"; self.data = gamedata.into(); self.score = Score::default(); self.end = timer.map(|dur| Instant::now() + dur); self.entities = Arc::new(RwLock::new(self.data.entities.clone())); for (&p, (tile, item)) in &self.data.initial_map { self.tiles.insert( p, Tile { kind: *tile, item: item.map(|i| Item { kind: i, active: None, }), }, ); if !self.data.tile_collide[tile.0] { self.walkable.insert(p); } } for (id, (name, character)) in players { self.players.insert( id, Player { item: None, character, movement: MovementBase { position: if character < 0 { self.data.customer_spawn } else { self.data.chef_spawn }, facing: Vec2::X, rotation: 0., velocity: Vec2::ZERO, boosting: false, stamina: 0., }, last_position_update: Instant::now(), boost: false, direction: Vec2::ZERO, communicate_persist: None, interacting: None, name: name.clone(), }, ); } packet_out.extend(self.prime_client()); } pub fn prime_client(&self) -> Vec { let mut out = Vec::new(); out.push(PacketC::Data { data: ClientGamedata { item_names: self.data.item_names.clone(), tile_names: self.data.tile_names.clone(), tile_collide: self.data.tile_collide.clone(), tile_interact: self.data.tile_interact.clone(), current_map: self.data.map_name.clone(), map_names: self .data .map .clone() .keys() .filter(|n| n.as_str() != "lobby") .map(|s| s.to_owned()) .collect(), maps: self .data .map .clone() .into_iter() .filter(|(n, _)| n != "lobby") .collect(), }, }); for (&id, player) in &self.players { out.push(PacketC::AddPlayer { id, position: player.movement.position, character: player.character, name: player.name.clone(), }); if let Some(item) = &player.item { out.push(PacketC::SetItem { location: ItemLocation::Player(id), item: Some(item.kind), }) } if let Some(c) = &player.communicate_persist { out.push(PacketC::Communicate { player: id, message: Some(c.to_owned()), persist: true, }) } } for (&tile, tdata) in &self.tiles { out.push(PacketC::UpdateMap { tile, neighbors: [ self.tiles.get(&(tile + IVec2::NEG_Y)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::NEG_X)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::Y)).map(|e| e.kind), self.tiles.get(&(tile + IVec2::X)).map(|e| e.kind), ], kind: Some(tdata.kind), }); if let Some(item) = &tdata.item { out.push(PacketC::SetItem { location: ItemLocation::Tile(tile), item: Some(item.kind), }) } } out.push(PacketC::Score(self.score.clone())); out.push(PacketC::SetIngame { state: true, lobby: self.lobby, }); out } pub fn packet_in( &mut self, player: PlayerID, packet: PacketS, replies: &mut Vec, packet_out: &mut VecDeque, ) -> Result<()> { match packet { PacketS::Join { name, character } => { if self.players.contains_key(&player) { bail!("You already joined.") } let position = if player.0 < 0 { self.data.customer_spawn } else { self.data.chef_spawn }; self.players.insert( player, Player { item: None, character, movement: MovementBase { position: if character < 0 { self.data.customer_spawn } else { self.data.chef_spawn }, facing: Vec2::X, rotation: 0., velocity: Vec2::ZERO, boosting: false, stamina: 0., }, last_position_update: Instant::now(), boost: false, direction: Vec2::ZERO, communicate_persist: None, interacting: None, name: name.clone(), }, ); packet_out.push_back(PacketC::AddPlayer { id: player, name, position, character, }); } PacketS::Leave => { let p = self .players .remove(&player) .ok_or(anyhow!("player does not exist"))?; self.players_spatial_index.remove_entry(player); if let Some(item) = p.item { let pos = p.movement.position.floor().as_ivec2(); if let Some(tile) = self.tiles.get_mut(&pos) { if tile.item.is_none() { packet_out.push_back(PacketC::SetItem { location: ItemLocation::Tile(pos), item: Some(item.kind), }); tile.item = Some(item); } } } packet_out.push_back(PacketC::RemovePlayer { id: player }) } PacketS::Movement { pos, boosting, direction, } => { let player = self .players .get_mut(&player) .ok_or(anyhow!("player does not exist"))?; player.direction = direction; player.boost = boosting; if let Some(pos) = pos { let dt = player.last_position_update.elapsed(); player.last_position_update += dt; let diff = pos - player.movement.position; player.movement.position += diff.clamp_length_max(dt.as_secs_f32()); if diff.length() > 1. { replies.push(PacketC::MovementSync); } } } PacketS::Collide { player, force } => { packet_out.push_back(PacketC::Collide { player, force }); } PacketS::Interact { pos } => { let pid = player; let player = self .players .get_mut(&pid) .ok_or(anyhow!("player does not exist"))?; let (pos, edge) = match (pos, player.interacting) { (None, None) => return Ok(()), // this is silent because of auto release (None, Some(pos)) => (pos, false), (Some(pos), None) => (pos, true), (Some(_), Some(_)) => bail!("already interacting"), }; let entpos = pos.as_vec2() + Vec2::splat(0.5); if edge && entpos.distance(player.movement.position) > 2. { bail!("interacting too far from player"); } let tile = self .tiles .get_mut(&pos) .ok_or(anyhow!("tile does not exist"))?; // No going back from here on player.interacting = if edge { Some(pos) } else { None }; let other_pid = if !self.data.is_tile_interactable(tile.kind) { self.players .iter() .find(|(id, p)| **id != pid && p.movement.position.distance(entpos) < 0.7) .map(|(&id, _)| id) } else { None }; if let Some(base_pid) = other_pid { let [other, this] = self .players .get_many_mut([&pid, &base_pid]) .ok_or(anyhow!("interacting with yourself. this is impossible"))?; if this.character < 0 || other.character < 0 { bail!("You shall not interact with customers.") } interact_effect( &self.data, edge, &mut this.item, ItemLocation::Player(base_pid), &mut other.item, ItemLocation::Player(pid), None, packet_out, &mut self.score, false, ) } else { let player = self .players .get_mut(&pid) .ok_or(anyhow!("player does not exist"))?; interact_effect( &self.data, edge, &mut tile.item, ItemLocation::Tile(pos), &mut player.item, ItemLocation::Player(pid), Some(tile.kind), packet_out, &mut self.score, false, ) } } PacketS::Communicate { message, persist } => { info!("{player:?} message {message:?}"); if persist { if let Some(player) = self.players.get_mut(&player) { player.communicate_persist = message.clone() } } packet_out.push_back(PacketC::Communicate { player, message, persist, }) } PacketS::ReplaceHand { item } => { let pdata = self .players .get_mut(&player) .ok_or(anyhow!("player does not exist"))?; pdata.item = item.map(|i| Item { kind: i, active: None, }); packet_out.push_back(PacketC::SetItem { location: ItemLocation::Player(player), item, }) } PacketS::ReplayTick { .. } => bail!("packet not supported in this session"), } Ok(()) } /// Returns true if the game should end pub fn tick(&mut self, dt: f32, packet_out: &mut VecDeque) -> bool { if self.score_changed { self.score_changed = false; packet_out.push_back(PacketC::Score(self.score.clone())); } for (&pos, tile) in &mut self.tiles { if let Some(effect) = tick_slot( dt, &self.data, Some(tile.kind), &mut tile.item, &mut self.score, ) { match effect { TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { warn, item: ItemLocation::Tile(pos), progress: tile .item .as_ref() .unwrap() .active .as_ref() .map(|i| i.progress), }), TickEffect::Produce => { packet_out.push_back(PacketC::SetItem { location: ItemLocation::Tile(pos), item: tile.item.as_ref().map(|i| i.kind), }); } } } } for (&pid, player) in &mut self.players { player .movement .update(&self.walkable, player.direction, player.boost, dt); self.players_spatial_index .update_entry(pid, player.movement.position); } self.players_spatial_index.all(|p1, pos1| { self.players_spatial_index.query(pos1, 2., |p2, _pos2| { if let Some([a, b]) = self.players.get_many_mut([&p1, &p2]) { a.movement.collide(&mut b.movement, dt) } }) }); for (&pid, player) in &mut self.players { packet_out.push_back(PacketC::Position { player: pid, pos: player.movement.position, boosting: player.movement.boosting, rot: player.movement.rotation, }); if let Some(effect) = tick_slot(dt, &self.data, None, &mut player.item, &mut self.score) { match effect { TickEffect::Progress(warn) => packet_out.push_back(PacketC::SetProgress { warn, item: ItemLocation::Player(pid), progress: player .item .as_ref() .unwrap() .active .as_ref() .map(|i| i.progress), }), TickEffect::Produce => { packet_out.push_back(PacketC::SetItem { location: ItemLocation::Player(pid), item: player.item.as_ref().map(|i| i.kind), }); } } } } let mut players_auto_release = Vec::new(); for (pid, player) in &mut self.players { if let Some(pos) = player.interacting { if let Some(tile) = self.tiles.get(&pos) { if let Some(item) = &tile.item { if let Some(involvement) = &item.active { if involvement.progress >= 1. { players_auto_release.push(*pid); } } } } } } for pid in players_auto_release.drain(..) { let _ = self.packet_in( pid, PacketS::Interact { pos: None }, &mut vec![], packet_out, ); } for entity in self.entities.clone().write().unwrap().iter_mut() { if let Err(e) = entity.tick(self, packet_out, dt) { warn!("entity tick failed: {e}") } } if let Some(end) = self.end { self.score.time_remaining = (end - Instant::now()).as_secs_f64(); if end < Instant::now() { packet_out.push_back(PacketC::Menu(Menu::Score(self.score.clone()))); true } else { false } } else { false } } pub fn count_chefs(&self) -> usize { self.players .values() .map(|p| if p.character >= 0 { 1 } else { 0 }) .sum() } } impl From for Tile { fn from(kind: TileIndex) -> Self { Self { kind, item: None } } } pub fn interact_effect( data: &Gamedata, edge: bool, this: &mut Option, this_loc: ItemLocation, other: &mut Option, other_loc: ItemLocation, this_tile_kind: Option, packet_out: &mut VecDeque, score: &mut Score, automated: bool, ) { let this_had_item = this.is_some(); let other_had_item = other.is_some(); if let Some(effect) = interact(data, edge, this_tile_kind, this, other, score, automated) { match effect { InteractEffect::Put => { info!("put {this_loc} <- {other_loc}"); packet_out.push_back(PacketC::MoveItem { from: other_loc, to: this_loc, }) } InteractEffect::Take => { info!("take {this_loc} -> {other_loc}"); packet_out.push_back(PacketC::MoveItem { from: this_loc, to: other_loc, }) } InteractEffect::Produce => { info!("produce {this_loc} <~ {other_loc}"); if this_had_item { packet_out.push_back(PacketC::SetProgress { item: this_loc, progress: None, warn: false, }); packet_out.push_back(PacketC::SetItem { location: this_loc, item: None, }); } if other_had_item { packet_out.push_back(PacketC::MoveItem { from: other_loc, to: this_loc, }); packet_out.push_back(PacketC::SetItem { location: this_loc, item: None, }); } if let Some(i) = &other { packet_out.push_back(PacketC::SetItem { location: this_loc, item: Some(i.kind), }); packet_out.push_back(PacketC::MoveItem { from: this_loc, to: other_loc, }) } if let Some(i) = &this { packet_out.push_back(PacketC::SetItem { location: this_loc, item: Some(i.kind), }); } } } } } impl Player { pub fn position(&self) -> Vec2 { self.movement.position } }