aboutsummaryrefslogtreecommitdiff
path: root/server/game-core/src
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-10-19 23:50:23 +0200
committermetamuffin <metamuffin@disroot.org>2025-10-19 23:50:23 +0200
commitab83f982601d93b2399102c4d030fd6e13c4c735 (patch)
treec0536ca9e328707d6b4f4cfc7a2307713466a5be /server/game-core/src
parent231a5ce21fcee9195fcc504ee672e4464d627c47 (diff)
downloadhurrycurry-ab83f982601d93b2399102c4d030fd6e13c4c735.tar
hurrycurry-ab83f982601d93b2399102c4d030fd6e13c4c735.tar.bz2
hurrycurry-ab83f982601d93b2399102c4d030fd6e13c4c735.tar.zst
Refactor and move interaction code
Diffstat (limited to 'server/game-core/src')
-rw-r--r--server/game-core/src/interaction.rs442
-rw-r--r--server/game-core/src/lib.rs4
2 files changed, 445 insertions, 1 deletions
diff --git a/server/game-core/src/interaction.rs b/server/game-core/src/interaction.rs
new file mode 100644
index 00000000..407294d6
--- /dev/null
+++ b/server/game-core/src/interaction.rs
@@ -0,0 +1,442 @@
+/*
+ Hurry Curry! - a game about cooking
+ Copyright (C) 2025 Hurry Curry! Contributors
+
+ 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 crate::{Game, Involvement, Item};
+use hurrycurry_locale::{TrError, tre};
+use hurrycurry_protocol::{ItemLocation, PacketC, Recipe};
+use log::info;
+use std::collections::{BTreeSet, VecDeque};
+
+impl Game {
+ pub fn interact(
+ &mut self,
+ this_loc: ItemLocation,
+ other_loc: ItemLocation,
+ edge: bool,
+ ) -> Result<(), TrError> {
+ let automated = this_loc.is_tile() && other_loc.is_tile();
+ let other_player_id = match other_loc {
+ ItemLocation::Player(pid, _) => Some(pid),
+ _ => None,
+ };
+ let this_tile_kind = match this_loc {
+ ItemLocation::Tile(t) => self.tiles.get(&t).map(|t| t.kind),
+ _ => None,
+ };
+
+ let (this_slot, other_slot) = match (this_loc, other_loc) {
+ (ItemLocation::Tile(p1), ItemLocation::Tile(p2)) => {
+ if p1 == p2 {
+ return Err(tre!("s.error.self_interact"));
+ }
+ let [Some(x), Some(y)] = self.tiles.get_disjoint_mut([&p1, &p2]) else {
+ return Err(tre!("s.error.no_tile"));
+ };
+ (&mut x.item, &mut y.item)
+ }
+ (ItemLocation::Tile(p1), ItemLocation::Player(p2, h2)) => {
+ let Some(x) = self.tiles.get_mut(&p1) else {
+ return Err(tre!("s.error.no_tile"));
+ };
+ let Some(y) = self.players.get_mut(&p2) else {
+ return Err(tre!("s.error.no_player"));
+ };
+ let Some(y) = y.items.get_mut(h2.0) else {
+ return Err(tre!("s.error.no_hand"));
+ };
+ (&mut x.item, y)
+ }
+ (ItemLocation::Player(p1, h1), ItemLocation::Tile(p2)) => {
+ let Some(x) = self.players.get_mut(&p1) else {
+ return Err(tre!("s.error.no_player"));
+ };
+ let Some(x) = x.items.get_mut(h1.0) else {
+ return Err(tre!("s.error.no_hand"));
+ };
+ let Some(y) = self.tiles.get_mut(&p2) else {
+ return Err(tre!("s.error.no_tile"));
+ };
+ (x, &mut y.item)
+ }
+ (ItemLocation::Player(p1, h1), ItemLocation::Player(p2, h2)) => {
+ if p1 == p2 {
+ return Err(tre!("s.error.self_interact"));
+ }
+ let [Some(x), Some(y)] = self.players.get_disjoint_mut([&p1, &p2]) else {
+ return Err(tre!("s.error.no_tile"));
+ };
+ let Some(x) = x.items.get_mut(h1.0) else {
+ return Err(tre!("s.error.no_hand"));
+ };
+ let Some(y) = y.items.get_mut(h2.0) else {
+ return Err(tre!("s.error.no_hand"));
+ };
+ (x, y)
+ }
+ };
+
+ if other_slot.is_none()
+ && let Some(item) = this_slot
+ && let Some(inv) = &mut item.active
+ {
+ let recipe = &self.data.recipe(inv.recipe);
+ if recipe.supports_tile(this_tile_kind)
+ && let Recipe::Active { outputs, speed, .. } = recipe
+ {
+ if edge {
+ inv.players.extend(other_player_id);
+ } else {
+ if let Some(player) = other_player_id {
+ inv.players.remove(&player);
+ }
+ }
+ inv.speed = speed * inv.players.len() as f32;
+
+ if inv.position >= 1. {
+ let this_had_item = this_slot.is_some();
+ let other_had_item = other_slot.is_some();
+ *other_slot = outputs[0].map(|kind| Item { kind, active: None });
+ *this_slot = outputs[1].map(|kind| Item { kind, active: None });
+ self.item_locations_index.remove(&this_loc);
+ self.item_locations_index.remove(&other_loc);
+ if this_slot.is_some() {
+ self.item_locations_index.insert(this_loc);
+ }
+ if other_slot.is_some() {
+ self.item_locations_index.insert(other_loc);
+ }
+ produce_events(
+ &mut self.events,
+ this_had_item,
+ other_had_item,
+ this_slot,
+ this_loc,
+ other_slot,
+ other_loc,
+ );
+ } else {
+ self.events.push_back(PacketC::SetProgress {
+ players: inv.players.clone(),
+ item: this_loc,
+ position: inv.position,
+ speed: inv.speed,
+ warn: inv.warn,
+ });
+ }
+ return Ok(());
+ }
+ }
+ if !edge {
+ return Ok(());
+ }
+ for (ri, recipe) in self.data.recipes() {
+ if !recipe.supports_tile(this_tile_kind) {
+ continue;
+ }
+ match recipe {
+ Recipe::Active { input, speed, .. } => {
+ if other_slot.is_none()
+ && let Some(item) = this_slot
+ && item.kind == *input
+ && item.active.is_none()
+ {
+ info!("start active {ri}");
+ item.active = Some(Involvement {
+ players: other_player_id.into_iter().collect(),
+ recipe: ri,
+ speed: *speed,
+ position: 0.,
+ warn: false,
+ });
+ }
+ if this_slot.is_none()
+ && let Some(item) = &other_slot
+ && item.kind == *input
+ && item.active.is_none()
+ {
+ let mut item = other_slot.take().unwrap();
+ info!("start active {ri}");
+ item.active = Some(Involvement {
+ players: other_player_id.into_iter().collect(),
+ recipe: ri,
+ speed: *speed,
+ position: 0.,
+ warn: false,
+ });
+ self.item_locations_index.remove(&other_loc);
+ self.item_locations_index.insert(this_loc);
+ *this_slot = Some(item);
+ self.score.active_recipes += 1;
+ self.events.push_back(PacketC::MoveItem {
+ from: other_loc,
+ to: this_loc,
+ });
+ self.events.push_back(PacketC::SetProgress {
+ players: other_player_id.into_iter().collect(),
+ item: this_loc,
+ position: 0.,
+ speed: *speed,
+ warn: false,
+ });
+ return Ok(());
+ }
+ }
+ Recipe::Instant {
+ inputs,
+ outputs,
+ points: pd,
+ ..
+ } => {
+ let on_tile = this_slot.as_ref().map(|i| i.kind);
+ let in_hand = other_slot.as_ref().map(|i| i.kind);
+ let ok = inputs[0] == on_tile && inputs[1] == in_hand;
+ let ok_rev = inputs[1] == on_tile && inputs[0] == in_hand;
+ if ok || ok_rev {
+ info!("instant {ri} reversed={ok_rev}");
+ let ok_rev = ok_rev as usize;
+ let this_had_item = this_slot.is_some();
+ let other_had_item = other_slot.is_some();
+ *other_slot = outputs[1 - ok_rev].map(|kind| Item { kind, active: None });
+ *this_slot = outputs[ok_rev].map(|kind| Item { kind, active: None });
+ self.item_locations_index.remove(&this_loc);
+ self.item_locations_index.remove(&other_loc);
+ if this_slot.is_some() {
+ self.item_locations_index.insert(this_loc);
+ }
+ if other_slot.is_some() {
+ self.item_locations_index.insert(other_loc);
+ }
+ self.score.points += pd;
+ self.score.instant_recipes += 1;
+ self.events.push_back(PacketC::Score(self.score.clone()));
+ produce_events(
+ &mut self.events,
+ this_had_item,
+ other_had_item,
+ this_slot,
+ this_loc,
+ other_slot,
+ other_loc,
+ );
+ return Ok(());
+ }
+ }
+ _ => (),
+ }
+ }
+
+ let can_place = automated
+ || this_tile_kind.is_none_or(|tile| {
+ other_slot.as_ref().is_some_and(|other| {
+ self.data
+ .tile_placeable_items
+ .get(&tile)
+ .is_none_or(|pl| pl.contains(&other.kind))
+ })
+ });
+
+ if can_place
+ && this_slot.is_none()
+ && let Some(item) = other_slot.take()
+ {
+ self.item_locations_index.remove(&other_loc);
+ self.item_locations_index.insert(this_loc);
+ *this_slot = Some(item);
+ self.events.push_back(PacketC::MoveItem {
+ from: other_loc,
+ to: this_loc,
+ });
+ return Ok(());
+ }
+ if other_slot.is_none()
+ && let Some(item) = this_slot.take()
+ {
+ self.item_locations_index.remove(&this_loc);
+ self.item_locations_index.insert(other_loc);
+ *other_slot = Some(item);
+ self.events.push_back(PacketC::MoveItem {
+ from: this_loc,
+ to: other_loc,
+ });
+ }
+
+ Ok(())
+ }
+
+ pub fn tick_slot(&mut self, loc: ItemLocation, dt: f32) -> Result<(), TrError> {
+ let tile = match loc {
+ ItemLocation::Tile(t) => self.tiles.get(&t).map(|t| t.kind),
+ _ => None,
+ };
+ let slot = match loc {
+ ItemLocation::Tile(p) => {
+ let Some(x) = self.tiles.get_mut(&p) else {
+ return Err(tre!("s.error.no_tile"));
+ };
+ &mut x.item
+ }
+ ItemLocation::Player(p, h) => {
+ let Some(x) = self.players.get_mut(&p) else {
+ return Err(tre!("s.error.no_player"));
+ };
+ let Some(x) = x.items.get_mut(h.0) else {
+ return Err(tre!("s.error.no_hand"));
+ };
+ x
+ }
+ };
+
+ if let Some(item) = slot {
+ if let Some(a) = &mut item.active {
+ let r = &self.data.recipe(a.recipe);
+ let prev_speed = a.speed;
+
+ if r.supports_tile(tile) {
+ if a.speed <= 0.
+ && let Recipe::Passive { speed, .. } = &self.data.recipe(a.recipe)
+ {
+ a.speed = *speed;
+ }
+ } else if let Some(revert_speed) = r.revert_speed() {
+ a.speed = -revert_speed
+ } else {
+ a.speed = 0.;
+ }
+
+ if a.position < 0. {
+ item.active = None;
+ self.events.push_back(PacketC::ClearProgress { item: loc });
+ return Ok(());
+ }
+ if a.position >= 1.
+ && let Recipe::Passive { output, warn, .. } = &self.data.recipe(a.recipe)
+ {
+ *slot = output.map(|kind| Item { kind, active: None });
+ self.score.passive_recipes += 1;
+ self.events.push_back(PacketC::Score(self.score.clone()));
+ self.events.push_back(PacketC::SetProgress {
+ players: BTreeSet::new(),
+ warn: *warn,
+ item: loc,
+ position: 1.,
+ speed: 0.,
+ });
+ self.events.push_back(PacketC::SetItem {
+ location: loc,
+ item: slot.as_ref().map(|i| i.kind),
+ });
+ return Ok(());
+ };
+
+ a.position += dt * a.speed;
+ a.position = a.position.min(1.);
+
+ if a.speed != prev_speed {
+ self.events.push_back(PacketC::SetProgress {
+ players: a.players.clone(),
+ position: a.position,
+ speed: a.speed,
+ warn: a.warn,
+ item: loc,
+ });
+ }
+ } else if let Some(recipes) = self.data_index.recipe_passive_by_input.get(&item.kind) {
+ for &ri in recipes {
+ let recipe = self.data.recipe(ri);
+ if recipe.supports_tile(tile)
+ && let Recipe::Passive {
+ input, warn, speed, ..
+ } = recipe
+ && *input == item.kind
+ {
+ item.active = Some(Involvement {
+ players: BTreeSet::new(),
+ recipe: ri,
+ position: 0.,
+ warn: *warn,
+ speed: *speed,
+ });
+ self.events.push_back(PacketC::SetProgress {
+ players: BTreeSet::new(),
+ position: 0.,
+ speed: *speed,
+ warn: *warn,
+ item: loc,
+ });
+ return Ok(());
+ }
+ }
+ }
+ }
+ Ok(())
+ }
+}
+
+pub enum TickEffect {
+ Progress {
+ speed: f32,
+ position: f32,
+ warn: bool,
+ },
+ ClearProgress,
+ Produce,
+}
+
+#[allow(clippy::too_many_arguments)]
+fn produce_events(
+ events: &mut VecDeque<PacketC>,
+ this_had_item: bool,
+ other_had_item: bool,
+ this: &Option<Item>,
+ this_loc: ItemLocation,
+ other: &Option<Item>,
+ other_loc: ItemLocation,
+) {
+ info!("produce {this_loc} <~ {other_loc}");
+ if this_had_item {
+ events.push_back(PacketC::SetItem {
+ location: this_loc,
+ item: None,
+ });
+ }
+ if other_had_item {
+ events.push_back(PacketC::MoveItem {
+ from: other_loc,
+ to: this_loc,
+ });
+ events.push_back(PacketC::SetItem {
+ location: this_loc,
+ item: None,
+ });
+ }
+ if let Some(i) = &other {
+ events.push_back(PacketC::SetItem {
+ location: this_loc,
+ item: Some(i.kind),
+ });
+ events.push_back(PacketC::MoveItem {
+ from: this_loc,
+ to: other_loc,
+ })
+ }
+ if let Some(i) = &this {
+ events.push_back(PacketC::SetItem {
+ location: this_loc,
+ item: Some(i.kind),
+ });
+ }
+}
diff --git a/server/game-core/src/lib.rs b/server/game-core/src/lib.rs
index cc77e570..994398c9 100644
--- a/server/game-core/src/lib.rs
+++ b/server/game-core/src/lib.rs
@@ -16,9 +16,11 @@
*/
pub mod gamedata_index;
+pub mod interaction;
pub mod network;
pub mod spatial_index;
+use crate::gamedata_index::GamedataIndex;
use hurrycurry_protocol::{
Character, Gamedata, Hand, ItemIndex, ItemLocation, Message, MessageTimeout, PacketC,
PlayerClass, PlayerID, RecipeIndex, Score, TileIndex, glam::IVec2, movement::MovementBase,
@@ -29,7 +31,6 @@ use std::{
sync::Arc,
time::Instant,
};
-use crate::gamedata_index::GamedataIndex;
#[derive(Debug, Clone, PartialEq)]
pub struct Involvement {
@@ -78,6 +79,7 @@ pub struct Game {
pub players_spatial_index: SpatialIndex<PlayerID>,
pub walkable: HashSet<IVec2>,
pub tile_index: HashMap<TileIndex, HashSet<IVec2>>,
+ pub item_locations_index: HashSet<ItemLocation>,
pub events: VecDeque<PacketC>,
}