use super::{Entity, EntityContext}; use crate::{trm, TrError}; 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, } impl Tutorial { pub fn new(player: PlayerID, item: ItemIndex) -> Self { Self { 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.delete_timer <= 0. } 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<()> { 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. { 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<'a> StepContext<'a> { fn is_hand_item(&self, item: ItemIndex) -> bool { self.ent .game .players .get(&self.player) .map_or(false, |p| p.item.as_ref().map_or(false, |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().map_or(false, |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 fulfil_demand(&mut self, item: ItemIndex) -> Result<(), (Option, Message)> { if !*self.had_aquired_target { self.aquire_item(item)?; *self.had_aquired_target = true; } if let Some(pos) = self.find_demand(item) { Err((Some(pos), trm!("s.tutorial.serve"))) } 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)?; return Err((Some(apos), 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, .. } => { if let Some(pos) = self.find_tile(*tile) { if let Some(item) = &self.ent.game.tiles.get(&pos).unwrap().item { if item.kind == *input { return Err((Some(pos), trm!("s.tutorial.hold_interact"))); } } self.aquire_item(*input)?; return Err(( Some(pos), trm!("s.tutorial.active", s = format!("{:.01}", 1. / speed)), )); } } Recipe::Passive { tile: Some(tile), input, .. } => { if let Some(pos) = self.find_tile(*tile) { if let Some(item) = &self.ent.game.tiles.get(&pos).unwrap().item { if item.kind == *input { return Err((Some(pos), trm!("s.tutorial.wait_finish"))); } } 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"))) } }