/*
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 .
*/
pub mod movement;
mod pathfinding;
use crate::{
data::Gamedata,
game::Game,
protocol::{DemandIndex, ItemIndex, Message, PacketC, PacketS, PlayerID},
state::State,
};
use glam::{IVec2, Vec2};
use log::{debug, error};
use movement::MovementBase;
use pathfinding::{find_path, Path};
use rand::{random, thread_rng};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
time::Duration,
};
use tokio::{
sync::{broadcast, RwLock},
time::interval,
};
struct CustomerManager {
disabled: bool,
walkable: HashSet,
chairs: HashMap,
items: HashMap,
customers: HashMap,
customer_id_counter: PlayerID,
demand: DemandState,
}
struct DemandState {
data: Gamedata,
}
enum CustomerState {
Entering {
path: Path,
chair: IVec2,
},
Waiting {
demand: DemandIndex,
chair: IVec2,
timeout: f32,
},
Eating {
demand: DemandIndex,
target: IVec2,
progress: f32,
chair: IVec2,
},
Exiting {
path: Path,
},
}
struct Customer {
movement: MovementBase,
state: CustomerState,
}
pub async fn customer(gstate: Arc>, mut grx: broadcast::Receiver) {
let mut state = CustomerManager {
customer_id_counter: PlayerID(0),
walkable: Default::default(),
chairs: Default::default(),
items: Default::default(),
customers: Default::default(),
disabled: true,
demand: DemandState {
data: Gamedata::default(),
},
};
let initial = gstate.write().await.game.prime_client();
for packet in initial {
state.packet(packet);
}
let mut interval = interval(Duration::from_millis(40));
let mut packets_out = Vec::new();
loop {
tokio::select! {
packet = grx.recv() => {
let packet = packet.unwrap();
match packet {
PacketC::PutItem { .. }
| PacketC::TakeItem { .. }
| PacketC::SetTileItem { .. } => {
let g = gstate.read().await;
update_items(&mut state, &g.game)
},
_ => ()
}
state.packet(packet);
}
_ = interval.tick() => {
if !state.disabled {
state.tick(&mut packets_out, 0.04);
for (player,packet) in packets_out.drain(..) {
if let Err(e) = gstate.write().await.packet_in(player, packet).await {
error!("customer misbehaved: {e}")
}
}
}
}
}
}
}
// TODO very inefficient, please do that incrementally
fn update_items(state: &mut CustomerManager, game: &Game) {
state.items.clear();
for (&pos, tile) in game.tiles() {
if let Some(item) = &tile.item {
state.items.insert(pos, item.kind);
}
}
}
impl DemandState {
pub fn target_customer_count(&self) -> usize {
// TODO insert sofa magic formula
5
}
pub fn generate_demand(&self) -> DemandIndex {
// TODO insert sofa magic formula
DemandIndex(random::() % self.data.demands.len())
}
}
impl CustomerManager {
pub fn packet(&mut self, packet: PacketC) {
match packet {
PacketC::Data { data } => {
self.disabled = data.demands.is_empty();
self.demand.data = data;
}
PacketC::RemovePlayer { id } => {
self.customers.remove(&id);
}
PacketC::UpdateMap {
tile: pos,
kind: Some(tile),
..
} => {
let tilename = self.demand.data.tile_name(tile);
if !self.demand.data.is_tile_colliding(tile) {
self.walkable.insert(pos);
}
if tilename == "chair" {
self.chairs.insert(pos, true);
}
}
_ => (),
}
}
pub fn tick(&mut self, packets_out: &mut Vec<(PlayerID, PacketS)>, dt: f32) {
if self.customers.len() < self.demand.target_customer_count() {
self.customer_id_counter.0 -= 1;
let id = self.customer_id_counter;
packets_out.push((
id,
PacketS::Join {
name: "George".to_string(),
character: -2,
},
));
let chair = select_chair(&mut self.chairs);
let path = find_path(
&self.walkable,
self.demand.data.customer_spawn.as_ivec2(),
chair,
)
.expect("no path");
self.customers.insert(
id,
Customer {
movement: MovementBase {
position: self.demand.data.customer_spawn,
facing: Vec2::X,
vel: Vec2::ZERO,
},
state: CustomerState::Entering { path, chair },
},
);
}
let mut customers_to_remove = Vec::new();
for (&id, p) in &mut self.customers {
match &mut p.state {
CustomerState::Entering { path, chair } => {
debug!("{id:?} entering");
packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
if path.is_done() {
let demand = self.demand.generate_demand();
packets_out.push((
id,
PacketS::Communicate {
message: Some(Message::Item(self.demand.data.demand(demand).from)),
},
));
p.state = CustomerState::Waiting {
chair: *chair,
timeout: 60.,
demand,
};
}
}
CustomerState::Waiting {
chair,
demand,
timeout,
} => {
debug!("{id:?} waiting");
*timeout -= dt;
if *timeout <= 0. {
packets_out.push((id, PacketS::Communicate { message: None }));
let path = find_path(
&self.walkable,
p.movement.position.as_ivec2(),
self.demand.data.customer_spawn.as_ivec2(),
)
.expect("no path to exit");
*self.chairs.get_mut(&chair).unwrap() = true;
p.state = CustomerState::Exiting { path }
} else {
let demand_data = &self.demand.data.demand(*demand);
let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y]
.into_iter()
.find_map(|off| {
let pos = *chair + off;
if self.items.get(&pos) == Some(&demand_data.from) {
Some(pos)
} else {
None
}
});
if let Some(pos) = demand_pos {
packets_out.push((id, PacketS::Communicate { message: None }));
for edge in [true, false] {
packets_out.push((id, PacketS::Interact { pos, edge }))
}
p.state = CustomerState::Eating {
demand: *demand,
target: pos,
progress: 0.,
chair: *chair,
}
}
}
}
CustomerState::Eating {
demand,
target,
progress,
chair,
} => {
debug!("{id:?} eating");
let demand = self.demand.data.demand(*demand);
*progress += dt / demand.duration;
if *progress >= 1. {
packets_out.push((
id,
PacketS::ReplaceHand {
item: Some(demand.to),
},
));
for edge in [true, false] {
packets_out.push((id, PacketS::Interact { pos: *target, edge }))
}
let path = find_path(
&self.walkable,
p.movement.position.as_ivec2(),
self.demand.data.customer_spawn.as_ivec2(),
)
.expect("no path to exit");
*self.chairs.get_mut(&chair).unwrap() = true;
p.state = CustomerState::Exiting { path }
}
}
CustomerState::Exiting { path } => {
debug!("{id:?} exiting");
packets_out.push((id, path.execute_tick(&mut p.movement, &self.walkable, dt)));
if path.is_done() {
packets_out.push((id, PacketS::Leave));
customers_to_remove.push(id);
}
}
}
}
for c in customers_to_remove {
self.customers.remove(&c).unwrap();
}
}
}
pub fn select_chair(chairs: &mut HashMap) -> IVec2 {
use rand::seq::IteratorRandom;
let (chosen, free) = chairs
.iter_mut()
.filter(|(_p, free)| **free)
.choose(&mut thread_rng())
.unwrap();
*free = false;
*chosen
}