aboutsummaryrefslogtreecommitdiff
path: root/server/src/entity
diff options
context:
space:
mode:
Diffstat (limited to 'server/src/entity')
-rw-r--r--server/src/entity/conveyor.rs34
-rw-r--r--server/src/entity/customers/demands.rs94
-rw-r--r--server/src/entity/customers/mod.rs274
-rw-r--r--server/src/entity/customers/pathfinding.rs98
-rw-r--r--server/src/entity/mod.rs43
-rw-r--r--server/src/entity/portal.rs26
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,
);
}