/*
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, Message, PacketC, PacketS, PlayerID, RecipeIndex,
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,
pub packet_out: VecDeque,
entities: Arc>>,
end: Option,
pub lobby: bool,
pub score_changed: bool,
pub points: i64,
pub demands_failed: usize,
pub demands_completed: usize,
}
impl Default for Game {
fn default() -> Self {
Self::new()
}
}
impl Game {
pub fn new() -> Self {
Self {
lobby: false,
data: Gamedata::default().into(),
packet_out: Default::default(),
players: HashMap::new(),
tiles: HashMap::new(),
walkable: HashSet::new(),
end: None,
entities: Arc::new(RwLock::new(vec![])),
players_spatial_index: SpatialIndex::default(),
points: 0,
demands_failed: 0,
demands_completed: 0,
score_changed: false,
}
}
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.walkable.clear();
}
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 = 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(),
},
);
}
self.packet_out.extend(self.prime_client());
}
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
.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(self.score());
out.push(PacketC::SetIngame {
state: true,
lobby: self.lobby,
});
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.demands_failed,
demands_completed: self.demands_completed,
}
}
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,
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(),
},
);
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"))?;
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() {
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::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;
player.movement.position +=
(pos - player.movement.position).clamp_length_max(dt.as_secs_f32());
}
}
PacketS::Collide { player, force } => {
self.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,
&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 self.score_changed {
self.score_changed = false;
self.packet_out.push_back(self.score());
}
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 {
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 {
self.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) {
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 self.entities.clone().write().unwrap().iter_mut() {
if let Err(e) = entity.tick(self, dt) {
warn!("entity tick failed: {e}")
}
}
self.end.map(|t| t < Instant::now()).unwrap_or_default()
}
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,
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 => {
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
}
}