/*
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 .
*/
pub mod movement;
mod pathfinding;
use crate::{data::Gamedata, game::Tile};
use anyhow::{anyhow, Result};
use fake::{faker, Fake};
use hurrycurry_protocol::{glam::IVec2, DemandIndex, Message, PacketS, PlayerID};
use log::debug;
use movement::MovementBase;
use pathfinding::{find_path, Path};
use rand::{random, thread_rng};
use std::{
collections::{HashMap, HashSet},
sync::Arc,
};
pub struct DemandState {
data: Arc,
walkable: HashSet,
chairs: HashMap,
customer_id_counter: PlayerID,
customers: HashMap,
spawn_cooldown: f32,
pub completed: usize,
pub failed: usize,
pub score_changed: bool,
}
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,
},
}
pub struct Customer {
movement: MovementBase,
state: CustomerState,
}
impl DemandState {
pub fn new(data: Arc, map: &HashMap) -> Self {
let chair = data.get_tile_by_name("chair");
Self {
score_changed: true,
completed: 0,
failed: 0,
walkable: map
.iter()
.filter(|(_, v)| !data.is_tile_colliding(v.kind))
.map(|(e, _)| *e)
.collect(),
chairs: map
.iter()
.filter(|(_, v)| Some(v.kind) == chair)
.map(|(e, _)| (*e, true))
.collect(),
customer_id_counter: PlayerID(0),
customers: Default::default(),
data,
spawn_cooldown: 0.,
}
}
}
impl DemandState {
pub fn tick(
&mut self,
packets_out: &mut Vec<(PlayerID, PacketS)>,
tiles: &mut HashMap,
data: &Gamedata,
dt: f32,
points: &mut i64,
) -> Result<()> {
self.spawn_cooldown -= dt;
self.spawn_cooldown = self.spawn_cooldown.max(0.);
if self.customers.len() < 5 && self.spawn_cooldown <= 0. {
self.spawn_cooldown = 5. + random::() * 10.;
self.customer_id_counter.0 -= 1;
let id = self.customer_id_counter;
packets_out.push((
id,
PacketS::Join {
name: faker::name::fr_fr::Name().fake(),
character: -1 - (random::() as i32),
},
));
let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?;
let from = data.customer_spawn.as_ivec2();
let path = find_path(&self.walkable, from, chair)
.ok_or(anyhow!("no path from {from} to {chair}"))?;
self.customers.insert(
id,
Customer {
movement: MovementBase::new(data.customer_spawn),
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 = DemandIndex(random::() % self.data.demands.len());
packets_out.push((
id,
PacketS::Communicate {
message: Some(Message::Item(data.demand(demand).from)),
persist: true,
},
));
p.state = CustomerState::Waiting {
chair: *chair,
timeout: 60. + random::() * 30.,
demand,
};
}
}
CustomerState::Waiting {
chair,
demand,
timeout,
} => {
debug!("{id:?} waiting");
*timeout -= dt;
if *timeout <= 0. {
packets_out.push((
id,
PacketS::Communicate {
message: None,
persist: true,
},
));
packets_out.push((
id,
PacketS::Communicate {
message: Some(Message::Effect("angry".to_string())),
persist: false,
},
));
let path = find_path(
&self.walkable,
p.movement.position.as_ivec2(),
data.customer_spawn.as_ivec2(),
)
.expect("no path to exit");
*self.chairs.get_mut(&chair).unwrap() = true;
self.failed += 1;
*points -= 1;
self.score_changed = true;
p.state = CustomerState::Exiting { path }
} else {
let demand_data = &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 tiles
.get(&pos)
.map(|t| {
t.item
.as_ref()
.map(|i| i.kind == demand_data.from)
.unwrap_or_default()
})
.unwrap_or_default()
{
Some(pos)
} else {
None
}
});
if let Some(pos) = demand_pos {
packets_out.push((
id,
PacketS::Communicate {
persist: true,
message: None,
},
));
packets_out.push((
id,
PacketS::Communicate {
message: Some(Message::Effect("satisfied".to_string())),
persist: false,
},
));
packets_out.push((id, PacketS::Interact { pos: Some(pos) }));
packets_out.push((id, PacketS::Interact { pos: None }));
p.state = CustomerState::Eating {
demand: *demand,
target: pos,
progress: 0.,
chair: *chair,
}
}
}
}
CustomerState::Eating {
demand,
target,
progress,
chair,
} => {
debug!("{id:?} eating");
let demand = data.demand(*demand);
*progress += dt / demand.duration;
if *progress >= 1. {
packets_out.push((id, PacketS::ReplaceHand { item: demand.to }));
if demand.to.is_some() {
packets_out.push((id, PacketS::Interact { pos: Some(*target) }));
packets_out.push((id, PacketS::Interact { pos: None }));
}
let path = find_path(
&self.walkable,
p.movement.position.as_ivec2(),
data.customer_spawn.as_ivec2(),
)
.ok_or(anyhow!("no path to exit"))?;
*self.chairs.get_mut(&chair).unwrap() = true;
self.completed += 1;
*points += demand.points;
self.score_changed = 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();
}
Ok(())
}
fn select_chair(&mut self) -> Option {
use rand::seq::IteratorRandom;
let (chosen, free) = self
.chairs
.iter_mut()
.filter(|(_p, free)| **free)
.choose(&mut thread_rng())?;
*free = false;
Some(*chosen)
}
}