From 7cacd111e2b443bac291244b168c20e2c8bf69ec Mon Sep 17 00:00:00 2001 From: metamuffin Date: Tue, 17 Sep 2024 16:57:23 +0200 Subject: add hint-based tutorial for item crafting --- server/src/commands.rs | 30 +++++- server/src/entity/bot.rs | 5 +- server/src/entity/customers.rs | 2 +- server/src/entity/mod.rs | 3 + server/src/entity/tutorial.rs | 218 ++++++++++++++++++++++++++++++++++++++++- server/src/server.rs | 2 + 6 files changed, 251 insertions(+), 9 deletions(-) (limited to 'server/src') diff --git a/server/src/commands.rs b/server/src/commands.rs index 96d9ab75..904305bf 100644 --- a/server/src/commands.rs +++ b/server/src/commands.rs @@ -15,7 +15,10 @@ along with this program. If not, see . */ -use crate::{entity::bot::BotDriver, server::Server}; +use crate::{ + entity::{bot::BotDriver, tutorial::Tutorial}, + server::Server, +}; use anyhow::{anyhow, bail, Result}; use clap::{Parser, ValueEnum}; use hurrycurry_bot::algos::ALGO_CONSTRUCTORS; @@ -58,18 +61,28 @@ enum Command { /// List all recipes and maps List, /// Send an effect - Effect { name: String }, + Effect { + name: String, + }, /// Send an item - Item { name: String }, + Item { + name: String, + }, /// Reload the resource index ReloadIndex, #[clap(alias = "summon-bot", alias = "spawn-bot")] - CreateBot { algo: String, name: Option }, + CreateBot { + algo: String, + name: Option, + }, /// Reload the current map #[clap(alias = "r")] Reload, /// Shows the recipe book Book, + StartTutorial { + item: String, + }, } #[derive(ValueEnum, Clone)] @@ -217,6 +230,15 @@ impl Server { info.players ) } + Command::StartTutorial { item } => { + let item = self + .game + .data + .get_item_by_name(&item) + .ok_or(anyhow!("unknown item"))?; + // TODO prevent too many (> 1) tutorials for this player + self.entities.push(Box::new(Tutorial::new(player, item))); + } } Ok(()) } diff --git a/server/src/entity/bot.rs b/server/src/entity/bot.rs index 79ca97f1..6f39fab2 100644 --- a/server/src/entity/bot.rs +++ b/server/src/entity/bot.rs @@ -29,7 +29,7 @@ pub struct BotDriver { join_data: Option<(String, i32)>, id: PlayerID, interacting: bool, - pub left: bool, + left: bool, } impl BotDriver { @@ -44,6 +44,9 @@ impl BotDriver { } } impl Entity for BotDriver { + fn finished(&self) -> bool { + self.left + } fn tick(&mut self, c: EntityContext<'_>) -> Result<()> { if let Some((name, character)) = self.join_data.take() { self.id = PlayerID(random()); // TODO bad code, can collide diff --git a/server/src/entity/customers.rs b/server/src/entity/customers.rs index da905bc0..8c515e92 100644 --- a/server/src/entity/customers.rs +++ b/server/src/entity/customers.rs @@ -58,7 +58,7 @@ impl Entity for Customers { load_map: c.load_map, })?; } - self.customers.retain(|c| !c.left); + self.customers.retain(|c| !c.finished()); Ok(()) } } diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index 70c5c785..f87dbb32 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -59,6 +59,9 @@ pub trait Entity { fn tick(&mut self, _c: EntityContext<'_>) -> Result<()> { Ok(()) } + fn finished(&self) -> bool { + false + } fn destructor(&mut self, _c: EntityContext<'_>) {} fn interact( &mut self, diff --git a/server/src/entity/tutorial.rs b/server/src/entity/tutorial.rs index 20b1d03e..24c83550 100644 --- a/server/src/entity/tutorial.rs +++ b/server/src/entity/tutorial.rs @@ -1,16 +1,68 @@ use super::{Entity, EntityContext}; use anyhow::Result; -use hurrycurry_protocol::{glam::IVec2, PlayerID}; +use hurrycurry_protocol::{ + glam::IVec2, ItemIndex, Message, PacketC, PlayerID, Recipe, RecipeIndex, TileIndex, +}; +use log::{debug, warn}; pub struct Tutorial { player: PlayerID, + target: ItemIndex, + + current_hint: Option<(Option, Message)>, + delete_timer: f32, +} + +impl Tutorial { + pub fn new(player: PlayerID, item: ItemIndex) -> Self { + Self { + player, + target: item, + current_hint: None, + delete_timer: 1.5, + } + } } impl Entity for Tutorial { - fn tick(&mut self, _c: EntityContext<'_>) -> Result<()> { + fn finished(&self) -> bool { + self.delete_timer <= 0. + } + fn tick(&mut self, c: EntityContext<'_>) -> Result<()> { + let mut hint = StepContext { + ent: &c, + player: self.player, + recursion_abort: 0, + } + .aquire_item(self.target) + .err(); + if hint.is_none() { + self.delete_timer -= c.dt; + if self.delete_timer <= 0. { + hint = None + } else { + hint = Some((None, Message::Text("Tutorial finished.".to_string()))); + } + } + + if hint != self.current_hint { + if let Some((position, _)) = self.current_hint.take() { + c.packet_out.push_back(PacketC::ServerHint { + position, + message: None, + }); + } + if let Some((position, message)) = hint.clone() { + c.packet_out.push_back(PacketC::ServerHint { + position, + message: Some(message), + }); + } + self.current_hint = hint; + } + Ok(()) } - fn destructor(&mut self, _c: EntityContext<'_>) {} fn interact( &mut self, _c: EntityContext<'_>, @@ -20,3 +72,163 @@ impl Entity for Tutorial { Ok(false) } } + +struct StepContext<'a> { + ent: &'a EntityContext<'a>, + 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)) + } + 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, Message::Text("put down this item".to_string()))) + } + 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, + Message::Text("server cant handle the recipe, too much recursion".to_string()), + )); + } + if self.is_hand_item(item) { + return Ok(()); + } + if let Some(pos) = self.find_item_on_map(item) { + return Err((Some(pos), Message::Text("pickup".to_string()))); + } + 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), Message::Text("take from crate".to_string()))); + } + } + Recipe::Instant { + tile: None, + inputs: [Some(a), Some(b)], + .. + } => { + let apos = self.aquire_placed_item(*a)?; + self.aquire_item(*b)?; + return Err((Some(apos), Message::Text("interact here".to_string()))); + } + Recipe::Instant { + tile: None, + inputs: [Some(input), None], + .. + } => { + self.aquire_item(*input)?; + return Err((None, Message::Text("interact with empty tile".to_string()))); + } + 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), + Message::Text(format!("hold interact here")), + )); + } else { + return Err((Some(pos), Message::Text(format!("clear tile")))); + } + } + self.aquire_item(*input)?; + return Err(( + Some(pos), + Message::Text(format!("active here for {:.01}s", 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), Message::Text(format!("wait for finish")))); + } else { + return Err((Some(pos), Message::Text(format!("clear tile")))); + } + } + self.aquire_item(*input)?; + return Err(( + Some(pos), + Message::Text(format!( + "put on {}", + self.ent.game.data.tile_name(*tile) + )), + )); + } + } + Recipe::Passive { + tile: None, input, .. + } => { + self.aquire_item(*input)?; + return Err((None, Message::Text(format!("wait for finish")))); + } + _ => warn!("recipe too hard {r:?}"), + } + } + warn!( + "stuck at making item {:?}", + self.ent.game.data.item_names[item.0] + ); + Err((None, Message::Text(format!("stuck")))) + } +} diff --git a/server/src/server.rs b/server/src/server.rs index 01460cb9..2259d159 100644 --- a/server/src/server.rs +++ b/server/src/server.rs @@ -670,6 +670,8 @@ impl Server { warn!("Entity tick failed: {e}") } } + self.entities.retain(|e| !e.finished()); + if let Some(map) = load_map { return Some((map, Some(Duration::from_secs(300)))); } -- cgit v1.2.3-70-g09d2