/* 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::{ customer::DemandState, data::Gamedata, entity::{Entity, EntityT}, interaction::{interact, tick_slot, InteractEffect, TickEffect}, }; use anyhow::{anyhow, bail, Result}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, ClientGamedata, ItemIndex, ItemLocation, Message, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex, }; use log::{info, warn}; use std::{ collections::{HashMap, VecDeque}, sync::Arc, 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 position: Vec2, pub last_position_ts: Instant, pub interacting: Option, pub item: Option, pub communicate_persist: Option, } pub struct Game { data: Arc, tiles: HashMap, pub players: HashMap, packet_out: VecDeque, demand: Option, pub points: i64, entities: Vec, end: Option, } impl Game { pub fn new() -> Self { Self { data: Gamedata::default().into(), packet_out: Default::default(), players: HashMap::new(), tiles: HashMap::new(), demand: None, end: None, entities: vec![], points: 0, } } fn unload(&mut self) { self.packet_out.push_back(PacketC::SetIngame { state: false, lobby: false, }); for (id, _) in self.players.drain() { self.packet_out.push_back(PacketC::RemovePlayer { id }) } for (pos, _) in self.tiles.drain() { self.packet_out.push_back(PacketC::UpdateMap { tile: pos, kind: None, neighbors: [None, None, None, None], }) } self.demand = None; } pub fn load(&mut self, gamedata: Gamedata, timer: Option) { let players = self .players .iter() .filter(|(id, _)| id.0 >= 0) .map(|(id, p)| (*id, (p.name.to_owned(), p.character))) .collect::>(); self.unload(); self.data = gamedata.into(); self.points = 0; self.end = timer.map(|dur| Instant::now() + dur); self.entities = 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, }), }, ); } for (id, (name, character)) in players { self.players.insert( id, Player { item: None, last_position_ts: Instant::now(), character, position: self.data.chef_spawn, communicate_persist: None, interacting: None, name: name.clone(), }, ); } if !self.data.demands.is_empty() { self.demand = Some(DemandState::new(self.data.clone(), &self.tiles)) } self.packet_out.extend(self.prime_client()); } pub fn tiles(&self) -> &HashMap { &self.tiles } pub fn packet_out(&mut self) -> Option { self.packet_out.pop_front() } 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(), map_names: self .data .map_names .clone() .into_iter() .filter(|n| n != "lobby") .collect(), }, }); for (&id, player) in &self.players { out.push(PacketC::AddPlayer { id, position: player.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.clone()), }); if let Some(item) = &tdata.item { out.push(PacketC::SetItem { location: ItemLocation::Tile(tile), item: Some(item.kind), }) } } out.push(self.score()); out.push(PacketC::SetIngame { state: true, lobby: self.demand.is_none(), }); out } pub fn score(&self) -> PacketC { PacketC::Score { time_remaining: self.end.map(|t| (t - Instant::now()).as_secs_f32()), points: self.points, demands_failed: self.demand.as_ref().map(|d| d.failed).unwrap_or_default(), demands_completed: self .demand .as_ref() .map(|d| d.completed) .unwrap_or_default(), } } pub fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<()> { let points_before = self.points; 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, last_position_ts: Instant::now(), character, position, communicate_persist: None, interacting: None, name: name.clone(), }, ); self.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"))?; if let Some(item) = p.item { let pos = p.position.floor().as_ivec2(); if let Some(tile) = self.tiles.get_mut(&pos) { if tile.item.is_none() { self.packet_out.push_back(PacketC::SetItem { location: ItemLocation::Tile(pos), item: Some(item.kind), }); tile.item = Some(item); } } } self.packet_out .push_back(PacketC::RemovePlayer { id: player }) } PacketS::Position { pos, rot, boosting } => { let pid = player; let player = self .players .get_mut(&player) .ok_or(anyhow!("player does not exist"))?; // let dt = player.last_position_ts.elapsed().as_secs_f32(); // let dist = pos.distance(player.position); // let speed = dist / dt; // let interact_dist = player // .interacting // .map(|p| (p.as_vec2() + Vec2::splat(0.5)).distance(player.position)) // .unwrap_or_default(); // let movement_ok = speed < PLAYER_SPEED_LIMIT && dist < 1. && interact_dist < 2.; // if movement_ok { player.position = pos; player.last_position_ts = Instant::now(); // } self.packet_out.push_back(PacketC::Position { player: pid, pos: player.position, rot, boosting, }); // if !movement_ok { // bail!( // "{:?} moved to quickly. speed={speed:.02} dist={dist:.02}", // player.name // ) // } } PacketS::Collide { player, force } => { self.packet_out .push_back(PacketC::Collide { player, force }); } PacketS::Interact { pos } => { info!("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.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.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, &mut self.packet_out, &mut self.points, 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), &mut self.packet_out, &mut self.points, 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() } } self.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, }); self.packet_out.push_back(PacketC::SetItem { location: ItemLocation::Player(player), item, }) } PacketS::ReplayTick { .. } => bail!("packet not supported in this session"), } if self.points != points_before { self.packet_out.push_back(self.score()) } Ok(()) } /// Returns true if the game should end pub fn tick(&mut self, dt: f32) -> bool { if let Some(demand) = &mut self.demand { let mut packet_out = Vec::new(); if let Err(err) = demand.tick( &mut packet_out, &mut self.tiles, &self.data, dt, &mut self.points, ) { warn!("demand tick {err}"); } if demand.score_changed { demand.score_changed = false; self.packet_out.push_back(self.score()); } for (player, packet) in packet_out { if let Err(err) = self.packet_in(player, packet) { warn!("demand packet {err}"); } } } for (&pos, tile) in &mut self.tiles { if let Some(effect) = tick_slot(dt, &self.data, Some(tile.kind), &mut tile.item) { match effect { TickEffect::Progress(warn) => self.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 => { self.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 { if let Some(effect) = tick_slot(dt, &self.data, None, &mut player.item) { match effect { TickEffect::Progress(warn) => self.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 => { self.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 }); } for entity in &mut self.entities { if let Err(e) = entity.tick( &self.data, &mut self.points, &mut self.packet_out, &mut self.tiles, dt, ) { warn!("entity tick failed: {e}") } } return self.end.map(|t| t < Instant::now()).unwrap_or_default(); } } 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, points: &mut i64, 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, points, automated) { match effect { InteractEffect::Put => packet_out.push_back(PacketC::MoveItem { from: other_loc, to: this_loc, }), InteractEffect::Take => packet_out.push_back(PacketC::MoveItem { from: this_loc, to: other_loc, }), InteractEffect::Produce => { 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), }); } } } } }