summaryrefslogtreecommitdiff
path: root/server/src/entity/customers/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/entity/customers/mod.rs')
-rw-r--r--server/src/entity/customers/mod.rs274
1 files changed, 274 insertions, 0 deletions
diff --git a/server/src/entity/customers/mod.rs b/server/src/entity/customers/mod.rs
new file mode 100644
index 00000000..7f0b0c22
--- /dev/null
+++ b/server/src/entity/customers/mod.rs
@@ -0,0 +1,274 @@
+/*
+ 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 <https://www.gnu.org/licenses/>.
+
+*/
+pub mod demands;
+mod pathfinding;
+
+use super::EntityT;
+use crate::{data::Demand, game::Game};
+use anyhow::{anyhow, Result};
+use fake::{faker, Fake};
+use hurrycurry_protocol::{glam::IVec2, DemandIndex, Message, PacketS, PlayerID};
+use log::{info, warn};
+use pathfinding::{find_path, Path};
+use rand::{random, thread_rng};
+use std::collections::{HashMap, VecDeque};
+
+#[derive(Debug, Clone)]
+pub struct Customers {
+ demands: Vec<Demand>,
+ cpackets: VecDeque<(PlayerID, PacketS)>,
+ chairs: HashMap<IVec2, bool>,
+ customer_id_counter: PlayerID,
+ customers: HashMap<PlayerID, CustomerState>,
+ spawn_cooldown: f32,
+}
+
+#[derive(Debug, Clone)]
+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,
+ },
+}
+
+impl Customers {
+ pub fn new(chairs: HashMap<IVec2, bool>, demands: Vec<Demand>) -> Self {
+ Self {
+ chairs,
+ customer_id_counter: PlayerID(0),
+ customers: Default::default(),
+ demands,
+ spawn_cooldown: 0.,
+ cpackets: VecDeque::new(),
+ }
+ }
+}
+
+impl EntityT for Customers {
+ fn tick(&mut self, game: &mut Game, dt: f32) -> 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 = 10. + random::<f32>() * 10.;
+ self.customer_id_counter.0 -= 1;
+ let id = self.customer_id_counter;
+ self.cpackets.push_back((
+ id,
+ PacketS::Join {
+ name: faker::name::fr_fr::Name().fake(),
+ character: -1 - (random::<u16>() as i32),
+ },
+ ));
+ let chair = self.select_chair().ok_or(anyhow!("no free chair found"))?;
+ let from = game.data.customer_spawn.as_ivec2();
+ let path = find_path(&game.walkable, from, chair)
+ .ok_or(anyhow!("no path from {from} to {chair}"))?;
+ info!("{id:?} -> entering");
+ self.customers
+ .insert(id, CustomerState::Entering { path, chair });
+ }
+ let mut customers_to_remove = Vec::new();
+ for (&id, state) in &mut self.customers {
+ let Some(player) = game.players.get_mut(&id) else {
+ continue;
+ };
+
+ match state {
+ CustomerState::Entering { path, chair } => {
+ player.direction = path.next_direction(player.position());
+ if path.is_done() {
+ let demand = DemandIndex(random::<usize>() % self.demands.len());
+ self.cpackets.push_back((
+ id,
+ PacketS::Communicate {
+ message: Some(Message::Item(self.demands[demand.0].from)),
+ persist: true,
+ },
+ ));
+ info!("{id:?} -> waiting");
+ *state = CustomerState::Waiting {
+ chair: *chair,
+ timeout: 90. + random::<f32>() * 60.,
+ demand,
+ };
+ }
+ }
+ CustomerState::Waiting {
+ chair,
+ demand,
+ timeout,
+ } => {
+ player.direction *= 0.;
+ *timeout -= dt;
+ if *timeout <= 0. {
+ self.cpackets.push_back((
+ id,
+ PacketS::Communicate {
+ message: None,
+ persist: true,
+ },
+ ));
+ self.cpackets.push_back((
+ id,
+ PacketS::Communicate {
+ message: Some(Message::Effect("angry".to_string())),
+ persist: false,
+ },
+ ));
+ let path = find_path(
+ &game.walkable,
+ player.position().as_ivec2(),
+ game.data.customer_spawn.as_ivec2(),
+ )
+ .expect("no path to exit");
+ *self.chairs.get_mut(&chair).unwrap() = true;
+ game.demands_failed += 1;
+ game.points -= 1;
+ game.score_changed = true;
+ info!("{id:?} -> exiting");
+ *state = CustomerState::Exiting { path }
+ } else {
+ let demand_data = &self.demands[demand.0];
+ let demand_pos = [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y]
+ .into_iter()
+ .find_map(|off| {
+ let pos = *chair + off;
+ if game
+ .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 {
+ self.cpackets.push_back((
+ id,
+ PacketS::Communicate {
+ persist: true,
+ message: None,
+ },
+ ));
+ self.cpackets.push_back((
+ id,
+ PacketS::Communicate {
+ message: Some(Message::Effect("satisfied".to_string())),
+ persist: false,
+ },
+ ));
+ self.cpackets
+ .push_back((id, PacketS::Interact { pos: Some(pos) }));
+ self.cpackets
+ .push_back((id, PacketS::Interact { pos: None }));
+ info!("{id:?} -> eating");
+ *state = CustomerState::Eating {
+ demand: *demand,
+ target: pos,
+ progress: 0.,
+ chair: *chair,
+ }
+ }
+ }
+ }
+ CustomerState::Eating {
+ demand,
+ target,
+ progress,
+ chair,
+ } => {
+ player.direction *= 0.;
+ let demand = &self.demands[demand.0];
+ *progress += dt / demand.duration;
+ if *progress >= 1. {
+ self.cpackets
+ .push_back((id, PacketS::ReplaceHand { item: demand.to }));
+ if demand.to.is_some() {
+ self.cpackets
+ .push_back((id, PacketS::Interact { pos: Some(*target) }));
+ self.cpackets
+ .push_back((id, PacketS::Interact { pos: None }));
+ }
+ let path = find_path(
+ &game.walkable,
+ player.position().as_ivec2(),
+ game.data.customer_spawn.as_ivec2(),
+ )
+ .ok_or(anyhow!("no path to exit"))?;
+ *self.chairs.get_mut(&chair).unwrap() = true;
+ game.demands_completed += 1;
+ game.points += demand.points;
+ game.score_changed = true;
+ info!("{id:?} -> exiting");
+ *state = CustomerState::Exiting { path }
+ }
+ }
+ CustomerState::Exiting { path } => {
+ player.direction = path.next_direction(player.position());
+ if path.is_done() {
+ info!("{id:?} -> leave");
+ self.cpackets.push_back((id, PacketS::Leave));
+ customers_to_remove.push(id);
+ }
+ }
+ }
+ }
+ for c in customers_to_remove {
+ self.customers.remove(&c).unwrap();
+ }
+ for (player, packet) in self.cpackets.drain(..) {
+ if let Err(err) = game.packet_in(player, packet) {
+ warn!("demand packet {err}");
+ }
+ }
+ Ok(())
+ }
+}
+impl Customers {
+ fn select_chair(&mut self) -> Option<IVec2> {
+ use rand::seq::IteratorRandom;
+ let (chosen, free) = self
+ .chairs
+ .iter_mut()
+ .filter(|(_p, free)| **free)
+ .choose(&mut thread_rng())?;
+ *free = false;
+ Some(*chosen)
+ }
+}