/*
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(gamedata: Arc) -> Self {
let mut g = Self {
data: gamedata.clone(),
packet_out: Default::default(),
players: Default::default(),
tiles: Default::default(),
};
for (&p, (tile, item)) in &gamedata.initial_map {
g.tiles.insert(
p,
Tile {
kind: *tile,
item: item.map(|i| Item {
kind: i,
active: None,
}),
},
);
}
g
}
pub fn tiles(&self) -> &HashMap {
&self.tiles
}
pub fn packet_out(&mut self) -> Option {
self.packet_out.pop_front()
}
pub fn prime_client(&self, id: PlayerID) -> Vec {
let mut out = Vec::new();
out.push(PacketC::Init {
id,
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 {
pos: 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),
],
tile: 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, player) {
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 }
}
}