aboutsummaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-09-17 16:57:23 +0200
committermetamuffin <metamuffin@disroot.org>2024-09-17 16:57:34 +0200
commit7cacd111e2b443bac291244b168c20e2c8bf69ec (patch)
tree277ace5a4e8223df4b156e766c2660406101a82b /server
parent77a73b415888b0285ba64b27c3f69440216e475c (diff)
downloadhurrycurry-7cacd111e2b443bac291244b168c20e2c8bf69ec.tar
hurrycurry-7cacd111e2b443bac291244b168c20e2c8bf69ec.tar.bz2
hurrycurry-7cacd111e2b443bac291244b168c20e2c8bf69ec.tar.zst
add hint-based tutorial for item crafting
Diffstat (limited to 'server')
-rw-r--r--server/protocol/src/lib.rs2
-rw-r--r--server/src/commands.rs30
-rw-r--r--server/src/entity/bot.rs5
-rw-r--r--server/src/entity/customers.rs2
-rw-r--r--server/src/entity/mod.rs3
-rw-r--r--server/src/entity/tutorial.rs218
-rw-r--r--server/src/server.rs2
7 files changed, 252 insertions, 10 deletions
diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs
index 54a7f868..706747f4 100644
--- a/server/protocol/src/lib.rs
+++ b/server/protocol/src/lib.rs
@@ -136,7 +136,7 @@ pub enum PacketS {
},
}
-#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)]
+#[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum Message {
Text(String),
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 <https://www.gnu.org/licenses/>.
*/
-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<String> },
+ CreateBot {
+ algo: String,
+ name: Option<String>,
+ },
/// 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<T> {
join_data: Option<(String, i32)>,
id: PlayerID,
interacting: bool,
- pub left: bool,
+ left: bool,
}
impl<T: BotAlgo> BotDriver<T> {
@@ -44,6 +44,9 @@ impl<T: BotAlgo> BotDriver<T> {
}
}
impl<T: BotAlgo> Entity for BotDriver<T> {
+ 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<IVec2>, 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<RecipeIndex> {
+ 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<IVec2> {
+ 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<IVec2> {
+ self.ent
+ .game
+ .tiles
+ .iter()
+ .find(|(_, t)| t.kind == tile)
+ .map(|(p, _)| *p)
+ }
+ fn aquire_placed_item(&mut self, item: ItemIndex) -> Result<IVec2, (Option<IVec2>, 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<IVec2>, 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))));
}