/* 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 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, PacketC, 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, cpackets: VecDeque<(PlayerID, PacketS)>, chairs: HashMap, customer_id_counter: PlayerID, customers: HashMap, 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, demands: Vec) -> 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, packet_out: &mut VecDeque, 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::() * 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::() 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::() % 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::() * 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.score.demands_failed += 1; game.score.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.score.demands_completed += 1; game.score.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, &mut vec![], packet_out) { warn!("demand packet {err}"); } } Ok(()) } } impl Customers { 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) } }