/* 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 . */ 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, this_had_item: bool, other_had_item: bool, this: &Option, this_loc: ItemLocation, other: &Option, 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), }); } }