diff options
Diffstat (limited to 'server/src/entity')
-rw-r--r-- | server/src/entity/conveyor.rs | 34 | ||||
-rw-r--r-- | server/src/entity/customers/demands.rs | 94 | ||||
-rw-r--r-- | server/src/entity/customers/mod.rs | 274 | ||||
-rw-r--r-- | server/src/entity/customers/pathfinding.rs | 98 | ||||
-rw-r--r-- | server/src/entity/mod.rs | 43 | ||||
-rw-r--r-- | server/src/entity/portal.rs | 26 |
6 files changed, 514 insertions, 55 deletions
diff --git a/server/src/entity/conveyor.rs b/server/src/entity/conveyor.rs index 2d56c144..d1594ce7 100644 --- a/server/src/entity/conveyor.rs +++ b/server/src/entity/conveyor.rs @@ -16,13 +16,9 @@ */ use super::EntityT; -use crate::{ - data::Gamedata, - game::{interact_effect, Tile}, -}; +use crate::game::{interact_effect, Game}; use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation, PacketC}; -use std::collections::{HashMap, VecDeque}; +use hurrycurry_protocol::{glam::IVec2, ItemIndex, ItemLocation}; #[derive(Debug, Clone)] pub struct Conveyor { @@ -35,21 +31,18 @@ pub struct Conveyor { } impl EntityT for Conveyor { - fn tick( - &mut self, - data: &Gamedata, - points: &mut i64, - packet_out: &mut VecDeque<PacketC>, - tiles: &mut HashMap<IVec2, Tile>, - dt: f32, - ) -> Result<()> { - let from = tiles + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()> { + let from = game + .tiles .get(&self.from) .ok_or(anyhow!("conveyor from missing"))?; if let Some(from_item) = from.item.as_ref() { let filter = if let Some(t) = &self.filter_tile { - let filter_tile = tiles.get(t).ok_or(anyhow!("conveyor filter missing"))?; + let filter_tile = game + .tiles + .get(t) + .ok_or(anyhow!("conveyor filter missing"))?; filter_tile.item.as_ref().map(|e| e.kind) } else if let Some(i) = &self.filter_item { Some(*i) @@ -69,20 +62,21 @@ impl EntityT for Conveyor { } self.cooldown = 0.; - let [from, to] = tiles + let [from, to] = game + .tiles .get_many_mut([&self.from, &self.to]) .ok_or(anyhow!("conveyor does ends in itself"))?; interact_effect( - data, + &game.data, true, &mut to.item, ItemLocation::Tile(self.to), &mut from.item, ItemLocation::Tile(self.from), Some(to.kind), - packet_out, - points, + &mut game.packet_out, + &mut game.points, true, ); } diff --git a/server/src/entity/customers/demands.rs b/server/src/entity/customers/demands.rs new file mode 100644 index 00000000..fa7e0dbf --- /dev/null +++ b/server/src/entity/customers/demands.rs @@ -0,0 +1,94 @@ +/* + 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/>. + +*/ +use super::Demand; +use crate::interaction::Recipe; +use hurrycurry_protocol::{ItemIndex, TileIndex}; +use std::collections::{HashMap, HashSet}; + +pub fn generate_demands( + tiles: &HashSet<TileIndex>, + items: &HashSet<ItemIndex>, + raw_demands: &[(ItemIndex, Option<ItemIndex>, f32)], + recipes: &[Recipe], +) -> Vec<Demand> { + let recipes = recipes + .iter() + .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) + .collect::<Vec<_>>(); + + let mut producable = HashMap::new(); + + for i in items { + producable.insert(*i, 0.0); + } + + loop { + let prod_count = producable.len(); + + for r in &recipes { + let output_count = r.outputs().iter().filter(|o| !items.contains(&o)).count(); + let Some(ingred_cost) = r + .inputs() + .iter() + .map(|i| producable.get(i).copied()) + .reduce(|a, b| { + if let (Some(a), Some(b)) = (a, b) { + Some(a + b) + } else { + None + } + }) + .unwrap_or(Some(0.)) + else { + continue; + }; + + let base_cost = match r { + Recipe::Passive { duration, .. } => 2. + duration * 0.1, + Recipe::Active { duration, .. } => 2. + duration, + Recipe::Instant { .. } => 1., + }; + + let output_cost = (ingred_cost + base_cost) / output_count as f32; + for o in r.outputs() { + let cost = producable.entry(o).or_insert(f32::INFINITY); + *cost = cost.min(output_cost); + } + } + + if prod_count == producable.len() { + break; + } + } + + raw_demands + .iter() + .filter_map(|(i, o, d)| { + if let Some(cost) = producable.get(i) { + Some(Demand { + from: *i, + to: *o, + duration: *d, + points: *cost as i64, + }) + } else { + None + } + }) + .collect() +} 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) + } +} diff --git a/server/src/entity/customers/pathfinding.rs b/server/src/entity/customers/pathfinding.rs new file mode 100644 index 00000000..97bd8328 --- /dev/null +++ b/server/src/entity/customers/pathfinding.rs @@ -0,0 +1,98 @@ +/* + 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/>. + +*/ +use hurrycurry_protocol::glam::{IVec2, Vec2}; +use log::trace; +use std::{ + cmp::Ordering, + collections::{BinaryHeap, HashMap, HashSet}, +}; + +#[derive(Debug, Clone)] +pub struct Path(Vec<Vec2>); + +impl Path { + pub fn next_direction(&mut self, position: Vec2) -> Vec2 { + if let Some(next) = self.0.last().copied() { + trace!("next {next}"); + if next.distance(position) < if self.0.len() == 1 { 0.1 } else { 0.6 } { + self.0.pop(); + } + (next - position).normalize_or_zero() * 0.5 + } else { + Vec2::ZERO + } + } + pub fn is_done(&self) -> bool { + self.0.is_empty() + } +} + +pub fn find_path(walkable: &HashSet<IVec2>, from: IVec2, to: IVec2) -> Option<Path> { + #[derive(Debug, PartialEq, Eq)] + struct Open(i32, IVec2, IVec2, i32); + impl PartialOrd for Open { + fn partial_cmp(&self, other: &Self) -> Option<Ordering> { + self.0.partial_cmp(&other.0) + } + } + impl Ord for Open { + fn cmp(&self, other: &Self) -> Ordering { + self.0.cmp(&other.0) + } + } + + let mut visited = HashMap::new(); + let mut open = BinaryHeap::new(); + open.push(Open(1, from, from, 0)); + + loop { + let Some(Open(_, pos, f, distance)) = open.pop() else { + return None; + }; + if visited.contains_key(&pos) { + continue; + } + visited.insert(pos, f); + if pos == to { + break; + } + for dir in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] { + let next = pos + dir; + if walkable.contains(&next) { + open.push(Open( + -(distance + next.distance_squared(to).isqrt()), + next, + pos, + distance + 1, + )); + } + } + } + + let mut path = Vec::new(); + let mut c = to; + loop { + path.push(c.as_vec2() + 0.5); + let cn = visited[&c]; + if cn == c { + break; + } + c = cn + } + Some(Path(path)) +} diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index a1f690a3..beee9309 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -16,27 +16,20 @@ */ pub mod conveyor; +pub mod customers; pub mod portal; -use crate::{ - data::{Gamedata, ItemTileRegistry}, - game::Tile, -}; +use std::collections::{HashMap, HashSet}; + +use crate::{data::ItemTileRegistry, game::Game, interaction::Recipe}; use anyhow::{anyhow, Result}; use conveyor::Conveyor; -use hurrycurry_protocol::{glam::IVec2, PacketC}; +use customers::{demands::generate_demands, Customers}; +use hurrycurry_protocol::{glam::IVec2, ItemIndex, TileIndex}; use portal::Portal; use serde::{Deserialize, Serialize}; -use std::collections::{HashMap, VecDeque}; pub trait EntityT: Clone { - fn tick( - &mut self, - data: &Gamedata, - points: &mut i64, - packet_out: &mut VecDeque<PacketC>, - tiles: &mut HashMap<IVec2, Tile>, - dt: f32, - ) -> Result<()>; + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()>; } macro_rules! entities { @@ -44,14 +37,14 @@ macro_rules! entities { #[derive(Debug, Clone)] pub enum Entity { $($e($e)),* } impl EntityT for Entity { - fn tick(&mut self, data: &Gamedata, points: &mut i64, packet_out: &mut VecDeque<PacketC>, tiles: &mut HashMap<IVec2, Tile>, dt: f32) -> Result<()> { - match self { $(Entity::$e(x) => x.tick(data, points, packet_out, tiles, dt)),*, } + fn tick(&mut self, game: &mut Game, dt: f32) -> Result<()> { + match self { $(Entity::$e(x) => x.tick(game, dt)),*, } } } }; } -entities!(Conveyor, Portal); +entities!(Conveyor, Portal, Customers); #[derive(Debug, Clone, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] @@ -68,12 +61,18 @@ pub enum EntityDecl { from: Option<IVec2>, to: IVec2, }, + Customers {}, } pub fn construct_entity( pos: Option<IVec2>, decl: &EntityDecl, reg: &ItemTileRegistry, + tiles_used: &HashSet<TileIndex>, + items_used: &HashSet<ItemIndex>, + raw_demands: &[(ItemIndex, Option<ItemIndex>, f32)], + recipes: &[Recipe], + initial_map: &HashMap<IVec2, (TileIndex, Option<ItemIndex>)>, ) -> Result<Entity> { Ok(match decl.to_owned() { EntityDecl::Portal { from, to } => Entity::Portal(Portal { @@ -101,5 +100,15 @@ pub fn construct_entity( cooldown: 0., }) } + EntityDecl::Customers {} => { + let demands = generate_demands(tiles_used, items_used, &raw_demands, &recipes); + let chair = reg.register_tile("chair".to_string()); + let chairs = initial_map + .iter() + .filter(|(_, (tile, _))| *tile == chair) + .map(|(e, _)| (*e, true)) + .collect(); + Entity::Customers(Customers::new(chairs, demands)) + } }) } diff --git a/server/src/entity/portal.rs b/server/src/entity/portal.rs index 092a8da5..3aed35ac 100644 --- a/server/src/entity/portal.rs +++ b/server/src/entity/portal.rs @@ -16,13 +16,9 @@ */ use super::EntityT; -use crate::{ - data::Gamedata, - game::{interact_effect, Tile}, -}; +use crate::game::{interact_effect, Game}; use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{glam::IVec2, ItemLocation, PacketC}; -use std::collections::{HashMap, VecDeque}; +use hurrycurry_protocol::{glam::IVec2, ItemLocation}; #[derive(Debug, Default, Clone)] pub struct Portal { @@ -31,29 +27,23 @@ pub struct Portal { } impl EntityT for Portal { - fn tick( - &mut self, - data: &Gamedata, - points: &mut i64, - packet_out: &mut VecDeque<PacketC>, - tiles: &mut HashMap<IVec2, Tile>, - _dt: f32, - ) -> Result<()> { - let [from, to] = tiles + fn tick(&mut self, game: &mut Game, _dt: f32) -> Result<()> { + let [from, to] = game + .tiles .get_many_mut([&self.from, &self.to]) .ok_or(anyhow!("conveyor does ends in itself"))?; if from.item.is_some() { interact_effect( - data, + &game.data, true, &mut to.item, ItemLocation::Tile(self.to), &mut from.item, ItemLocation::Tile(self.from), Some(to.kind), - packet_out, - points, + &mut game.packet_out, + &mut game.points, true, ); } |