/*
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),
});
}
}