/* Undercooked - 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::movement::PLAYER_SPEED_LIMIT, data::Gamedata, interaction::{interact, tick_tile, InteractEffect, TickEffect}, protocol::{ItemIndex, Message, PacketC, PacketS, PlayerID, RecipeIndex, TileIndex}, }; use anyhow::{anyhow, bail, Result}; use glam::{IVec2, Vec2}; use log::info; use std::{ collections::{HashMap, VecDeque}, ops::Deref, sync::Arc, time::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: Option, } pub struct Game { data: Arc, tiles: HashMap, players: HashMap, packet_out: VecDeque, } impl Game { pub fn new() -> Self { Self { data: Gamedata::default().into(), packet_out: Default::default(), players: Default::default(), tiles: Default::default(), } } fn unload(&mut self) { 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], }) } } pub fn load(&mut self, gamedata: Gamedata) { 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(); 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: None, interacting: None, name: name.clone(), }, ); } 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: self.data.deref().to_owned(), }); 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::SetPlayerItem { player: id, item: Some(item.kind), }) } if let Some(c) = &player.communicate { out.push(PacketC::Communicate { player: id, message: Some(c.to_owned()), }) } } 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::SetTileItem { tile, item: Some(item.kind), }) } } out } pub fn packet_in(&mut self, player: PlayerID, packet: PacketS) -> Result<()> { match packet { PacketS::Join { name, character } => { self.players.insert( player, Player { item: None, last_position_ts: Instant::now(), character, position: if player.0 < 0 { self.data.customer_spawn } else { self.data.chef_spawn }, communicate: None, interacting: None, name: name.clone(), }, ); self.packet_out.push_back(PacketC::AddPlayer { id: player, name, position: self.data.chef_spawn, 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::SetTileItem { tile: pos, item: Some(item.kind), }); tile.item = Some(item); } } } self.packet_out .push_back(PacketC::RemovePlayer { id: player }) } PacketS::Position { pos, rot } => { 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, }); 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, edge } => { info!("interact {pos:?} edge={edge}"); let pid = player; let player = self .players .get_mut(&player) .ok_or(anyhow!("player does not exist"))?; let tile = self .tiles .get_mut(&pos) .ok_or(anyhow!("tile does not exist"))?; if edge && player.interacting.is_some() { bail!("already interacting") } if !edge && player.interacting != Some(pos) { bail!("already not interacting here") } if edge && (pos.as_vec2() + Vec2::splat(0.5)).distance(player.position) > 2. { bail!("interacting too far from player"); } let tile_had_item = tile.item.is_some(); let player_had_item = player.item.is_some(); if let Some(effect) = interact( &self.data, edge, tile.kind, &mut tile.item, &mut player.item, ) { match effect { InteractEffect::Put => self.packet_out.push_back(PacketC::PutItem { player: pid, tile: pos, }), InteractEffect::Take => self.packet_out.push_back(PacketC::TakeItem { player: pid, tile: pos, }), InteractEffect::Produce => { if tile_had_item { self.packet_out.push_back(PacketC::SetActive { tile: pos, progress: None, warn: false, }); self.packet_out.push_back(PacketC::SetTileItem { tile: pos, item: None, }); } if player_had_item { self.packet_out.push_back(PacketC::PutItem { player: pid, tile: pos, }); self.packet_out.push_back(PacketC::SetTileItem { tile: pos, item: None, }); } if let Some(i) = &player.item { self.packet_out.push_back(PacketC::SetTileItem { tile: pos, item: Some(i.kind), }); self.packet_out.push_back(PacketC::TakeItem { player: pid, tile: pos, }) } if let Some(i) = &tile.item { self.packet_out.push_back(PacketC::SetTileItem { tile: pos, item: Some(i.kind), }); } } } } player.interacting = if edge { Some(pos) } else { None }; } PacketS::Communicate { message } => { info!("{player:?} message {message:?}"); if let Some(player) = self.players.get_mut(&player) { player.communicate = message.clone() } self.packet_out .push_back(PacketC::Communicate { player, message }) } 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::SetPlayerItem { player, item }) } } Ok(()) } pub fn tick(&mut self, dt: f32) { for (&pos, tile) in &mut self.tiles { if let Some(effect) = tick_tile(dt, &self.data, tile) { match effect { TickEffect::Progress(warn) => self.packet_out.push_back(PacketC::SetActive { warn, tile: pos, progress: tile .item .as_ref() .unwrap() .active .as_ref() .map(|i| i.progress), }), TickEffect::Produce => { self.packet_out.push_back(PacketC::SetTileItem { tile: pos, item: tile.item.as_ref().map(|i| i.kind), }); } } } } } } impl From for Tile { fn from(kind: TileIndex) -> Self { Self { kind, item: None } } }