/* 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 super::{Entity, EntityContext}; use crate::{message::TrError, trm}; use anyhow::Result; use hurrycurry_protocol::{ glam::IVec2, ItemIndex, Message, PacketC, PlayerID, Recipe, RecipeIndex, TileIndex, }; use log::{debug, warn}; pub struct Tutorial { pub player: PlayerID, target: ItemIndex, next_update_due: f32, had_aquired_target: bool, current_hint: Option<(Option, Message)>, delete_timer: f32, pub ended: bool, } impl Tutorial { pub fn new(player: PlayerID, item: ItemIndex) -> Self { Self { ended: false, player, next_update_due: 0., target: item, current_hint: None, delete_timer: 1.5, had_aquired_target: false, } } } impl Entity for Tutorial { fn finished(&self) -> bool { self.ended } fn destructor(&mut self, c: EntityContext<'_>) { if let Some((position, _)) = self.current_hint { c.packet_out.push_back(PacketC::ServerHint { position, player: self.player, message: None, }); } c.packet_out.push_back(PacketC::TutorialEnded { player: self.player, item: self.target, success: false, }); } fn tick(&mut self, c: EntityContext<'_>) -> Result<()> { if self.ended { return Ok(()); } const TARGET_DT: f32 = 0.2; self.next_update_due -= c.dt; if self.next_update_due > 0. { return Ok(()); } self.next_update_due += TARGET_DT; let mut hint = StepContext { ent: &c, had_aquired_target: &mut self.had_aquired_target, player: self.player, recursion_abort: 0, } .fulfil_demand(self.target) .err(); if hint.is_none() { self.delete_timer -= TARGET_DT; if self.delete_timer <= 0. { self.ended = true; hint = None; c.packet_out.push_back(PacketC::TutorialEnded { item: self.target, player: self.player, success: true, }); } else { hint = Some((None, trm!("s.tutorial.finished"))); } } if hint != self.current_hint { if let Some((position, _)) = self.current_hint.take() { c.packet_out.push_back(PacketC::ServerHint { position, player: self.player, message: None, }); } if let Some((position, message)) = hint.clone() { c.packet_out.push_back(PacketC::ServerHint { player: self.player, position, message: Some(message), }); } self.current_hint = hint; } Ok(()) } fn interact( &mut self, _c: EntityContext<'_>, _pos: Option, _player: PlayerID, ) -> Result { Ok(false) } } struct StepContext<'a> { ent: &'a EntityContext<'a>, had_aquired_target: &'a mut bool, recursion_abort: usize, player: PlayerID, } impl StepContext<'_> { fn is_hand_item(&self, item: ItemIndex) -> bool { self.ent .game .players .get(&self.player) .is_some_and(|p| p.items.iter().flatten().any(|i| i.kind == item)) } pub fn find_demand(&self, item: ItemIndex) -> Option { self.ent .game .players .iter() .find_map(|(_, pl)| match &pl.communicate_persist { Some((Message::Item(i), _)) if *i == item => Some(pl.movement.position.as_ivec2()), _ => None, }) } fn find_recipe_with_output(&self, item: ItemIndex) -> Option { self.ent .game .data .recipes .iter() .enumerate() .find(|(_, r)| r.outputs().contains(&item)) .map(|(i, _)| RecipeIndex(i)) } fn find_item_on_map(&self, item: ItemIndex) -> Option { self.ent .game .tiles .iter() .find(|(_, t)| t.item.as_ref().is_some_and(|t| t.kind == item)) .map(|(p, _)| *p) } fn find_tile(&self, tile: TileIndex) -> Option { self.ent .game .tiles .iter() .find(|(_, t)| t.kind == tile) .map(|(p, _)| *p) } fn aquire_placed_item(&mut self, item: ItemIndex) -> Result, Message)> { debug!( "aquire placed item {:?}", self.ent.game.data.item_names[item.0] ); if let Some(pos) = self.find_item_on_map(item) { return Ok(pos); } self.aquire_item(item)?; Err((None, trm!("s.tutorial.put_away"))) } fn prevent_burning(&self) -> Result<(), (Option, Message)> { if let Some((pos, tile)) = self.ent.game.tiles.iter().find(|(_, t)| { t.item .as_ref() .is_some_and(|t| t.active.as_ref().is_some_and(|i| i.warn && i.speed > 0.)) }) { Err(( Some(*pos), match self.ent.game.data.tile_name(tile.kind).as_str() { "stove" | "oven" => trm!("s.tutorial.prevent_burning"), _ => trm!("s.tutorial.take_now"), }, )) } else { Ok(()) } } fn fulfil_demand(&mut self, item: ItemIndex) -> Result<(), (Option, Message)> { if self.ent.game.data.item_name(item) == "unknown-order" { return if let Some(pos) = self.find_demand(item) { Err((Some(pos), trm!("s.tutorial.accept_order"))) } else { Ok(()) }; } if !*self.had_aquired_target { self.prevent_burning()?; self.aquire_item(item)?; *self.had_aquired_target = true; } if self .ent .game .players .get(&self.player) .is_some_and(|p| p.items.iter().flatten().any(|i| i.kind == item)) { if let Some(pos) = self.find_demand(item) { Err((Some(pos), trm!("s.tutorial.serve"))) } else { Ok(()) } } else { Ok(()) } } fn aquire_item(&mut self, item: ItemIndex) -> Result<(), (Option, Message)> { debug!("aquire item {:?}", self.ent.game.data.item_names[item.0]); self.recursion_abort += 1; if self.recursion_abort > 32 { warn!("too much recursion"); return Err((None, trm!("s.tutorial.error"))); } if self.is_hand_item(item) { return Ok(()); } if let Some(pos) = self.find_item_on_map(item) { return Err((Some(pos), trm!("s.tutorial.pickup"))); } if let Some(recipe) = self.find_recipe_with_output(item) { let r = &self.ent.game.data.recipes[recipe.0]; match r { Recipe::Instant { tile: Some(tile), inputs: [None, None], .. } => { if let Some(pos) = self.find_tile(*tile) { return Err((Some(pos), trm!("s.tutorial.take", i = item))); } } Recipe::Instant { tile: None, inputs: [Some(a), Some(b)], .. } => { let apos = self.aquire_placed_item(*a)?; self.aquire_item(*b)?; let aname = self.ent.game.data.item_name(*a); let bname = self.ent.game.data.item_name(*b); return Err(( Some(apos), if aname.starts_with("plate:") || bname.starts_with("plate:") { trm!("s.tutorial.interact_plate") } else { trm!("s.tutorial.interact") }, )); } Recipe::Instant { tile: None, inputs: [Some(input), None], .. } => { self.aquire_item(*input)?; return Err((None, trm!("s.tutorial.interact_empty"))); } Recipe::Active { tile: Some(tile), input, speed, .. } => { for (pos, tile) in self.ent.game.tiles.iter().filter(|(_, t)| t.kind == *tile) { if let Some(item) = &tile.item { if item.kind == *input { return Err((Some(*pos), trm!("s.tutorial.hold_interact"))); } } } if let Some(pos) = self.find_tile(*tile) { self.aquire_item(*input)?; return Err(( Some(pos), if self.ent.game.data.tile_name(*tile) == "cuttingboard" { trm!("s.tutorial.active_cuttingboard") } else { trm!("s.tutorial.active", s = format!("{:.01}", 1. / speed)) }, )); } } Recipe::Passive { tile: Some(tile), input, .. } => { for (pos, tile) in self.ent.game.tiles.iter().filter(|(_, t)| t.kind == *tile) { if let Some(item) = &tile.item { if item.kind == *input { return Err((Some(*pos), trm!("s.tutorial.wait_finish"))); } } } if let Some(pos) = self.find_tile(*tile) { self.aquire_item(*input)?; return Err((Some(pos), trm!("s.tutorial.put_on", t = *tile))); } } Recipe::Passive { tile: None, input, .. } => { let pos = self.aquire_placed_item(*input)?; return Err((Some(pos), trm!("s.tutorial.wait_finish"))); } _ => warn!("recipe too hard {r:?}"), } } warn!( "stuck at making item {:?}", self.ent.game.data.item_names[item.0] ); Err((None, trm!("s.tutorial.error"))) } }