/*
Hurry Curry! - a game about cooking
Copyright 2024 metamuffin
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.item.as_ref().is_some_and(|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.item.as_ref().is_some_and(|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")))
}
}