/* 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::{ pathfinding::{find_path, Path}, BotAlgo, BotInput, }; use hurrycurry_client_lib::Game; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, DemandIndex, Message, PacketS, PlayerClass, PlayerID, Score, }; use log::info; use rand::{random, seq::IndexedRandom, thread_rng}; #[derive(Debug, Clone, Default)] pub struct Customer { config: CustomerConfig, state: CustomerState, } #[derive(Debug, Clone, Default)] pub struct CustomerConfig { pub unknown_order: bool, } #[derive(Debug, Clone, Default)] enum CustomerState { #[default] New, Entering { path: Path, chair: IVec2, origin: IVec2, ticks: usize, }, Waiting { demand: DemandIndex, chair: IVec2, facing: Vec2, timeout: f32, origin: IVec2, check: u8, pinned: bool, }, Eating { demand: DemandIndex, table: IVec2, progress: f32, chair: IVec2, origin: IVec2, }, Finishing { table: IVec2, origin: IVec2, cooldown: f32, }, Exiting { path: Path, }, } impl Customer { pub fn new(config: CustomerConfig) -> Self { Customer { config, state: CustomerState::default(), } } } impl BotAlgo for Customer { fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput { let Some(playerdata) = game.players.get(&me) else { return BotInput::default(); }; let pos = playerdata.movement.position; self.state.tick(me, &self.config, pos, game, dt) } } impl CustomerState { fn tick( &mut self, me: PlayerID, config: &CustomerConfig, pos: Vec2, game: &Game, dt: f32, ) -> BotInput { match self { CustomerState::New => { if !game.data.demands.is_empty() { if let Some(&chair) = game .tiles .iter() .filter(|(_, t)| game.data.tile_name(t.kind) == "chair") .map(|(p, _)| *p) .collect::>() .choose(&mut thread_rng()) { if let Some(path) = find_path(&game.walkable, pos.as_ivec2(), chair) { info!("{me:?} -> entering"); *self = CustomerState::Entering { path, chair, origin: pos.as_ivec2(), ticks: 0, }; } } } BotInput::default() } CustomerState::Entering { path, chair, origin, ticks, } => { *ticks += 1; let check = *ticks % 10 == 0; if path.is_done() { let demand = DemandIndex(random::() as usize % game.data.demands.len()); info!("{me:?} -> waiting"); let timeout = 90. + random::() * 60.; let mut facing = Vec2::ZERO; for off in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { if game .tiles .get(&(off + *chair)) .map_or(false, |t| game.data.is_tile_interactable(t.kind)) { facing = off.as_vec2(); } } *self = CustomerState::Waiting { chair: *chair, timeout, demand, facing, origin: *origin, check: 0, pinned: false, }; let message_item = if config.unknown_order { game.data .get_item_by_name("unknown-order") .unwrap_or(game.data.demands[demand.0].input) } else { game.data.demands[demand.0].input }; BotInput { extra: vec![PacketS::Communicate { message: Some(Message::Item(message_item)), timeout: Some(timeout), player: me, pin: Some(false), }], ..Default::default() } } else if check && path.remaining_segments() < 5 && game .players .iter() .find(|(id, p)| { p.class == PlayerClass::Customer && **id != me && p.movement.position.distance(chair.as_vec2() + 0.5) < 1. }) .is_some() { *self = CustomerState::New; BotInput::default() } else if path.is_stuck() { if let Some(path) = find_path(&game.walkable, pos.as_ivec2(), *origin) { *self = CustomerState::Exiting { path }; } BotInput::default() } else { BotInput { direction: path.next_direction(pos, dt) * 0.6, ..Default::default() } } } CustomerState::Waiting { chair, demand, timeout, origin, check, pinned, facing, } => { *timeout -= dt; *check += 1; if *timeout <= 0. { let path = find_path(&game.walkable, pos.as_ivec2(), *origin) .expect("no path to exit"); info!("{me:?} -> exiting"); *self = CustomerState::Exiting { path }; return BotInput { extra: vec![ PacketS::Communicate { message: None, timeout: Some(0.), player: me, pin: Some(false), }, PacketS::ApplyScore(Score { points: -1, demands_failed: 1, ..Default::default() }), PacketS::Effect { name: "angry".to_string(), player: me, }, ], ..Default::default() }; } else if *check > 10 { let demand_data = &game.data.demands[demand.0]; *check = 0; if !*pinned { let mut pin = false; if config.unknown_order { game.players_spatial_index.query(pos, 3., |pid, _| { if game .players .get(&pid) .map_or(false, |p| p.class.is_cheflike()) { pin = true } }); } else { pin = true; } if pin { *pinned = true; return BotInput { extra: vec![PacketS::Communicate { player: me, message: Some(Message::Item(demand_data.input)), timeout: Some(*timeout), pin: Some(true), }], ..Default::default() }; } } 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.input) .unwrap_or_default() }) .unwrap_or_default() { Some(pos) } else { None } }); if let Some(pos) = demand_pos { info!("{me:?} -> eating"); let points = game.data.demands[demand.0].points; *self = CustomerState::Eating { demand: *demand, table: pos, progress: 0., chair: *chair, origin: *origin, }; return BotInput { extra: vec![ PacketS::Communicate { message: None, timeout: Some(0.), player: me, pin: Some(false), }, PacketS::Communicate { message: None, timeout: Some(0.), player: me, pin: Some(true), }, PacketS::Effect { name: "satisfied".to_string(), player: me, }, PacketS::Interact { pos: Some(pos), player: me, }, PacketS::ApplyScore(Score { demands_completed: 1, points, ..Default::default() }), PacketS::Interact { pos: None, player: me, }, ], ..Default::default() }; } } BotInput { direction: dir_input(pos, *chair, *facing), ..Default::default() } } CustomerState::Eating { demand, table, progress, chair, origin, } => { let demand = &game.data.demands[demand.0]; *progress += dt / demand.duration; if *progress >= 1. { info!("{me:?} -> finishing"); *self = CustomerState::Finishing { table: *table, origin: *origin, cooldown: 0.5, }; return BotInput { extra: vec![PacketS::ReplaceHand { player: me, item: demand.output, }], ..Default::default() }; } BotInput { direction: dir_input(pos, *chair, (*table - *chair).as_vec2()), ..Default::default() } } CustomerState::Finishing { table, origin, cooldown, } => { *cooldown -= dt; if game.players.get(&me).map_or(false, |pl| pl.item.is_none()) { if let Some(path) = find_path(&game.walkable, pos.as_ivec2(), *origin) { *self = CustomerState::Exiting { path }; } BotInput::default() } else { let direction = (table.as_vec2() + 0.5) - pos; if *cooldown < 0. { *cooldown += 1.; BotInput { extra: vec![ PacketS::Interact { player: me, pos: Some(*table), }, PacketS::Interact { player: me, pos: None, }, ], direction, ..Default::default() } } else { BotInput { direction, ..Default::default() } } } } CustomerState::Exiting { path } => { if path.is_done() || path.is_stuck() { info!("{me:?} -> leave"); BotInput { leave: true, ..Default::default() } } else { BotInput { direction: path.next_direction(pos, dt) * 0.6, ..Default::default() } } } } } } fn dir_input(pos: Vec2, target: IVec2, facing: Vec2) -> Vec2 { let diff = (target.as_vec2() + 0.5) - pos; (if diff.length() > 0.3 { diff.normalize() } else { diff * 0.5 }) + facing.clamp_length_max(1.) * 0.3 }