From 261151881d30143fc8e3337181b91b40e3cb125e Mon Sep 17 00:00:00 2001 From: Condorra Date: Tue, 20 Jun 2023 22:53:46 +1000 Subject: [PATCH] Refactor to move command queue to item. This is the start of being able to implement following in a way that works for NPCs, but it isn't finished yet. It does mean NPCs can do things like climb immediately, and will make it far simpler for NPCs to do other player-like actions in the future. --- .../src/message_handler/user_commands.rs | 5 + .../message_handler/user_commands/butcher.rs | 3 + .../message_handler/user_commands/close.rs | 38 +- .../src/message_handler/user_commands/cut.rs | 78 ++- .../src/message_handler/user_commands/drop.rs | 53 +- .../message_handler/user_commands/follow.rs | 46 ++ .../src/message_handler/user_commands/get.rs | 69 ++- .../user_commands/improvise.rs | 209 ++++---- .../message_handler/user_commands/movement.rs | 418 ++++++++-------- .../src/message_handler/user_commands/open.rs | 78 ++- .../message_handler/user_commands/remove.rs | 77 ++- .../message_handler/user_commands/use_cmd.rs | 113 ++--- .../src/message_handler/user_commands/wear.rs | 66 ++- .../message_handler/user_commands/wield.rs | 66 +-- blastmud_game/src/models/item.rs | 163 +++---- blastmud_game/src/models/session.rs | 18 +- .../src/regular_tasks/queued_command.rs | 245 +++++++--- blastmud_game/src/static_content/npc.rs | 460 ++++++++++-------- .../src/static_content/npc/melbs_citizen.rs | 408 +++++++++++++--- .../src/static_content/npc/melbs_dog.rs | 136 +++--- .../src/static_content/npc/statbot.rs | 236 ++++++--- .../src/static_content/possession_type.rs | 8 +- .../static_content/possession_type/lock.rs | 15 +- blastmud_game/src/static_content/room.rs | 307 +++++++----- .../src/static_content/room/chonkers.rs | 3 +- .../src/static_content/room/cok_murl.rs | 9 +- .../src/static_content/room/melbs.rs | 32 +- .../src/static_content/room/repro_xv.rs | 5 +- .../src/static_content/room/special.rs | 34 +- queued_command.rs | 0 30 files changed, 1985 insertions(+), 1413 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/follow.rs create mode 100644 queued_command.rs diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 599c39c..a55c7e5 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -26,6 +26,7 @@ pub mod cut; pub mod delete; mod describe; pub mod drop; +mod follow; mod gear; pub mod get; mod help; @@ -146,6 +147,10 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "cut" => cut::VERB, "delete" => delete::VERB, "drop" => drop::VERB, + + "follow" => follow::VERB, + "unfollow" => follow::VERB, + "gear" => gear::VERB, "get" => get::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/butcher.rs b/blastmud_game/src/message_handler/user_commands/butcher.rs index 5e7ecc1..1953ba0 100644 --- a/blastmud_game/src/message_handler/user_commands/butcher.rs +++ b/blastmud_game/src/message_handler/user_commands/butcher.rs @@ -67,6 +67,7 @@ impl UserVerb for Verb { ensure_has_butcher_tool(&ctx.trans, &player_item).await?; + let mut player_item_mut = (*player_item).clone(); for possession_type in possession_types { let possession_data = possession_data() .get(&possession_type) @@ -74,6 +75,7 @@ impl UserVerb for Verb { queue_command( ctx, + &mut player_item_mut, &QueueCommand::Cut { from_corpse: corpse.item_code.clone(), what_part: possession_data.display.to_owned(), @@ -81,6 +83,7 @@ impl UserVerb for Verb { ) .await?; } + ctx.trans.save_item_model(&player_item_mut).await?; Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/close.rs b/blastmud_game/src/message_handler/user_commands/close.rs index 1bd174d..6273d0f 100644 --- a/blastmud_game/src/message_handler/user_commands/close.rs +++ b/blastmud_game/src/message_handler/user_commands/close.rs @@ -5,7 +5,9 @@ use super::{ }; use crate::{ models::item::DoorState, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::comms::broadcast_to_room, static_content::room::Direction, }; @@ -15,20 +17,15 @@ use std::time; pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let direction = match command { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + let direction = match ctx.command { QueueCommand::CloseDoor { direction } => direction, _ => user_error("Unexpected queued command".to_owned())?, }; - let player_item = get_player_item_or_fail(ctx).await?; - let use_location = if player_item.death_data.is_some() { + let use_location = if ctx.item.death_data.is_some() { user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())? } else { - &player_item.location + &ctx.item.location }; match is_door_in_direction(&ctx.trans, &direction, use_location).await? { DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?, @@ -47,20 +44,15 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let direction = match command { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + let direction = match ctx.command { QueueCommand::CloseDoor { direction } => direction, _ => user_error("Unexpected queued command".to_owned())?, }; - let player_item = get_player_item_or_fail(ctx).await?; - let use_location = if player_item.death_data.is_some() { + let use_location = if ctx.item.death_data.is_some() { user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())? } else { - &player_item.location + &ctx.item.location }; let (room_1, dir_in_room, room_2) = match is_door_in_direction(&ctx.trans, &direction, use_location).await? { @@ -123,12 +115,12 @@ impl QueueCommandHandler for QueueHandler { None, &format!( "{} closes the door to the {}.\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), dir ), Some(&format!( "{} closes the door to the {}.\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), dir )), ) @@ -157,8 +149,10 @@ impl UserVerb for Verb { ) -> UResult<()> { let dir = Direction::parse(remaining).ok_or_else(|| UserError("Unknown direction".to_owned()))?; - queue_command( + let player_item = get_player_item_or_fail(ctx).await?; + queue_command_and_save( ctx, + &player_item, &QueueCommand::CloseDoor { direction: dir.clone(), }, diff --git a/blastmud_game/src/message_handler/user_commands/cut.rs b/blastmud_game/src/message_handler/user_commands/cut.rs index cfb1a00..bd03df8 100644 --- a/blastmud_game/src/message_handler/user_commands/cut.rs +++ b/blastmud_game/src/message_handler/user_commands/cut.rs @@ -8,7 +8,9 @@ use crate::{ db::ItemSearchParams, language::join_words, models::item::{DeathData, Item, SkillType}, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{ capacity::{check_item_capacity, CapacityLevel}, combat::corpsify_item, @@ -26,18 +28,13 @@ use std::{sync::Arc, time}; pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You butcher things while they are dead, not while YOU are dead!".to_owned(), )?; } - let (from_corpse_id, what_part) = match command { + let (from_corpse_id, what_part) = match ctx.command { QueueCommand::Cut { from_corpse, what_part, @@ -52,13 +49,15 @@ impl QueueCommandHandler for QueueHandler { None => user_error("The corpse seems to be gone".to_owned())?, Some(it) => it, }; - if corpse.location != player_item.location { + + let explicit = ctx.explicit().await?; + if corpse.location != ctx.item.location { user_error(format!( "You try to cut {} but realise it is no longer there.", - corpse.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + corpse.display_for_sentence(explicit, 1, false) ))? } - ensure_has_butcher_tool(&ctx.trans, &player_item).await?; + ensure_has_butcher_tool(&ctx.trans, &ctx.item).await?; match corpse.death_data.as_ref() { None => user_error(format!( "You can't do that while {} is still alive!", @@ -74,7 +73,8 @@ impl QueueCommandHandler for QueueHandler { == Some(true) }) { user_error(format!( - "That part is now gone. Parts you can cut: {}", + "That part ({}) is now gone. Parts you can cut: {}", + &what_part, &join_words( &parts_remaining .iter() @@ -89,19 +89,19 @@ impl QueueCommandHandler for QueueHandler { let msg_exp = format!( "{} prepares to cut {} from {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &what_part, &corpse.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} prepares to cut {} from {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &what_part, &corpse.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -111,25 +111,20 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You butcher things while they are dead, not while YOU are dead!".to_owned(), )?; } - let (from_corpse_id, what_part) = match command { + let (from_corpse_id, what_part) = match ctx.command { QueueCommand::Cut { from_corpse, what_part, } => (from_corpse, what_part), _ => user_error("Unexpected command".to_owned())?, }; - ensure_has_butcher_tool(&ctx.trans, &player_item).await?; + ensure_has_butcher_tool(&ctx.trans, &ctx.item).await?; let corpse = match ctx .trans .find_item_by_type_code("corpse", &from_corpse_id) @@ -138,10 +133,12 @@ impl QueueCommandHandler for QueueHandler { None => user_error("The corpse seems to be gone".to_owned())?, Some(it) => it, }; - if corpse.location != player_item.location { + + let explicit = ctx.explicit().await?; + if corpse.location != ctx.item.location { user_error(format!( "You try to cut {} but realise it is no longer there.", - corpse.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + corpse.display_for_sentence(explicit, 1, false) ))? } @@ -197,8 +194,7 @@ impl QueueCommandHandler for QueueHandler { } } - match check_item_capacity(&ctx.trans, &player_item.refstr(), possession_data.weight).await? - { + match check_item_capacity(&ctx.trans, &ctx.item.refstr(), possession_data.weight).await? { CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => { user_error("You have too much stuff to take that on!".to_owned())? } @@ -216,23 +212,20 @@ impl QueueCommandHandler for QueueHandler { ctx.trans.save_item_model(&corpse_mut).await?; } - let mut player_item_mut = (*player_item).clone(); - if skill_check_and_grind(&ctx.trans, &mut player_item_mut, &SkillType::Craft, 10.0).await? - < 0.0 - { + if skill_check_and_grind(&ctx.trans, ctx.item, &SkillType::Craft, 10.0).await? < 0.0 { broadcast_to_room( &ctx.trans, - &player_item.location, + &ctx.item.location, None, &format!( "{} tries to cut the {} from {}, but only leaves a mutilated mess.\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), possession_data.display, corpse.display_for_sentence(true, 1, false) ), Some(&format!( "{} tries to cut the {} from {}, but only leaves a mutilated mess.\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), possession_data.display, corpse.display_for_sentence(true, 1, false) )), @@ -241,22 +234,22 @@ impl QueueCommandHandler for QueueHandler { } else { let mut new_item: Item = (*possession_type).clone().into(); new_item.item_code = format!("{}", ctx.trans.alloc_item_code().await?); - new_item.location = player_item.refstr(); + new_item.location = ctx.item.refstr(); ctx.trans.save_item_model(&new_item).await?; broadcast_to_room( &ctx.trans, - &player_item.location, + &ctx.item.location, None, &format!( "{} expertly cuts the {} from {}.\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), possession_data.display, corpse.display_for_sentence(true, 1, false) ), Some(&format!( "{} expertly cuts the {} from {}.\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), possession_data.display, corpse.display_for_sentence(true, 1, false) )), @@ -264,8 +257,6 @@ impl QueueCommandHandler for QueueHandler { .await?; } - ctx.trans.save_item_model(&player_item_mut).await?; - Ok(()) } } @@ -367,8 +358,9 @@ impl UserVerb for Verb { ensure_has_butcher_tool(&ctx.trans, &player_item).await?; - queue_command( + queue_command_and_save( ctx, + &player_item, &QueueCommand::Cut { from_corpse: corpse.item_code.clone(), what_part: possession_data.display.to_owned(), diff --git a/blastmud_game/src/message_handler/user_commands/drop.rs b/blastmud_game/src/message_handler/user_commands/drop.rs index fe83c7b..fd378a3 100644 --- a/blastmud_game/src/message_handler/user_commands/drop.rs +++ b/blastmud_game/src/message_handler/user_commands/drop.rs @@ -10,7 +10,7 @@ use crate::{ task::{Task, TaskDetails, TaskMeta}, }, regular_tasks::{ - queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + queued_command::{queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext}, TaskHandler, TaskRunContext, }, services::{ @@ -107,18 +107,13 @@ pub async fn consider_expire_job_for_item(trans: &DBTrans, item: &Item) -> DResu pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You try to drop it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Drop { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -130,10 +125,10 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != format!("{}/{}", &player_item.item_type, &player_item.item_code) { + if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) { user_error(format!( "You try to drop {} but realise you no longer have it", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } if item.action_type == LocationActionType::Worn { @@ -143,17 +138,17 @@ impl QueueCommandHandler for QueueHandler { } let msg_exp = format!( "{} prepares to drop {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} prepares to drop {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -163,18 +158,13 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You try to get it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Drop { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -187,10 +177,10 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != format!("{}/{}", &player_item.item_type, &player_item.item_code) { + if item.location != format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code) { user_error(format!( "You try to drop {} but realise you no longer have it!", - &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + &item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } if item.action_type == LocationActionType::Worn { @@ -210,34 +200,34 @@ impl QueueCommandHandler for QueueHandler { Some(pd) => pd, }; - match check_item_capacity(ctx.trans, &player_item.location, possession_data.weight).await? { + match check_item_capacity(ctx.trans, &ctx.item.location, possession_data.weight).await? { CapacityLevel::AboveItemLimit => user_error(format!( "You can't drop {}, because it is so cluttered here there is no where to put it!", - &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + &item.display_for_sentence(ctx.explicit().await?, 1, false) ))?, _ => (), } let msg_exp = format!( "{} drops {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} drops {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), ) .await?; let mut item_mut = (*item).clone(); - item_mut.location = player_item.location.clone(); + item_mut.location = ctx.item.location.clone(); consider_expire_job_for_item(ctx.trans, &item_mut).await?; item_mut.action_type = LocationActionType::Normal; ctx.trans.save_item_model(&item_mut).await?; @@ -281,18 +271,21 @@ impl UserVerb for Verb { )?; } + let mut player_item_mut = (*player_item).clone(); for target in targets { if target.item_type != "possession" { user_error("You can't drop that!".to_owned())?; } queue_command( ctx, + &mut player_item_mut, &QueueCommand::Drop { possession_id: target.item_code.clone(), }, ) .await?; } + ctx.trans.save_item_model(&player_item_mut).await?; Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/follow.rs b/blastmud_game/src/message_handler/user_commands/follow.rs new file mode 100644 index 0000000..bd685c5 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/follow.rs @@ -0,0 +1,46 @@ +use super::{ + get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef, + VerbContext, +}; +use crate::db::ItemSearchParams; +use async_trait::async_trait; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle( + self: &Self, + ctx: &mut VerbContext, + verb: &str, + remaining: &str, + ) -> UResult<()> { + let player_item = (*(get_player_item_or_fail(ctx).await?)).clone(); + if verb == "follow" { + let follow_who = search_item_for_user( + ctx, + &ItemSearchParams { + include_loc_contents: true, + ..ItemSearchParams::base(&player_item, remaining.trim()) + }, + ) + .await?; + if follow_who.item_type != "player" && follow_who.item_type != "npc" { + user_error("Only characters (player / NPC) can be followed.".to_owned())?; + } + if let Some(follow) = player_item.following.as_ref() { + if follow.follow_whom == player_item.refstr() { + user_error(format!( + "You're already following {}!", + follow_who.pronouns.possessive + ))?; + } + // sess_dets.queue.filter() + } + } else { + } + user_error("Sorry, we're still building the follow command!".to_owned())?; + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/get.rs b/blastmud_game/src/message_handler/user_commands/get.rs index edd01b4..3d37091 100644 --- a/blastmud_game/src/message_handler/user_commands/get.rs +++ b/blastmud_game/src/message_handler/user_commands/get.rs @@ -4,7 +4,9 @@ use super::{ }; use crate::{ models::item::LocationActionType, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{ capacity::{check_item_capacity, CapacityLevel}, comms::broadcast_to_room, @@ -17,18 +19,13 @@ use std::time; pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You try to get it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Get { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -40,25 +37,25 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.location { + if item.location != ctx.item.location { user_error(format!( "You try to get {} but realise it is no longer there", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } let msg_exp = format!( "{} fumbles around trying to pick up {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} fumbles around trying to pick up {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -68,18 +65,13 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You try to get it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Get { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -91,10 +83,10 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.location { + if item.location != ctx.item.location { user_error(format!( "You try to get {} but realise it is no longer there", - &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + &item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } @@ -109,36 +101,35 @@ impl QueueCommandHandler for QueueHandler { Some(pd) => pd, }; - let player_as_loc = format!("{}/{}", &player_item.item_type, &player_item.item_code); + let player_as_loc = format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code); match check_item_capacity(ctx.trans, &player_as_loc, possession_data.weight).await? { CapacityLevel::AboveItemLimit => { user_error("You just can't hold that many things!".to_owned())? } - CapacityLevel::OverBurdened => user_error(format!( - "{} You drop {} because it is too heavy!", - if ctx.session_dat.less_explicit_mode { - "Rats!" - } else { - "Fuck!" - }, - &player_item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) - ))?, + CapacityLevel::OverBurdened => { + let explicit = ctx.explicit().await?; + user_error(format!( + "{} You drop {} because it is too heavy!", + if explicit { "Fuck!" } else { "Rats!" }, + &ctx.item.display_for_sentence(explicit, 1, false) + ))? + } _ => (), } let msg_exp = format!( "{} picks up {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} picks up {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -188,6 +179,7 @@ impl UserVerb for Verb { } let mut did_anything: bool = false; + let mut player_item_mut = (*player_item).clone(); for target in targets .iter() .filter(|t| t.action_type.is_visible_in_look()) @@ -198,6 +190,7 @@ impl UserVerb for Verb { did_anything = true; queue_command( ctx, + &mut player_item_mut, &QueueCommand::Get { possession_id: target.item_code.clone(), }, @@ -206,6 +199,8 @@ impl UserVerb for Verb { } if !did_anything { user_error("I didn't find anything matching.".to_owned())?; + } else { + ctx.trans.save_item_model(&player_item_mut).await?; } Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands/improvise.rs b/blastmud_game/src/message_handler/user_commands/improvise.rs index e23da46..6b96ebf 100644 --- a/blastmud_game/src/message_handler/user_commands/improvise.rs +++ b/blastmud_game/src/message_handler/user_commands/improvise.rs @@ -5,7 +5,9 @@ use super::{ use crate::{ language::{self, indefinite_article}, models::item::Item, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{ comms::broadcast_to_room, destroy_container, @@ -27,16 +29,11 @@ use std::time; pub struct WithQueueHandler; #[async_trait] impl QueueCommandHandler for WithQueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::ImprovWith { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -48,23 +45,23 @@ impl QueueCommandHandler for WithQueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.refstr() { + if item.location != ctx.item.refstr() { user_error("You try improvising but realise you no longer have it.".to_owned())?; } broadcast_to_room( &ctx.trans, - &player_item.location, + &ctx.item.location, None, &format!( "{} tries to work out what {} can make from {}.\n", - &player_item.display_for_sentence(true, 1, true), - &player_item.pronouns.subject, + &ctx.item.display_for_sentence(true, 1, true), + &ctx.item.pronouns.subject, &item.display_for_sentence(true, 1, false), ), Some(&format!( "{} tries to work out what {} can make from {}.\n", - &player_item.display_for_sentence(false, 1, true), - &player_item.pronouns.subject, + &ctx.item.display_for_sentence(false, 1, true), + &ctx.item.pronouns.subject, &item.display_for_sentence(false, 1, false), )), ) @@ -73,16 +70,11 @@ impl QueueCommandHandler for WithQueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::ImprovWith { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -94,9 +86,20 @@ impl QueueCommandHandler for WithQueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.refstr() { + if item.location != ctx.item.refstr() { user_error("You try improvising but realise you no longer have it.".to_owned())?; } + let session = if ctx.item.item_type == "player" { + ctx.trans + .find_session_for_player(&ctx.item.item_code) + .await? + } else { + None + }; + let explicit = session + .as_ref() + .map(|s| !s.1.less_explicit_mode) + .unwrap_or(false); let opts: Vec<&'static PossessionData> = improv_by_ingredient() .get( item.possession_type @@ -106,12 +109,12 @@ impl QueueCommandHandler for WithQueueHandler { .ok_or_else(|| { UserError(format!( "You can't think of anything you could make with {}", - item.display_for_session(&ctx.session_dat) + item.display_for_sentence(explicit, 1, false) )) })? .iter() .filter_map(|it| possession_data().get(&it.output).map(|v| *v)) - .filter(|pd| !ctx.session_dat.less_explicit_mode || pd.display_less_explicit.is_none()) + .filter(|pd| explicit || pd.display_less_explicit.is_none()) .collect(); let result_data = opts .as_slice() @@ -119,20 +122,22 @@ impl QueueCommandHandler for WithQueueHandler { .ok_or_else(|| { UserError(format!( "You can't think of anything you could make with {}", - item.display_for_session(&ctx.session_dat) + item.display_for_sentence(explicit, 1, false) )) })?; - ctx.trans - .queue_for_session( - &ctx.session, - Some(&format!( - "You think you could make {} {} from {}\n", - indefinite_article(result_data.display), - result_data.display, - item.display_for_session(&ctx.session_dat) - )), - ) - .await?; + if let Some((sess, _)) = session { + ctx.trans + .queue_for_session( + &sess, + Some(&format!( + "You think you could make {} {} from {}\n", + indefinite_article(result_data.display), + result_data.display, + item.display_for_sentence(explicit, 1, false) + )), + ) + .await?; + } Ok(()) } } @@ -140,16 +145,11 @@ impl QueueCommandHandler for WithQueueHandler { pub struct FromQueueHandler; #[async_trait] impl QueueCommandHandler for FromQueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } - let (already_used, item_ids) = match command { + let (already_used, item_ids) = match ctx.command { QueueCommand::ImprovFrom { possession_ids, already_used, @@ -169,7 +169,7 @@ impl QueueCommandHandler for FromQueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.refstr() { + if item.location != ctx.item.refstr() { user_error("You try improvising but realise you no longer have the things you'd planned to use." .to_owned())?; } @@ -178,16 +178,11 @@ impl QueueCommandHandler for FromQueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error("The dead aren't very good at improvisation.".to_owned())?; } - let (output, possession_ids, already_used) = match command { + let (output, possession_ids, already_used) = match ctx.command { QueueCommand::ImprovFrom { output, possession_ids, @@ -218,6 +213,17 @@ impl QueueCommandHandler for FromQueueHandler { } let mut possession_id_iter = possession_ids.iter(); + let session = if ctx.item.item_type == "player" { + ctx.trans + .find_session_for_player(&ctx.item.item_code) + .await? + } else { + None + }; + let explicit = session + .as_ref() + .map(|s| !s.1.less_explicit_mode) + .unwrap_or(false); match possession_id_iter.next() { None => { let choice = ingredients_left @@ -232,23 +238,23 @@ impl QueueCommandHandler for FromQueueHandler { } let mut new_item: Item = craft_data.output.clone().into(); new_item.item_code = ctx.trans.alloc_item_code().await?.to_string(); - new_item.location = player_item.refstr(); + new_item.location = ctx.item.refstr(); ctx.trans.create_item(&new_item).await?; broadcast_to_room( &ctx.trans, - &player_item.location, + &ctx.item.location, None, &format!( "{} proudly holds up the {} {} just made.\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &new_item.display_for_sentence(true, 1, false), - &player_item.pronouns.subject + &ctx.item.pronouns.subject ), Some(&format!( "{} proudly holds up the {} {} just made.\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &new_item.display_for_sentence(false, 1, false), - &player_item.pronouns.subject + &ctx.item.pronouns.subject )), ) .await?; @@ -287,67 +293,66 @@ impl QueueCommandHandler for FromQueueHandler { ) { user_error(format!( "You try adding {}, but it doesn't really seem to fit right.", - &item.display_for_session(&ctx.session_dat) + &item.display_for_sentence(explicit, 1, false) ))?; } - let mut player_item_mut = (*player_item).clone(); let skill_result = skill_check_and_grind( &ctx.trans, - &mut player_item_mut, + ctx.item, &craft_data.skill, craft_data.difficulty, ) .await?; if skill_result <= -0.5 { - crit_fail_penalty_for_skill( - &ctx.trans, - &mut player_item_mut, - &craft_data.skill, - ) - .await?; + crit_fail_penalty_for_skill(&ctx.trans, ctx.item, &craft_data.skill).await?; ctx.trans .delete_item(&item.item_type, &item.item_code) .await?; - ctx.trans - .queue_for_session( - &ctx.session, - Some(&format!( - "You try adding {}, but it goes badly and you waste it.\n", - &item.display_for_session(&ctx.session_dat) - )), - ) - .await?; + if let Some((sess, _)) = session { + ctx.trans + .queue_for_session( + &sess, + Some(&format!( + "You try adding {}, but it goes badly and you waste it.\n", + &item.display_for_sentence(explicit, 1, false) + )), + ) + .await?; + } } else if skill_result <= 0.0 { - ctx.trans - .queue_for_session( - &ctx.session, - Some(&format!( - "You try and fail at adding {}.\n", - &item.display_for_session(&ctx.session_dat) - )), - ) - .await?; + if let Some((sess, _)) = session { + ctx.trans + .queue_for_session( + &sess, + Some(&format!( + "You try and fail at adding {}.\n", + &item.display_for_sentence(explicit, 1, false) + )), + ) + .await?; + } } else { - ctx.trans - .queue_for_session( - &ctx.session, - Some(&format!( - "You try adding {}.\n", - &item.display_for_session(&ctx.session_dat), - )), - ) - .await?; + if let Some((sess, _)) = session { + ctx.trans + .queue_for_session( + &sess, + Some(&format!( + "You try adding {}.\n", + &item.display_for_sentence(explicit, 1, false), + )), + ) + .await?; + } let mut new_possession_ids = possession_ids.clone(); new_possession_ids.remove(possession_id); let mut new_already_used = already_used.clone(); new_already_used.insert(possession_id.clone()); - ctx.session_dat.queue.push_front(QueueCommand::ImprovFrom { + ctx.item.queue.push_front(QueueCommand::ImprovFrom { output: output.clone(), possession_ids: new_possession_ids, already_used: new_already_used, }); } - ctx.trans.save_item_model(&player_item_mut).await?; } } Ok(()) @@ -371,8 +376,9 @@ async fn improv_query( if item.item_type != "possession" { user_error("You can't improvise with that!".to_owned())? } - queue_command( + queue_command_and_save( ctx, + player_item, &QueueCommand::ImprovWith { possession_id: item.item_code.clone(), }, @@ -444,8 +450,9 @@ impl UserVerb for Verb { input_ids.insert(item.item_code.to_owned()); } } - queue_command( + queue_command_and_save( ctx, + &player_item, &QueueCommand::ImprovFrom { output: output_type, possession_ids: input_ids, diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index ddddb35..9484b91 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -11,7 +11,10 @@ use crate::{ consent::ConsentType, item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType}, }, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command_and_save, MovementSource, QueueCommand, QueueCommandHandler, + QueuedCommandContext, + }, services::{ check_consent, combat::{change_health, handle_resurrect, stop_attacking_mut}, @@ -84,14 +87,16 @@ pub async fn announce_move( } async fn move_to_where( - trans: &DBTrans, use_location: &str, direction: &Direction, - mover_for_exit_check: Option<&mut Item>, - player_ctx: &mut Option<&mut VerbContext<'_>>, + ctx: &mut QueuedCommandContext<'_>, ) -> UResult<(String, Option, Option<&'static ExitClimb>)> { // Firstly check dynamic exits, since they apply to rooms and dynrooms... - if let Some(dynroom_result) = trans.find_exact_dyn_exit(use_location, direction).await? { + if let Some(dynroom_result) = ctx + .trans + .find_exact_dyn_exit(use_location, direction) + .await? + { return Ok(( format!( "{}/{}", @@ -107,7 +112,7 @@ async fn move_to_where( .unwrap_or(("room", "repro_xv_chargen")); if heretype == "dynroom" { - let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? { + let old_dynroom_item = match ctx.trans.find_item_by_type_code(heretype, herecode).await? { None => user_error("Your current room has vanished!".to_owned())?, Some(v) => v, }; @@ -140,7 +145,8 @@ async fn move_to_where( .location .split_once("/") .ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?; - let zoneitem = trans + let zoneitem = ctx + .trans .find_item_by_type_code(zonetype, zonecode) .await? .ok_or_else(|| UserError("Can't find your zone".to_owned()))?; @@ -159,7 +165,8 @@ async fn move_to_where( Ok((zone_exit.to_string(), None, None)) } DynExitTarget::Intrazone { subcode } => { - let to_item = trans + let to_item = ctx + .trans .find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode) .await? .ok_or_else(|| { @@ -188,12 +195,8 @@ async fn move_to_where( match exit.exit_type { ExitType::Free => {} ExitType::Blocked(blocker) => { - if let Some(mover) = mover_for_exit_check { - if let Some(ctx) = player_ctx { - if !blocker.attempt_exit(*ctx, mover, exit).await? { - user_error("Stopping movement".to_owned())?; - } - } + if !blocker.attempt_exit(ctx, exit).await? { + user_error("Stopping movement".to_owned())?; } } } @@ -295,24 +298,23 @@ pub async fn handle_fall(trans: &DBTrans, faller: &mut Item, fall_dist: u64) -> Ok(descriptor.to_owned()) } -pub async fn attempt_move_immediate( - trans: &DBTrans, - orig_mover: &Item, +async fn attempt_move_immediate( direction: &Direction, - // player_ctx should only be Some if called from queue_handler finish_command - // for the orig_mover's queue, because might re-queue a move command. - mut player_ctx: &mut Option<&mut VerbContext<'_>>, + mut ctx: &mut QueuedCommandContext<'_>, + source: &MovementSource, ) -> UResult<()> { - let use_location = if orig_mover.death_data.is_some() { - if orig_mover.item_type != "player" { + let use_location = if ctx.item.death_data.is_some() { + if ctx.item.item_type != "player" { user_error("Dead players don't move".to_owned())?; } - "room/repro_xv_respawn" + "room/repro_xv_respawn".to_owned() } else { - &orig_mover.location + ctx.item.location.clone() }; - match is_door_in_direction(trans, direction, use_location).await? { + let session = ctx.get_session().await?; + + match is_door_in_direction(ctx.trans, direction, &use_location).await? { DoorSituation::NoDoor | DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, @@ -323,184 +325,168 @@ pub async fn attempt_move_immediate( room_with_door, .. } => { - check_room_access(trans, orig_mover, &room_with_door).await?; + check_room_access(ctx.trans, ctx.item, &room_with_door).await?; } _ => { - attempt_open_immediate(trans, player_ctx, orig_mover, direction).await?; - match player_ctx.as_mut() { - None => { - // NPCs etc... open and move in one step, but can't unlock. - } - Some(actual_player_ctx) => { - // Players take an extra step. So tell them to come back. - actual_player_ctx - .session_dat - .queue - .push_front(QueueCommand::Movement { - direction: direction.clone(), - }); - return Ok(()); - } - } + attempt_open_immediate(ctx, direction).await?; + // Players take an extra step. So tell them to come back. + ctx.item.queue.push_front(QueueCommand::Movement { + direction: direction.clone(), + source: source.clone(), + }); + return Ok(()); } } - let mut mover = (*orig_mover).clone(); - let (new_loc, new_loc_item, climb_opt) = move_to_where( - trans, - use_location, - direction, - Some(&mut mover), - &mut player_ctx, - ) - .await?; + let (new_loc, new_loc_item, climb_opt) = move_to_where(&use_location, direction, ctx).await?; let mut skip_escape_check: bool = false; let mut escape_check_only: bool = false; if let Some(climb) = climb_opt { - // We need to think about if NPCs climb at all. - if let Some(ctx) = player_ctx { - if let Some(active_climb) = mover.active_climb.clone() { - skip_escape_check = true; // Already done if we get here. - let skills = skill_check_and_grind( - trans, - &mut mover, - &SkillType::Climb, - climb.difficulty as f64, - ) - .await?; - let mut narrative = String::new(); - if skills <= -0.25 { - // Crit fail - they have fallen. - let (fall_dist, from_room, to_room) = if climb.height < 0 { - // At least they get to where they want to go! - mover.location = new_loc.clone(); - ( - climb.height.abs() as u64 - active_climb.height, - new_loc.to_owned(), - use_location.to_owned(), - ) - } else { - ( - active_climb.height, - use_location.to_owned(), - new_loc.to_owned(), - ) - }; - mover.active_climb = None; - let descriptor = handle_fall(&trans, &mut mover, fall_dist).await?; - let msg_exp = format!( - "{} loses {} grip from {} metres up and {}!\n", - &mover.display_for_sentence(true, 1, true), - &mover.pronouns.possessive, - fall_dist, - &descriptor - ); - let msg_nonexp = format!( - "{} loses {} grip from {} metres up and {}!\n", - &mover.display_for_sentence(true, 1, false), - &mover.pronouns.possessive, - fall_dist, - &descriptor - ); - trans.save_item_model(&mover).await?; - broadcast_to_room(&trans, &from_room, None, &msg_exp, Some(&msg_nonexp)) - .await?; - broadcast_to_room(&trans, &to_room, None, &msg_exp, Some(&msg_nonexp)).await?; - ctx.session_dat.queue.truncate(0); - return Ok(()); - } else if skills <= 0.0 { - if climb.height >= 0 { - narrative.push_str("You lose your grip and slide a metre back down"); - } else { - narrative.push_str( - "You struggle to find a foothold and reluctantly climb a metre back up", - ); - } - if let Some(ac) = mover.active_climb.as_mut() { - if ac.height > 0 { - ac.height -= 1; - } - } + if let Some(active_climb) = ctx.item.active_climb.clone() { + skip_escape_check = true; // Already done if we get here. + let skills = skill_check_and_grind( + ctx.trans, + ctx.item, + &SkillType::Climb, + climb.difficulty as f64, + ) + .await?; + let mut narrative = String::new(); + if skills <= -0.25 { + // Crit fail - they have fallen. + let (fall_dist, from_room, to_room) = if climb.height < 0 { + // At least they get to where they want to go! + ctx.item.location = new_loc.clone(); + ( + climb.height.abs() as u64 - active_climb.height, + new_loc.to_owned(), + use_location.clone(), + ) } else { - if climb.height < 0 { - narrative.push_str("You climb down another metre"); - } else { - narrative.push_str("You climb up another metre"); - } - if let Some(ac) = mover.active_climb.as_mut() { - ac.height += 1; + ( + active_climb.height, + use_location.clone(), + new_loc.to_owned(), + ) + }; + ctx.item.active_climb = None; + let descriptor = handle_fall(&ctx.trans, ctx.item, fall_dist).await?; + let msg_exp = format!( + "{} loses {} grip from {} metres up and {}!\n", + ctx.item.display_for_sentence(true, 1, true), + ctx.item.pronouns.possessive, + fall_dist, + &descriptor + ); + let msg_nonexp = format!( + "{} loses {} grip from {} metres up and {}!\n", + ctx.item.display_for_sentence(false, 1, true), + &ctx.item.pronouns.possessive, + fall_dist, + &descriptor + ); + broadcast_to_room(ctx.trans, &from_room, None, &msg_exp, Some(&msg_nonexp)).await?; + broadcast_to_room(ctx.trans, &to_room, None, &msg_exp, Some(&msg_nonexp)).await?; + ctx.item.queue.truncate(0); + return Ok(()); + } else if skills <= 0.0 { + if climb.height >= 0 { + narrative.push_str("You lose your grip and slide a metre back down"); + } else { + narrative.push_str( + "You struggle to find a foothold and reluctantly climb a metre back up", + ); + } + if let Some(ac) = ctx.item.active_climb.as_mut() { + if ac.height > 0 { + ac.height -= 1; } } - if let Some(ac) = mover.active_climb.as_ref() { - if climb.height >= 0 && ac.height >= climb.height as u64 { - trans + } else { + if climb.height < 0 { + narrative.push_str("You climb down another metre"); + } else { + narrative.push_str("You climb up another metre"); + } + if let Some(ac) = ctx.item.active_climb.as_mut() { + ac.height += 1; + } + } + if let Some(ac) = ctx.item.active_climb.as_ref() { + if climb.height >= 0 && ac.height >= climb.height as u64 { + if let Some((sess, _)) = session.as_ref() { + ctx.trans .queue_for_session( - &ctx.session, + sess, Some( "You brush yourself off and finish climbing - you \ - made it to the top!\n", + made it to the top!\n", ), ) .await?; - mover.active_climb = None; - } else if climb.height < 0 && ac.height >= (-climb.height) as u64 { - trans + } + ctx.item.active_climb = None; + } else if climb.height < 0 && ac.height >= (-climb.height) as u64 { + if let Some((sess, _)) = session.as_ref() { + ctx.trans .queue_for_session( - &ctx.session, + sess, Some( "You brush yourself off and finish climbing - you \ - made it down!\n", + made it down!\n", ), ) .await?; - mover.active_climb = None; - } else { - let progress_quant = - (((ac.height as f64) / (climb.height.abs() as f64)) * 10.0) as u64; - trans.queue_for_session( - &ctx.session, + } + ctx.item.active_climb = None; + } else { + let progress_quant = + (((ac.height as f64) / (climb.height.abs() as f64)) * 10.0) as u64; + if let Some((sess, _)) = session { + ctx.trans.queue_for_session( + &sess, Some(&format!(ansi!("[{}{}] [{}/{} m] {}\n"), "=".repeat(progress_quant as usize), " ".repeat((10 - progress_quant) as usize), ac.height, climb.height.abs(), &narrative ))).await?; - ctx.session_dat.queue.push_front(QueueCommand::Movement { - direction: direction.clone(), - }); - trans.save_item_model(&mover).await?; - return Ok(()); } + ctx.item.queue.push_front(QueueCommand::Movement { + direction: direction.clone(), + source: source.clone(), + }); + return Ok(()); } - } else { - let msg_exp = format!( - "{} starts climbing {}\n", - &orig_mover.display_for_sentence(true, 1, true), - &direction.describe_climb(if climb.height > 0 { "up" } else { "down" }) - ); - let msg_nonexp = format!( - "{} starts climbing {}\n", - &orig_mover.display_for_sentence(true, 1, false), - &direction.describe_climb(if climb.height > 0 { "up" } else { "down" }) - ); - broadcast_to_room(&trans, &use_location, None, &msg_exp, Some(&msg_nonexp)).await?; - - mover.active_climb = Some(ActiveClimb { - ..Default::default() - }); - - ctx.session_dat.queue.push_front(QueueCommand::Movement { - direction: direction.clone(), - }); - escape_check_only = true; } } else { - user_error("NPC climbing not supported yet.".to_owned())?; + let msg_exp = format!( + "{} starts climbing {}\n", + &ctx.item.display_for_sentence(true, 1, true), + &direction.describe_climb(if climb.height > 0 { "up" } else { "down" }) + ); + let msg_nonexp = format!( + "{} starts climbing {}\n", + &ctx.item.display_for_sentence(true, 1, false), + &direction.describe_climb(if climb.height > 0 { "up" } else { "down" }) + ); + broadcast_to_room(&ctx.trans, &use_location, None, &msg_exp, Some(&msg_nonexp)).await?; + + ctx.item.active_climb = Some(ActiveClimb { + ..Default::default() + }); + + ctx.item.queue.push_front(QueueCommand::Movement { + direction: direction.clone(), + source: source.clone(), + }); + escape_check_only = true; } } if !skip_escape_check { - match mover + match ctx + .item .active_combat .as_ref() .and_then(|ac| ac.attacking.clone()) @@ -508,15 +494,16 @@ pub async fn attempt_move_immediate( None => {} Some(old_victim) => { if let Some((vcode, vtype)) = old_victim.split_once("/") { - if let Some(vitem) = trans.find_item_by_type_code(vcode, vtype).await? { + if let Some(vitem) = ctx.trans.find_item_by_type_code(vcode, vtype).await? { let mut vitem_mut = (*vitem).clone(); - stop_attacking_mut(trans, &mut mover, &mut vitem_mut, false).await?; - trans.save_item_model(&vitem_mut).await? + stop_attacking_mut(ctx.trans, ctx.item, &mut vitem_mut, false).await?; + ctx.trans.save_item_model(&vitem_mut).await? } } } } - match mover + match ctx + .item .active_combat .clone() .as_ref() @@ -526,11 +513,13 @@ pub async fn attempt_move_immediate( Some(attackers) => { let mut attacker_names = Vec::new(); let mut attacker_items = Vec::new(); - if let Some(ctx) = player_ctx.as_ref() { + if let Some((_, session_dat)) = session.as_ref() { for attacker in &attackers[..] { if let Some((acode, atype)) = attacker.split_once("/") { - if let Some(aitem) = trans.find_item_by_type_code(acode, atype).await? { - attacker_names.push(aitem.display_for_session(ctx.session_dat)); + if let Some(aitem) = + ctx.trans.find_item_by_type_code(acode, atype).await? + { + attacker_names.push(aitem.display_for_session(session_dat)); attacker_items.push(aitem); } } @@ -542,18 +531,18 @@ pub async fn attempt_move_immediate( .collect::>(); let attacker_names_str = language::join_words(&attacker_names_ref[..]); if skill_check_and_grind( - trans, - &mut mover, + ctx.trans, + ctx.item, &SkillType::Dodge, attackers.len() as f64 + 8.0, ) .await? >= 0.0 { - if let Some(ctx) = player_ctx.as_ref() { - trans + if let Some((sess, _)) = session.as_ref() { + ctx.trans .queue_for_session( - ctx.session, + sess, Some(&format!( "You successfully get away from {}\n", &attacker_names_str @@ -563,14 +552,14 @@ pub async fn attempt_move_immediate( } for item in &attacker_items[..] { let mut item_mut = (**item).clone(); - stop_attacking_mut(trans, &mut item_mut, &mut mover, true).await?; - trans.save_item_model(&item_mut).await?; + stop_attacking_mut(ctx.trans, &mut item_mut, ctx.item, true).await?; + ctx.trans.save_item_model(&item_mut).await?; } } else { - if let Some(ctx) = player_ctx.as_ref() { - trans + if let Some((sess, _)) = session.as_ref() { + ctx.trans .queue_for_session( - ctx.session, + sess, Some(&format!( "You try and fail to run past {}\n", &attacker_names_str @@ -578,48 +567,59 @@ pub async fn attempt_move_immediate( ) .await?; } - trans.save_item_model(&mover).await?; return Ok(()); } } } } if escape_check_only { - trans.save_item_model(&mover).await?; return Ok(()); } - if mover.death_data.is_some() { - if !handle_resurrect(trans, &mut mover).await? { + if ctx.item.death_data.is_some() { + if !handle_resurrect(ctx.trans, ctx.item).await? { user_error("You couldn't be resurrected.".to_string())?; } } - mover.location = new_loc.clone(); - mover.action_type = LocationActionType::Normal; - mover.active_combat = None; + ctx.item.location = new_loc.clone(); + ctx.item.action_type = LocationActionType::Normal; + ctx.item.active_combat = None; - trans.save_item_model(&mover).await?; - - if let Some(ctx) = player_ctx { - look::VERB.handle(ctx, "look", "").await?; + if let Some((sess, mut session_dat)) = session { + let mut user = ctx.trans.find_by_username(&ctx.item.item_code).await?; + // Look reads it, so we ensure we save it first. + ctx.trans.save_item_model(&ctx.item).await?; + look::VERB + .handle( + &mut VerbContext { + session: &sess, + session_dat: &mut session_dat, + trans: ctx.trans, + user_dat: &mut user, + }, + "look", + "", + ) + .await?; } if let Some((old_loc_type, old_loc_code)) = use_location.split_once("/") { - if let Some(old_room_item) = trans + if let Some(old_room_item) = ctx + .trans .find_item_by_type_code(old_loc_type, old_loc_code) .await? { if let Some((new_loc_type, new_loc_code)) = new_loc.split_once("/") { if let Some(new_room_item) = match new_loc_item { None => { - trans + ctx.trans .find_item_by_type_code(new_loc_type, new_loc_code) .await? } v => v.map(Arc::new), } { - announce_move(&trans, &mover, &old_room_item, &new_room_item).await?; + announce_move(&ctx.trans, ctx.item, &old_room_item, &new_room_item).await?; } } } @@ -631,26 +631,17 @@ pub async fn attempt_move_immediate( pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - _ctx: &mut VerbContext<'_>, - _command: &QueueCommand, - ) -> UResult { + async fn start_command(&self, _ctx: &mut QueuedCommandContext<'_>) -> UResult { Ok(time::Duration::from_secs(1)) } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let direction = match command { - QueueCommand::Movement { direction } => direction, + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + let (direction, source) = match ctx.command { + QueueCommand::Movement { direction, source } => (direction, source), _ => user_error("Unexpected command".to_owned())?, }; - let player_item = get_player_item_or_fail(ctx).await?; - attempt_move_immediate(ctx.trans, &player_item, direction, &mut Some(ctx)).await?; + attempt_move_immediate(direction, ctx, source).await?; Ok(()) } } @@ -667,10 +658,13 @@ impl UserVerb for Verb { ) -> UResult<()> { let dir = Direction::parse(&(verb.to_owned() + " " + remaining.trim()).trim()) .ok_or_else(|| UserError("Unknown direction".to_owned()))?; - queue_command( + let player_item = get_player_item_or_fail(ctx).await?; + queue_command_and_save( ctx, + &player_item, &QueueCommand::Movement { direction: dir.clone(), + source: MovementSource::Command, }, ) .await diff --git a/blastmud_game/src/message_handler/user_commands/open.rs b/blastmud_game/src/message_handler/user_commands/open.rs index 75a3097..4215977 100644 --- a/blastmud_game/src/message_handler/user_commands/open.rs +++ b/blastmud_game/src/message_handler/user_commands/open.rs @@ -10,7 +10,9 @@ use crate::{ task::{Task, TaskDetails, TaskMeta}, }, regular_tasks::{ - queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, TaskHandler, TaskRunContext, }, services::comms::broadcast_to_room, @@ -92,18 +94,16 @@ impl TaskHandler for SwingShutHandler { } pub async fn attempt_open_immediate( - trans: &DBTrans, - ctx_opt: &mut Option<&mut VerbContext<'_>>, - who: &Item, + ctx: &mut QueuedCommandContext<'_>, direction: &Direction, ) -> UResult<()> { - let use_location = if who.death_data.is_some() { + let use_location = if ctx.item.death_data.is_some() { user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())? } else { - &who.location + &ctx.item.location }; let (room_1, dir_in_room, room_2) = - match is_door_in_direction(trans, &direction, use_location).await? { + match is_door_in_direction(ctx.trans, &direction, use_location).await? { DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?, DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, @@ -120,7 +120,8 @@ pub async fn attempt_open_immediate( } => { let entering_room_loc = room_with_door.refstr(); if let Some(revdir) = direction.reverse() { - if let Some(lock) = trans + if let Some(lock) = ctx + .trans .find_by_action_and_location( &entering_room_loc, &LocationActionType::InstalledOnDoorAsLock(revdir.clone()), @@ -128,18 +129,13 @@ pub async fn attempt_open_immediate( .await? .first() { - if let Some(ctx) = ctx_opt { - if let Some(lockcheck) = lock - .possession_type - .as_ref() - .and_then(|pt| possession_data().get(pt)) - .and_then(|pd| pd.lockcheck_handler) - { - lockcheck.cmd(ctx, &who, &lock).await? - } - } else { - // NPCs don't unlock doors. - user_error("Can't get through locked doors".to_owned())?; + if let Some(lockcheck) = lock + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + .and_then(|pd| pd.lockcheck_handler) + { + lockcheck.cmd(ctx, &lock).await? } } let mut entering_room_mut = (*room_with_door).clone(); @@ -148,7 +144,7 @@ pub async fn attempt_open_immediate( (*door).open = true; } } - trans.save_item_model(&entering_room_mut).await?; + ctx.trans.save_item_model(&entering_room_mut).await?; (room_with_door, revdir, current_room) } else { user_error("There's no door possible there.".to_owned())? @@ -165,7 +161,7 @@ pub async fn attempt_open_immediate( (*door).open = true; } } - trans.save_item_model(&entering_room_mut).await?; + ctx.trans.save_item_model(&entering_room_mut).await?; (room_with_door, direction.clone(), new_room) } }; @@ -181,24 +177,24 @@ pub async fn attempt_open_immediate( ), ] { broadcast_to_room( - &trans, + &ctx.trans, loc, None, &format!( "{} opens the door to the {}.\n", - &who.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), dir ), Some(&format!( "{} opens the door to the {}.\n", - &who.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), dir )), ) .await?; } - trans + ctx.trans .upsert_task(&Task { meta: TaskMeta { task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()), @@ -218,20 +214,15 @@ pub async fn attempt_open_immediate( pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let direction = match command { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + let direction = match ctx.command { QueueCommand::OpenDoor { direction } => direction, _ => user_error("Unexpected command".to_owned())?, }; - let player_item = get_player_item_or_fail(ctx).await?; - let use_location = if player_item.death_data.is_some() { + let use_location = if ctx.item.death_data.is_some() { user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())? } else { - &player_item.location + &ctx.item.location }; match is_door_in_direction(&ctx.trans, &direction, use_location).await? { DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?, @@ -264,7 +255,7 @@ impl QueueCommandHandler for QueueHandler { .and_then(|pt| possession_data().get(pt)) .and_then(|pd| pd.lockcheck_handler) { - lockcheck.cmd(ctx, &player_item, &lock).await? + lockcheck.cmd(ctx, &lock).await? } } } @@ -276,17 +267,12 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let direction = match command { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + let direction = match ctx.command { QueueCommand::OpenDoor { direction } => direction, _ => user_error("Unexpected command".to_owned())?, }; - let player_item = get_player_item_or_fail(ctx).await?; - attempt_open_immediate(&ctx.trans, &mut Some(ctx), &player_item, &direction).await?; + attempt_open_immediate(ctx, &direction).await?; Ok(()) } @@ -359,8 +345,10 @@ impl UserVerb for Verb { ) -> UResult<()> { let dir = Direction::parse(remaining).ok_or_else(|| UserError("Unknown direction".to_owned()))?; - queue_command( + let player_item = get_player_item_or_fail(ctx).await?; + queue_command_and_save( ctx, + &player_item, &QueueCommand::OpenDoor { direction: dir.clone(), }, diff --git a/blastmud_game/src/message_handler/user_commands/remove.rs b/blastmud_game/src/message_handler/user_commands/remove.rs index dcd1543..8f4b23a 100644 --- a/blastmud_game/src/message_handler/user_commands/remove.rs +++ b/blastmud_game/src/message_handler/user_commands/remove.rs @@ -4,22 +4,20 @@ use super::{ }; use crate::{ models::item::{BuffCause, Item, LocationActionType}, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{comms::broadcast_to_room, skills::calculate_total_stats_skills_for_user}, static_content::possession_type::{possession_data, WearData}, }; use async_trait::async_trait; use std::time; -async fn check_removeable( - ctx: &mut VerbContext<'_>, - item: &Item, - player_item: &Item, -) -> UResult<()> { - if item.location != player_item.refstr() { +async fn check_removeable(ctx: &mut QueuedCommandContext<'_>, item: &Item) -> UResult<()> { + if item.location != ctx.item.refstr() { user_error(format!( "You try to remove {} but realise you no longer have it.", - &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + &item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } @@ -46,7 +44,7 @@ async fn check_removeable( let other_clothes = ctx .trans - .find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn) + .find_by_action_and_location(&ctx.item.refstr(), &LocationActionType::Worn) .await?; if let Some(my_worn_since) = item.action_type_started { @@ -70,8 +68,8 @@ async fn check_removeable( }) { user_error(format!( "You can't do that without first removing your {} from your {}.", - &other_item.display_for_session(&ctx.session_dat), - part.display(player_item.sex.clone()) + &other_item.display_for_sentence(ctx.explicit().await?, 1, false), + part.display(ctx.item.sex.clone()) ))?; } } @@ -82,18 +80,13 @@ async fn check_removeable( pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You try to remove it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Remove { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -106,21 +99,21 @@ impl QueueCommandHandler for QueueHandler { Some(it) => it, }; - check_removeable(ctx, &item, &player_item).await?; + check_removeable(ctx, &item).await?; let msg_exp = format!( "{} fumbles around trying to take off {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} fumbles around trying to take off {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -130,18 +123,13 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You try to remove it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Remove { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -154,21 +142,21 @@ impl QueueCommandHandler for QueueHandler { Some(it) => it, }; - check_removeable(ctx, &item, &player_item).await?; + check_removeable(ctx, &item).await?; let msg_exp = format!( "{} removes {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} removes {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -189,10 +177,12 @@ impl QueueCommandHandler for QueueHandler { .to_owned(), ) })?; + ctx.trans.save_item_model(&item_mut).await?; if wear_data.dodge_penalty != 0.0 { - let mut player_item_mut = (*player_item).clone(); - player_item_mut.temporary_buffs = player_item_mut + ctx.item.temporary_buffs = ctx + .item .temporary_buffs + .clone() .into_iter() .filter(|buf| { buf.cause @@ -202,13 +192,12 @@ impl QueueCommandHandler for QueueHandler { } }) .collect(); - if let Some(ref usr) = ctx.user_dat { - calculate_total_stats_skills_for_user(&mut player_item_mut, usr); + if ctx.item.item_type == "player" { + if let Some(usr) = ctx.trans.find_by_username(&ctx.item.item_code).await? { + calculate_total_stats_skills_for_user(ctx.item, &usr); + } } - ctx.trans.save_item_model(&player_item_mut).await?; } - - ctx.trans.save_item_model(&item_mut).await?; Ok(()) } } @@ -247,6 +236,7 @@ impl UserVerb for Verb { } let mut did_anything: bool = false; + let mut player_item_mut = (*player_item).clone(); for target in targets .iter() .filter(|t| t.action_type.is_visible_in_look()) @@ -257,6 +247,7 @@ impl UserVerb for Verb { did_anything = true; queue_command( ctx, + &mut player_item_mut, &QueueCommand::Remove { possession_id: target.item_code.clone(), }, @@ -265,6 +256,8 @@ impl UserVerb for Verb { } if !did_anything { user_error("I didn't find anything matching.".to_owned())?; + } else { + ctx.trans.save_item_model(&player_item_mut).await?; } Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands/use_cmd.rs b/blastmud_game/src/message_handler/user_commands/use_cmd.rs index 24a68e5..748b20b 100644 --- a/blastmud_game/src/message_handler/user_commands/use_cmd.rs +++ b/blastmud_game/src/message_handler/user_commands/use_cmd.rs @@ -5,30 +5,28 @@ use super::{ use crate::{ language, models::item::SkillType, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{ check_consent, comms::broadcast_to_room, effect::run_effects, skills::skill_check_and_grind, }, static_content::possession_type::possession_data, }; use async_trait::async_trait; +use std::sync::Arc; use std::time; pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You try to use it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let (item_id, target_type_code) = match command { + let (item_id, target_type_code) = match ctx.command { QueueCommand::Use { possession_id, target_id, @@ -43,19 +41,19 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != format!("player/{}", player_item.item_code) { + if item.location != format!("player/{}", ctx.item.item_code) { user_error(format!( "You try to use {} but realise you no longer have it", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } let (target_type, target_code) = match target_type_code.split_once("/") { None => user_error("Couldn't handle use command (invalid target)".to_owned())?, Some(spl) => spl, }; - let is_self_use = target_type == "player" && target_code == player_item.item_code; + let is_self_use = target_type == "player" && target_code == ctx.item.item_code; let target = if is_self_use { - player_item.clone() + Arc::new(ctx.item.clone()) } else { match ctx .trans @@ -70,62 +68,57 @@ impl QueueCommandHandler for QueueHandler { } }; if !is_self_use - && target.location != player_item.location - && target.location != format!("player/{}", player_item.item_code) + && target.location != ctx.item.location + && target.location != format!("player/{}", ctx.item.item_code) { - let target_name = - target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false); + let explicit = ctx.explicit().await?; + let target_name = target.display_for_sentence(explicit, 1, false); user_error(format!( "You try to use {} on {}, but realise {} is no longer here", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false), + item.display_for_sentence(explicit, 1, false), target_name, target_name ))? } let msg_exp = format!( "{} prepares to use {} {} on {}\n", - &player_item.display_for_sentence(true, 1, true), - &player_item.pronouns.possessive, + &ctx.item.display_for_sentence(true, 1, true), + &ctx.item.pronouns.possessive, &item.display_for_sentence(true, 1, false), &if is_self_use { - player_item.pronouns.intensive.clone() + ctx.item.pronouns.intensive.clone() } else { - player_item.display_for_sentence(true, 1, false) + ctx.item.display_for_sentence(true, 1, false) } ); let msg_nonexp = format!( "{} prepares to use {} {} on {}\n", - &player_item.display_for_sentence(false, 1, true), - &player_item.pronouns.possessive, + &ctx.item.display_for_sentence(false, 1, true), + &ctx.item.pronouns.possessive, &item.display_for_sentence(false, 1, false), &if is_self_use { - player_item.pronouns.intensive.clone() + ctx.item.pronouns.intensive.clone() } else { - player_item.display_for_sentence(true, 1, false) + ctx.item.display_for_sentence(true, 1, false) } ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), ) .await?; - let mut draw_level: f64 = *player_item + let mut draw_level: f64 = *ctx + .item .total_skills .get(&SkillType::Quickdraw) .to_owned() .unwrap_or(&8.0); - let mut player_item_mut = (*player_item).clone(); - let skill_result = skill_check_and_grind( - ctx.trans, - &mut player_item_mut, - &SkillType::Quickdraw, - draw_level, - ) - .await?; + let skill_result = + skill_check_and_grind(ctx.trans, ctx.item, &SkillType::Quickdraw, draw_level).await?; if skill_result < -0.5 { draw_level -= 2.0; } else if skill_result < -0.25 { @@ -135,7 +128,6 @@ impl QueueCommandHandler for QueueHandler { } else if skill_result > 0.25 { draw_level += 1.0; } - ctx.trans.save_item_model(&player_item_mut).await?; let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0); Ok(time::Duration::from_millis( @@ -144,19 +136,14 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You try to use it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let (ref item_id, ref target_type_code) = match command { + let (ref item_id, ref target_type_code) = match ctx.command { QueueCommand::Use { possession_id, target_id, @@ -171,10 +158,10 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != format!("player/{}", player_item.item_code) { + if item.location != format!("player/{}", ctx.item.item_code) { user_error(format!( "You try to use {} but realise you no longer have it", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + item.display_for_sentence(ctx.explicit().await?, 1, false) ))? } let (ref target_type, ref target_code) = match target_type_code.split_once("/") { @@ -189,14 +176,14 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Couldn't handle use command (target missing)".to_owned())?, Some(it) => it, }; - if target.location != player_item.location - && target.location != format!("player/{}", player_item.item_code) + if target.location != ctx.item.location + && target.location != format!("player/{}", ctx.item.item_code) { - let target_name = - target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false); + let explicit = ctx.explicit().await?; + let target_name = target.display_for_sentence(explicit, 1, false); user_error(format!( "You try to use {} on {}, but realise {} is no longer here", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false), + item.display_for_sentence(explicit, 1, false), target_name, target_name ))? @@ -211,10 +198,10 @@ impl QueueCommandHandler for QueueHandler { Some(d) => d, }; if let Some(consent_type) = use_data.needs_consent_check.as_ref() { - if !check_consent(ctx.trans, "use", consent_type, &player_item, &target).await? { + if !check_consent(ctx.trans, "use", consent_type, &ctx.item, &target).await? { user_error(format!( "{} doesn't allow {} from you", - &target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true), + &target.display_for_sentence(ctx.explicit().await?, 1, true), consent_type.to_str() ))? } @@ -228,7 +215,7 @@ impl QueueCommandHandler for QueueHandler { if item.charges < 1 { user_error(format!( "{} has no {} {} left", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true), + item.display_for_sentence(ctx.explicit().await?, 1, true), &language::pluralise(charge_data.charge_name_prefix), charge_data.charge_name_suffix ))?; @@ -248,17 +235,17 @@ impl QueueCommandHandler for QueueHandler { ) .await? { + let explicit = ctx.explicit().await?; user_error(format!( "You see no reason to use {} on {}", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false), - target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + item.display_for_sentence(explicit, 1, false), + target.display_for_sentence(explicit, 1, false) ))?; } - let is_self_use = target_type == &"player" && target_code == &player_item.item_code; - let mut player_mut = (*player_item).clone(); + let is_self_use = target_type == &"player" && target_code == &ctx.item.item_code; let skillcheck = skill_check_and_grind( &ctx.trans, - &mut player_mut, + ctx.item, &use_data.uses_skill, use_data.diff_level, ) @@ -280,7 +267,7 @@ impl QueueCommandHandler for QueueHandler { run_effects( ctx.trans, &effects, - &mut player_mut, + ctx.item, &item, &mut target_mut, skilllvl, @@ -290,7 +277,6 @@ impl QueueCommandHandler for QueueHandler { if let Some(target_mut_save) = target_mut { ctx.trans.save_item_model(&target_mut_save).await?; } - ctx.trans.save_item_model(&player_mut).await?; let mut item_mut = (*item).clone(); let mut save_item = false; @@ -396,8 +382,9 @@ impl UserVerb for Verb { if let Some(err) = (use_data.errorf)(&item, &target) { user_error(err)?; } - queue_command( + queue_command_and_save( ctx, + &player_item, &QueueCommand::Use { possession_id: item.item_code.clone(), target_id: format!("{}/{}", target.item_type, target.item_code), diff --git a/blastmud_game/src/message_handler/user_commands/wear.rs b/blastmud_game/src/message_handler/user_commands/wear.rs index ffe6f0e..6a9f236 100644 --- a/blastmud_game/src/message_handler/user_commands/wear.rs +++ b/blastmud_game/src/message_handler/user_commands/wear.rs @@ -4,7 +4,9 @@ use super::{ }; use crate::{ models::item::{Buff, BuffCause, BuffImpact, LocationActionType, SkillType}, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{comms::broadcast_to_room, skills::calculate_total_stats_skills_for_user}, static_content::possession_type::possession_data, }; @@ -15,18 +17,13 @@ use std::time; pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You try to wear it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Wear { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -38,10 +35,11 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.refstr() { + let explicit = ctx.explicit().await?; + if item.location != ctx.item.refstr() { user_error(format!( "You try to wear {} but realise you no longer have it", - item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + item.display_for_sentence(explicit, 1, false) ))? } @@ -64,17 +62,17 @@ impl QueueCommandHandler for QueueHandler { let msg_exp = format!( "{} fumbles around trying to put on {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} fumbles around trying to put on {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -84,18 +82,13 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You try to wear it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Wear { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -107,10 +100,11 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != player_item.refstr() { + let explicit = ctx.explicit().await?; + if item.location != ctx.item.refstr() { user_error(format!( "You try to wear {} but realise it is no longer there.", - &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + &item.display_for_sentence(explicit, 1, false) ))? } @@ -133,7 +127,7 @@ impl QueueCommandHandler for QueueHandler { let other_clothes = ctx .trans - .find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn) + .find_by_action_and_location(&ctx.item.refstr(), &LocationActionType::Worn) .await?; for part in &wear_data.covers_parts { let thickness: f64 = @@ -153,24 +147,24 @@ impl QueueCommandHandler for QueueHandler { if thickness > 12.0 { user_error(format!( "You're wearing too much on your {} already.", - part.display(player_item.sex.clone()) + part.display(ctx.item.sex.clone()) ))?; } } let msg_exp = format!( "{} wears {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} wears {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -181,8 +175,7 @@ impl QueueCommandHandler for QueueHandler { item_mut.action_type_started = Some(Utc::now()); if wear_data.dodge_penalty != 0.0 { - let mut player_item_mut = (*player_item).clone(); - player_item_mut.temporary_buffs.push(Buff { + ctx.item.temporary_buffs.push(Buff { description: "Dodge penalty".to_owned(), cause: BuffCause::ByItem { item_type: item_mut.item_type.clone(), @@ -193,10 +186,11 @@ impl QueueCommandHandler for QueueHandler { magnitude: -wear_data.dodge_penalty, }], }); - if let Some(ref usr) = ctx.user_dat { - calculate_total_stats_skills_for_user(&mut player_item_mut, usr); + if ctx.item.item_type == "player" { + if let Some(usr) = ctx.trans.find_by_username(&ctx.item.item_code).await? { + calculate_total_stats_skills_for_user(ctx.item, &usr); + } } - ctx.trans.save_item_model(&player_item_mut).await?; } ctx.trans.save_item_model(&item_mut).await?; @@ -238,6 +232,7 @@ impl UserVerb for Verb { } let mut did_anything: bool = false; + let mut player_item_mut = (*player_item).clone(); for target in targets .iter() .filter(|t| t.action_type.is_visible_in_look()) @@ -248,6 +243,7 @@ impl UserVerb for Verb { did_anything = true; queue_command( ctx, + &mut player_item_mut, &QueueCommand::Wear { possession_id: target.item_code.clone(), }, @@ -256,6 +252,8 @@ impl UserVerb for Verb { } if !did_anything { user_error("I didn't find anything matching.".to_owned())?; + } else { + ctx.trans.save_item_model(&player_item_mut).await?; } Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands/wield.rs b/blastmud_game/src/message_handler/user_commands/wield.rs index 05760af..c8eb553 100644 --- a/blastmud_game/src/message_handler/user_commands/wield.rs +++ b/blastmud_game/src/message_handler/user_commands/wield.rs @@ -4,7 +4,9 @@ use super::{ }; use crate::{ models::item::{LocationActionType, SkillType}, - regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler}, + regular_tasks::queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, services::{comms::broadcast_to_room, skills::skill_check_and_grind}, static_content::possession_type::possession_data, }; @@ -14,18 +16,13 @@ use std::time; pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { user_error( "You try to wield it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Wield { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -37,43 +34,39 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != format!("player/{}", player_item.item_code) { + if item.location != format!("player/{}", ctx.item.item_code) { user_error("You try to wield it but realise you no longer have it".to_owned())? } let msg_exp = format!( "{} fumbles around with {} {}\n", - &player_item.display_for_sentence(true, 1, true), - &player_item.pronouns.possessive, + &ctx.item.display_for_sentence(true, 1, true), + &ctx.item.pronouns.possessive, &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} fumbles around with {} {}\n", - &player_item.display_for_sentence(false, 1, true), - &player_item.pronouns.possessive, + &ctx.item.display_for_sentence(false, 1, true), + &ctx.item.pronouns.possessive, &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), ) .await?; - let mut draw_level: f64 = *player_item + let mut draw_level: f64 = ctx + .item .total_skills .get(&SkillType::Quickdraw) .to_owned() - .unwrap_or(&8.0); - let mut player_item_mut = (*player_item).clone(); + .unwrap_or(&8.0) + .clone(); - let skill_result = skill_check_and_grind( - ctx.trans, - &mut player_item_mut, - &SkillType::Quickdraw, - draw_level, - ) - .await?; + let skill_result = + skill_check_and_grind(ctx.trans, ctx.item, &SkillType::Quickdraw, draw_level).await?; if skill_result < -0.5 { draw_level -= 2.0; } else if skill_result < -0.25 { @@ -83,7 +76,6 @@ impl QueueCommandHandler for QueueHandler { } else if skill_result > 0.25 { draw_level += 1.0; } - ctx.trans.save_item_model(&player_item_mut).await?; let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0); Ok(time::Duration::from_millis( @@ -92,18 +84,13 @@ impl QueueCommandHandler for QueueHandler { } #[allow(unreachable_patterns)] - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()> { - let player_item = get_player_item_or_fail(ctx).await?; - if player_item.death_data.is_some() { + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { user_error( "You try to wield it, but your ghostly hands slip through it uselessly".to_owned(), )?; } - let item_id = match command { + let item_id = match ctx.command { QueueCommand::Wield { possession_id } => possession_id, _ => user_error("Unexpected command".to_owned())?, }; @@ -115,22 +102,22 @@ impl QueueCommandHandler for QueueHandler { None => user_error("Item not found".to_owned())?, Some(it) => it, }; - if item.location != format!("player/{}", player_item.item_code) { + if item.location != format!("player/{}", ctx.item.item_code) { user_error("You try to wield it but realise you no longer have it".to_owned())? } let msg_exp = format!( "{} wields {}\n", - &player_item.display_for_sentence(true, 1, true), + &ctx.item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false) ); let msg_nonexp = format!( "{} wields {}\n", - &player_item.display_for_sentence(false, 1, true), + &ctx.item.display_for_sentence(false, 1, true), &item.display_for_sentence(false, 1, false) ); broadcast_to_room( ctx.trans, - &player_item.location, + &ctx.item.location, None, &msg_exp, Some(&msg_nonexp), @@ -184,8 +171,9 @@ impl UserVerb for Verb { { user_error("You can't wield that!".to_owned())?; } - queue_command( + queue_command_and_save( ctx, + &player_item, &QueueCommand::Wield { possession_id: weapon.item_code.clone(), }, diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index c91de20..3125cd0 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -1,31 +1,37 @@ -use serde::{Serialize, Deserialize}; -use std::collections::BTreeMap; -use crate::{ - language, - static_content::species::SpeciesType, - static_content::possession_type::PossessionType, - static_content::room::Direction, -}; use super::session::Session; +use crate::{ + language, regular_tasks::queued_command::QueueCommand, + static_content::possession_type::PossessionType, static_content::room::Direction, + static_content::species::SpeciesType, +}; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; +use std::collections::VecDeque; #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, PartialOrd)] pub enum BuffCause { - WaitingTask { task_code: String, task_type: String }, - ByItem { item_code: String, item_type: String } + WaitingTask { + task_code: String, + task_type: String, + }, + ByItem { + item_code: String, + item_type: String, + }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] pub enum BuffImpact { ChangeStat { stat: StatType, magnitude: f64 }, - ChangeSkill { skill: SkillType, magnitude: f64 } + ChangeSkill { skill: SkillType, magnitude: f64 }, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] pub struct Buff { pub description: String, pub cause: BuffCause, - pub impacts: Vec + pub impacts: Vec, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] @@ -62,47 +68,17 @@ pub enum SkillType { Throw, Track, Wrestle, - Whips + Whips, } impl SkillType { pub fn values() -> Vec { use SkillType::*; - vec!( - Appraise, - Blades, - Bombs, - Chemistry, - Climb, - Clubs, - Craft, - Dodge, - Fish, - Fists, - Flails, - Focus, - Fuck, - Hack, - Locksmith, - Medic, - Persuade, - Pilot, - Pistols, - Quickdraw, - Repair, - Ride, - Rifles, - Scavenge, - Science, - Sneak, - Spears, - Swim, - Teach, - Throw, - Track, - Wrestle, - Whips - ) + vec![ + Appraise, Blades, Bombs, Chemistry, Climb, Clubs, Craft, Dodge, Fish, Fists, Flails, + Focus, Fuck, Hack, Locksmith, Medic, Persuade, Pilot, Pistols, Quickdraw, Repair, Ride, + Rifles, Scavenge, Science, Sneak, Spears, Swim, Teach, Throw, Track, Wrestle, Whips, + ] } pub fn display(&self) -> &'static str { use SkillType::*; @@ -139,12 +115,11 @@ impl SkillType { Throw => "throw", Track => "track", Wrestle => "wrestle", - Whips => "whips" + Whips => "whips", } } } - #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] pub enum StatType { Brains, @@ -152,20 +127,13 @@ pub enum StatType { Brawn, Reflexes, Endurance, - Cool + Cool, } impl StatType { pub fn values() -> Vec { use StatType::*; - vec!( - Brains, - Senses, - Brawn, - Reflexes, - Endurance, - Cool - ) + vec![Brains, Senses, Brawn, Reflexes, Endurance, Cool] } pub fn display(&self) -> &'static str { use StatType::*; @@ -175,7 +143,7 @@ impl StatType { Brawn => "brawn", Reflexes => "reflexes", Endurance => "endurance", - Cool => "cool" + Cool => "cool", } } } @@ -213,7 +181,7 @@ impl Pronouns { is_proper: true, } } - + #[allow(dead_code)] pub fn default_male() -> Pronouns { Pronouns { @@ -226,7 +194,6 @@ impl Pronouns { } } - #[allow(dead_code)] pub fn default_female() -> Pronouns { Pronouns { @@ -246,7 +213,7 @@ pub enum Subattack { Powerattacking, Feinting, Grabbing, - Wrestling + Wrestling, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] @@ -265,14 +232,14 @@ impl LocationActionType { use LocationActionType::*; match self { InstalledOnDoorAsLock(_) => false, - _ => true + _ => true, } } pub fn is_in_direction(&self, dir: &Direction) -> bool { use LocationActionType::*; match self { InstalledOnDoorAsLock(d) if d == dir => true, - _ => false + _ => false, } } } @@ -295,14 +262,14 @@ pub enum ItemFlag { #[serde(default)] pub struct ActiveCombat { pub attacking: Option, - pub attacked_by: Vec + pub attacked_by: Vec, } impl Default for ActiveCombat { fn default() -> Self { Self { attacking: None, - attacked_by: vec!() + attacked_by: vec![], } } } @@ -310,27 +277,27 @@ impl Default for ActiveCombat { #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[serde(default)] pub struct ActiveClimb { - pub height: u64 + pub height: u64, } impl Default for ActiveClimb { fn default() -> Self { - Self { - height: 0 - } + Self { height: 0 } } } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] pub enum ItemSpecialData { - ItemWriting { text: String }, + ItemWriting { + text: String, + }, DynroomData { dynzone_code: String, dynroom_code: String, }, DynzoneData { zone_exit: Option, - vacate_after: Option> + vacate_after: Option>, }, } @@ -344,7 +311,7 @@ pub struct DynamicEntrance { #[serde(default)] pub struct DoorState { pub open: bool, - pub description: String + pub description: String, } impl Default for DoorState { @@ -359,17 +326,33 @@ impl Default for DoorState { #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[serde(default)] pub struct DeathData { - pub parts_remaining: Vec + pub parts_remaining: Vec, } impl Default for DeathData { fn default() -> Self { Self { - parts_remaining: vec!() + parts_remaining: vec![], } } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] +pub enum FollowState { + // Every move is mirrored to the follower. + Active, + // If the followee is in the same room, mirror a movement and go to Active, + // otherwise ignore and don't mirror. This happens after a mirrored move fails, + // or the follower moves independently. + IfSameRoom, +} + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] +pub struct FollowData { + pub follow_whom: String, + pub state: FollowState, +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[serde(default)] pub struct Item { @@ -404,6 +387,8 @@ pub struct Item { pub dynamic_entrance: Option, pub owner: Option, pub door_states: Option>, + pub following: Option, + pub queue: VecDeque, } impl Item { @@ -416,8 +401,11 @@ impl Item { buf.push_str("the body of "); } } - let singular = if explicit_ok { &self.display } else { - self.display_less_explicit.as_ref().unwrap_or(&self.display) }; + let singular = if explicit_ok { + &self.display + } else { + self.display_less_explicit.as_ref().unwrap_or(&self.display) + }; if !self.pronouns.is_proper && pluralise == 1 { buf.push_str(language::indefinite_article(&singular)); buf.push(' '); @@ -441,14 +429,13 @@ impl Item { self.display_for_sentence(!session.less_explicit_mode, 1, false) } - pub fn details_for_session<'l>(self: &'l Self, session: &Session) -> Option<&'l str>{ - self.details.as_ref() - .map(|dets| - session.explicit_if_allowed( - dets.as_str(), - self.details_less_explicit.as_ref().map(String::as_str) - ) + pub fn details_for_session<'l>(self: &'l Self, session: &Session) -> Option<&'l str> { + self.details.as_ref().map(|dets| { + session.explicit_if_allowed( + dets.as_str(), + self.details_less_explicit.as_ref().map(String::as_str), ) + }) } pub fn refstr(&self) -> String { @@ -466,7 +453,7 @@ impl Default for Item { display_less_explicit: None, details: None, details_less_explicit: None, - aliases: vec!(), + aliases: vec![], location: "room/storage".to_owned(), action_type: LocationActionType::Normal, action_type_started: None, @@ -480,7 +467,7 @@ impl Default for Item { total_skills: BTreeMap::new(), temporary_buffs: Vec::new(), pronouns: Pronouns::default_inanimate(), - flags: vec!(), + flags: vec![], sex: None, active_combat: Some(Default::default()), active_climb: None, @@ -490,6 +477,8 @@ impl Default for Item { dynamic_entrance: None, owner: None, door_states: None, + following: None, + queue: VecDeque::new(), } } } diff --git a/blastmud_game/src/models/session.rs b/blastmud_game/src/models/session.rs index 12b5806..2d8b970 100644 --- a/blastmud_game/src/models/session.rs +++ b/blastmud_game/src/models/session.rs @@ -1,14 +1,11 @@ -use serde::{Serialize, Deserialize}; -use std::collections::VecDeque; -use crate::regular_tasks::queued_command::QueueCommand; use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(default)] pub struct Session { pub source: String, pub less_explicit_mode: bool, - pub queue: VecDeque, pub last_active: Option>, // Reminder: Consider backwards compatibility when updating this. New fields should generally // be an Option, or you should ensure the default value is sensible, or things will @@ -16,7 +13,11 @@ pub struct Session { } impl Session { - pub fn explicit_if_allowed<'l>(self: &Self, explicit: &'l str, non_explicit: Option<&'l str>) -> &'l str { + pub fn explicit_if_allowed<'l>( + self: &Self, + explicit: &'l str, + non_explicit: Option<&'l str>, + ) -> &'l str { if self.less_explicit_mode { non_explicit.unwrap_or(explicit) } else { @@ -27,7 +28,10 @@ impl Session { impl Default for Session { fn default() -> Self { - Session { source: "unknown".to_owned(), less_explicit_mode: false, - queue: VecDeque::new(), last_active: None } + Session { + source: "unknown".to_owned(), + less_explicit_mode: false, + last_active: None, + } } } diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index 58be554..138128b 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -1,19 +1,33 @@ use super::{TaskHandler, TaskRunContext}; +#[double] +use crate::db::DBTrans; use crate::message_handler::user_commands::{ - close, cut, drop, get, get_user_or_fail, improvise, movement, open, remove, use_cmd, - user_error, wear, wield, CommandHandlingError, UResult, VerbContext, + close, cut, drop, get, improvise, movement, open, remove, use_cmd, user_error, wear, wield, + CommandHandlingError, UResult, VerbContext, +}; +use crate::message_handler::ListenerSession; +use crate::models::session::Session; +use crate::models::{ + item::Item, + task::{Task, TaskDetails, TaskMeta}, }; -use crate::models::task::{Task, TaskDetails, TaskMeta}; use crate::static_content::{possession_type::PossessionType, room::Direction}; use crate::DResult; use async_trait::async_trait; use chrono::Utc; +use mockall_double::double; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, BTreeSet}; use std::time; -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Ord, PartialOrd)] +pub enum MovementSource { + Command, + Follow, +} + +#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd, Serialize, Deserialize)] pub enum QueueCommand { CloseDoor { direction: Direction, @@ -30,6 +44,7 @@ pub enum QueueCommand { }, Movement { direction: Direction, + source: MovementSource, }, OpenDoor { direction: Direction, @@ -76,18 +91,36 @@ impl QueueCommand { } } +pub struct QueuedCommandContext<'l> { + pub trans: &'l DBTrans, + pub command: &'l QueueCommand, + pub item: &'l mut Item, +} + +impl<'l> QueuedCommandContext<'l> { + pub async fn get_session(&self) -> UResult> { + Ok(if self.item.item_type != "player" { + None + } else { + self.trans + .find_session_for_player(&self.item.item_code) + .await? + }) + } + pub async fn explicit(&self) -> UResult { + Ok(self + .trans + .find_session_for_player(&self.item.item_code) + .await? + .map(|(_, sess)| !sess.less_explicit_mode) + .unwrap_or(false)) + } +} + #[async_trait] pub trait QueueCommandHandler { - async fn start_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult; - async fn finish_command( - &self, - ctx: &mut VerbContext<'_>, - command: &QueueCommand, - ) -> UResult<()>; + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult; + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()>; } fn queue_command_registry( @@ -151,38 +184,64 @@ fn queue_command_registry( }) } -pub async fn queue_command(ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult<()> { - let was_empty = ctx.session_dat.queue.is_empty(); - let username = get_user_or_fail(ctx)?.username.to_lowercase(); - if ctx.session_dat.queue.len() >= 20 { +// Note: Saves item so you can return after calling. +pub async fn queue_command_and_save( + ctx: &mut VerbContext<'_>, + item: &Item, + command: &QueueCommand, +) -> UResult<()> { + let mut item_mut = item.clone(); + queue_command(ctx, &mut item_mut, command).await?; + ctx.trans.save_item_model(&item_mut).await?; + Ok(()) +} + +// Note: Saves item so you can return after calling. +pub async fn queue_command_for_npc_and_save( + trans: &DBTrans, + item: &Item, + command: &QueueCommand, +) -> DResult<()> { + let mut item_mut = item.clone(); + queue_command_for_npc(trans, &mut item_mut, command).await?; + trans.save_item_model(&item_mut).await?; + Ok(()) +} + +// Caller must save item or it won't actually work. +pub async fn queue_command( + ctx: &mut VerbContext<'_>, + item: &mut Item, + command: &QueueCommand, +) -> UResult<()> { + let was_empty = item.queue.is_empty(); + if item.queue.len() >= 20 { user_error("Can't queue more than 20 actions\n".to_owned())?; } - ctx.session_dat.queue.push_back(command.clone()); + item.queue.push_back(command.clone()); if was_empty { match queue_command_registry() .get(&command.name()) .expect("QueueCommand to have been registered") - .start_command(ctx, &command) + .start_command(&mut QueuedCommandContext { + trans: ctx.trans, + command, + item, + }) .await { Err(CommandHandlingError::UserError(err_msg)) => { - ctx.session_dat.queue.truncate(0); - ctx.trans - .save_session_model(ctx.session, ctx.session_dat) - .await?; + item.queue.truncate(0); ctx.trans .queue_for_session(&ctx.session, Some(&(err_msg + "\r\n"))) .await?; } Err(CommandHandlingError::SystemError(e)) => Err(e)?, Ok(dur) => { - ctx.trans - .save_session_model(ctx.session, ctx.session_dat) - .await?; ctx.trans .upsert_task(&Task { meta: TaskMeta { - task_code: username, + task_code: item.refstr(), next_scheduled: Utc::now() + chrono::Duration::from_std(dur)?, ..Default::default() }, @@ -195,9 +254,49 @@ pub async fn queue_command(ctx: &mut VerbContext<'_>, command: &QueueCommand) -> ctx.trans .queue_for_session(ctx.session, Some("[queued]\n")) .await?; - ctx.trans - .save_session_model(ctx.session, ctx.session_dat) - .await?; + } + Ok(()) +} + +// Caller must save item or it won't actually work. +pub async fn queue_command_for_npc( + trans: &DBTrans, + item: &mut Item, + command: &QueueCommand, +) -> DResult<()> { + let was_empty = item.queue.is_empty(); + if item.queue.len() >= 20 { + Err("Can't queue more than 20 actions\n")?; + } + item.queue.push_back(command.clone()); + if was_empty { + match queue_command_registry() + .get(&command.name()) + .expect("QueueCommand to have been registered") + .start_command(&mut QueuedCommandContext { + trans, + command, + item, + }) + .await + { + Err(CommandHandlingError::UserError(_err_msg)) => { + item.queue.truncate(0); + } + Err(CommandHandlingError::SystemError(e)) => Err(e)?, + Ok(dur) => { + trans + .upsert_task(&Task { + meta: TaskMeta { + task_code: item.refstr(), + next_scheduled: Utc::now() + chrono::Duration::from_std(dur)?, + ..Default::default() + }, + details: TaskDetails::RunQueuedCommand, + }) + .await?; + } + } } Ok(()) } @@ -206,67 +305,81 @@ pub struct RunQueuedCommandTaskHandler; #[async_trait] impl TaskHandler for RunQueuedCommandTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { - let username: &str = ctx.task.meta.task_code.as_str(); - let (listener_sess, mut sess_dets) = - match ctx.trans.find_session_for_player(username).await? { - None => { - // Queue is gone if session is gone, and don't schedule another - // job, but otherwise this is a successful run. - return Ok(None); - } - Some(x) => x, - }; - let queue_command = match sess_dets.queue.pop_front() { + let (item_type, item_code) = ctx + .task + .meta + .task_code + .as_str() + .split_once("/") + .ok_or("QueuedCommandHandler has bad item refstr")?; + let mut item = (*(ctx + .trans + .find_item_by_type_code(item_type, item_code) + .await? + .ok_or("Can't find player to process QueuedCommand")?)) + .clone(); + let queue_command = match item.queue.pop_front() { None => { return Ok(None); } Some(x) => x, }; - let mut user = ctx.trans.find_by_username(username).await?; - let mut verbcontext = VerbContext { - session: &listener_sess, - session_dat: &mut sess_dets, - user_dat: &mut user, + let mut qcontext = QueuedCommandContext { trans: ctx.trans, + item: &mut item, + command: &queue_command, }; let uresult_finish = queue_command_registry() .get(&queue_command.name()) .expect("QueueCommand to have been registered") - .finish_command(&mut verbcontext, &queue_command) + .finish_command(&mut qcontext) .await; match uresult_finish { Ok(()) => {} Err(CommandHandlingError::UserError(err_msg)) => { - ctx.trans - .queue_for_session(&listener_sess, Some(&(err_msg + "\r\n"))) - .await?; - sess_dets.queue.truncate(0); - ctx.trans - .save_session_model(&listener_sess, &sess_dets) - .await?; + if item.item_type == "player" { + if let Some((listener_sess, _)) = + ctx.trans.find_session_for_player(&item.item_code).await? + { + ctx.trans + .queue_for_session(&listener_sess, Some(&(err_msg + "\r\n"))) + .await?; + } + } + item.queue.truncate(0); + ctx.trans.save_item_model(&item).await?; return Ok(None); } Err(CommandHandlingError::SystemError(e)) => Err(e)?, }; - let next_command_opt = verbcontext.session_dat.queue.front().cloned(); + let next_command_opt = item.queue.front().cloned(); let result = match next_command_opt { None => None, Some(next_command) => { + let mut qcontext = QueuedCommandContext { + trans: ctx.trans, + item: &mut item, + command: &next_command, + }; match queue_command_registry() .get(&next_command.name()) .expect("QueueCommand to have been registered") - .start_command(&mut verbcontext, &next_command) + .start_command(&mut qcontext) .await { Err(CommandHandlingError::UserError(err_msg)) => { - ctx.trans - .queue_for_session(&listener_sess, Some(&(err_msg + "\r\n"))) - .await?; - sess_dets.queue.truncate(0); - ctx.trans - .save_session_model(&listener_sess, &sess_dets) - .await?; + if item.item_type == "player" { + if let Some((listener_sess, _)) = + ctx.trans.find_session_for_player(&item.item_code).await? + { + ctx.trans + .queue_for_session(&listener_sess, Some(&(err_msg + "\r\n"))) + .await?; + } + } + item.queue.truncate(0); + ctx.trans.save_item_model(&item).await?; None } Err(CommandHandlingError::SystemError(e)) => Err(e)?, @@ -274,9 +387,7 @@ impl TaskHandler for RunQueuedCommandTaskHandler { } } }; - ctx.trans - .save_session_model(&listener_sess, &sess_dets) - .await?; + ctx.trans.save_item_model(&item).await?; Ok(result) } diff --git a/blastmud_game/src/static_content/npc.rs b/blastmud_game/src/static_content/npc.rs index 9c77101..08e4c7a 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -1,42 +1,36 @@ use super::{ - StaticItem, - StaticTask, possession_type::PossessionType, + room::{resolve_exit, room_map_by_code}, species::SpeciesType, - room::{ - room_map_by_code, - resolve_exit - } + StaticItem, StaticTask, }; -use crate::models::{ - item::{Item, Pronouns, SkillType}, - task::{Task, TaskMeta, TaskRecurrence, TaskDetails}, - consent::{ConsentType}, +use crate::{ + message_handler::user_commands::{ + say::say_to_room, CommandHandlingError, UResult, VerbContext, + }, + models::{ + consent::ConsentType, + item::{Item, Pronouns, SkillType}, + task::{Task, TaskDetails, TaskMeta, TaskRecurrence}, + }, + regular_tasks::{ + queued_command::{queue_command_for_npc_and_save, MovementSource, QueueCommand}, + TaskHandler, TaskRunContext, + }, + services::combat::{corpsify_item, start_attack}, + DResult, }; -use crate::services::{ - combat::{ - corpsify_item, - start_attack, - } -}; -use once_cell::sync::OnceCell; -use std::collections::BTreeMap; -use crate::message_handler::user_commands::{ - VerbContext, UResult, CommandHandlingError, - say::say_to_room, - movement::attempt_move_immediate, -}; -use crate::DResult; use async_trait::async_trait; use chrono::Utc; -use rand::{thread_rng, Rng, prelude::*}; -use crate::regular_tasks::{TaskHandler, TaskRunContext}; use log::info; +use once_cell::sync::OnceCell; +use rand::{prelude::*, thread_rng, Rng}; +use std::collections::BTreeMap; use std::time; -pub mod statbot; mod melbs_citizen; mod melbs_dog; +pub mod statbot; #[async_trait] pub trait NPCMessageHandler { @@ -45,21 +39,21 @@ pub trait NPCMessageHandler { ctx: &mut VerbContext, source: &Item, target: &Item, - message: &str + message: &str, ) -> UResult<()>; } #[derive(Clone, Debug)] pub enum NPCSayType { // Bool is true if it should be filtered for less-explicit. - FromFixedList(Vec<(bool, &'static str)>) + FromFixedList(Vec<(bool, &'static str)>), } #[derive(Clone, Debug)] pub struct NPCSayInfo { pub say_code: &'static str, pub frequency_secs: u64, - pub talk_type: NPCSayType + pub talk_type: NPCSayType, } pub struct KillBonus { @@ -96,178 +90,209 @@ impl Default for NPC { description: "default", spawn_location: "default", message_handler: None, - aliases: vec!(), - says: vec!(), + aliases: vec![], + says: vec![], total_xp: 1000, - total_skills: SkillType::values().into_iter() - .map(|sk| (sk.clone(), if &sk == &SkillType::Dodge { 8.0 } else { 10.0 })).collect(), + total_skills: SkillType::values() + .into_iter() + .map(|sk| { + ( + sk.clone(), + if &sk == &SkillType::Dodge { 8.0 } else { 10.0 }, + ) + }) + .collect(), aggression: 0, max_health: 24, intrinsic_weapon: None, species: SpeciesType::Human, - wander_zones: vec!(), + wander_zones: vec![], kill_bonus: None, - player_consents: vec!(), + player_consents: vec![], } } } pub fn npc_list() -> &'static Vec { static NPC_LIST: OnceCell> = OnceCell::new(); - NPC_LIST.get_or_init( - || { - let mut npcs = vec!( - NPC { - code: "repro_xv_chargen_statbot", - name: "Statbot", - description: "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.", - spawn_location: "room/repro_xv_chargen", - message_handler: Some(&statbot::StatbotMessageHandler), - says: vec!(), - ..Default::default() - }, - ); - npcs.append(&mut melbs_citizen::npc_list()); - npcs.append(&mut melbs_dog::npc_list()); - npcs - }) + NPC_LIST.get_or_init(|| { + let mut npcs = vec![NPC { + code: "repro_xv_chargen_statbot", + name: "Statbot", + description: + "A silvery shiny metal mechanical being. It lets out a whirring sound as it moves.", + spawn_location: "room/repro_xv_chargen", + message_handler: Some(&statbot::StatbotMessageHandler), + says: vec![], + ..Default::default() + }]; + npcs.append(&mut melbs_citizen::npc_list()); + npcs.append(&mut melbs_dog::npc_list()); + npcs + }) } pub fn npc_by_code() -> &'static BTreeMap<&'static str, &'static NPC> { static NPC_CODE_MAP: OnceCell> = OnceCell::new(); - NPC_CODE_MAP.get_or_init( - || npc_list().iter() - .map(|npc| (npc.code, npc)) - .collect()) + NPC_CODE_MAP.get_or_init(|| npc_list().iter().map(|npc| (npc.code, npc)).collect()) } -pub fn npc_say_info_by_npc_code_say_code() -> &'static BTreeMap<(&'static str, &'static str), - &'static NPCSayInfo> { - static NPC_SAYINFO_MAP: OnceCell> = OnceCell::new(); - NPC_SAYINFO_MAP.get_or_init( - || npc_list().iter().flat_map( - |npc| npc.says.iter().map( - |says| ((npc.code, says.say_code), says) - ) - ).collect()) +pub fn npc_say_info_by_npc_code_say_code( +) -> &'static BTreeMap<(&'static str, &'static str), &'static NPCSayInfo> { + static NPC_SAYINFO_MAP: OnceCell> = + OnceCell::new(); + NPC_SAYINFO_MAP.get_or_init(|| { + npc_list() + .iter() + .flat_map(|npc| { + npc.says + .iter() + .map(|says| ((npc.code, says.say_code), says)) + }) + .collect() + }) } pub fn npc_static_items() -> Box> { Box::new(npc_list().iter().map(|c| StaticItem { item_code: c.code, - initial_item: Box::new(|| Item { - item_code: c.code.to_owned(), - item_type: "npc".to_owned(), - display: c.name.to_owned(), - details: Some(c.description.to_owned()), - location: c.spawn_location.to_owned(), - is_static: true, - pronouns: c.pronouns.clone(), - total_xp: c.total_xp.clone(), - total_skills: c.total_skills.clone(), - species: c.species.clone(), - health: c.max_health.clone(), - aliases: c.aliases.iter().map(|a| (*a).to_owned()).collect::>(), - ..Item::default() - }) + initial_item: Box::new(|| { + Item { + item_code: c.code.to_owned(), + item_type: "npc".to_owned(), + display: c.name.to_owned(), + details: Some(c.description.to_owned()), + location: c.spawn_location.to_owned(), + is_static: true, + pronouns: c.pronouns.clone(), + total_xp: c.total_xp.clone(), + total_skills: c.total_skills.clone(), + species: c.species.clone(), + health: c.max_health.clone(), + aliases: c + .aliases + .iter() + .map(|a| (*a).to_owned()) + .collect::>(), + ..Item::default() + } + }), })) } pub fn npc_say_tasks() -> Box> { - Box::new(npc_list().iter().flat_map(|c| c.says.iter().map(|say| StaticTask { - task_code: c.code.to_owned() + "_" + say.say_code, - initial_task: Box::new( - || { - let mut rng = thread_rng(); - Task { - meta: TaskMeta { - task_code: c.code.to_owned() + "_" + say.say_code, - is_static: true, - recurrence: Some(TaskRecurrence::FixedDuration { seconds: say.frequency_secs as u32 }), - next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..say.frequency_secs) as i64), - ..TaskMeta::default() - }, - details: TaskDetails::NPCSay { - npc_code: c.code.to_owned(), - say_code: say.say_code.to_owned() - }, - } + Box::new(npc_list().iter().flat_map(|c| { + c.says.iter().map(|say| StaticTask { + task_code: c.code.to_owned() + "_" + say.say_code, + initial_task: Box::new(|| { + let mut rng = thread_rng(); + Task { + meta: TaskMeta { + task_code: c.code.to_owned() + "_" + say.say_code, + is_static: true, + recurrence: Some(TaskRecurrence::FixedDuration { + seconds: say.frequency_secs as u32, + }), + next_scheduled: Utc::now() + + chrono::Duration::seconds( + rng.gen_range(0..say.frequency_secs) as i64 + ), + ..TaskMeta::default() + }, + details: TaskDetails::NPCSay { + npc_code: c.code.to_owned(), + say_code: say.say_code.to_owned(), + }, + } + }), }) - }))) + })) } pub fn npc_wander_tasks() -> Box> { - Box::new(npc_list().iter().filter(|c| !c.wander_zones.is_empty()) - .map(|c| StaticTask { - task_code: c.code.to_owned(), - initial_task: Box::new( - || { - let mut rng = thread_rng(); - Task { - meta: TaskMeta { - task_code: c.code.to_owned(), - is_static: true, - recurrence: Some(TaskRecurrence::FixedDuration { seconds: rng.gen_range(250..350) as u32 }), - next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..300) as i64), - ..TaskMeta::default() - }, - details: TaskDetails::NPCWander { - npc_code: c.code.to_owned(), - }, - } - }) - })) + Box::new( + npc_list() + .iter() + .filter(|c| !c.wander_zones.is_empty()) + .map(|c| StaticTask { + task_code: c.code.to_owned(), + initial_task: Box::new(|| { + let mut rng = thread_rng(); + Task { + meta: TaskMeta { + task_code: c.code.to_owned(), + is_static: true, + recurrence: Some(TaskRecurrence::FixedDuration { + seconds: rng.gen_range(250..350) as u32, + }), + next_scheduled: Utc::now() + + chrono::Duration::seconds(rng.gen_range(0..300) as i64), + ..TaskMeta::default() + }, + details: TaskDetails::NPCWander { + npc_code: c.code.to_owned(), + }, + } + }), + }), + ) } pub fn npc_aggro_tasks() -> Box> { - Box::new(npc_list().iter().filter(|c| c.aggression != 0) - .map(|c| StaticTask { - task_code: c.code.to_owned(), - initial_task: Box::new( - || { - let mut rng = thread_rng(); - let aggro_time = (rng.gen_range(450..550) as u64) / c.aggression; - Task { - meta: TaskMeta { - task_code: c.code.to_owned(), - is_static: true, - recurrence: Some(TaskRecurrence::FixedDuration { seconds: aggro_time as u32 }), - next_scheduled: Utc::now() + chrono::Duration::seconds(rng.gen_range(0..aggro_time) as i64), - ..TaskMeta::default() - }, - details: TaskDetails::NPCAggro { - npc_code: c.code.to_owned(), - }, - } - }) - })) + Box::new( + npc_list() + .iter() + .filter(|c| c.aggression != 0) + .map(|c| StaticTask { + task_code: c.code.to_owned(), + initial_task: Box::new(|| { + let mut rng = thread_rng(); + let aggro_time = (rng.gen_range(450..550) as u64) / c.aggression; + Task { + meta: TaskMeta { + task_code: c.code.to_owned(), + is_static: true, + recurrence: Some(TaskRecurrence::FixedDuration { + seconds: aggro_time as u32, + }), + next_scheduled: Utc::now() + + chrono::Duration::seconds(rng.gen_range(0..aggro_time) as i64), + ..TaskMeta::default() + }, + details: TaskDetails::NPCAggro { + npc_code: c.code.to_owned(), + }, + } + }), + }), + ) } - pub struct NPCSayTaskHandler; #[async_trait] impl TaskHandler for NPCSayTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let (npc_code, say_code) = match &ctx.task.details { TaskDetails::NPCSay { npc_code, say_code } => (npc_code.clone(), say_code.clone()), - _ => Err("Expected NPC say task to be NPCSay type")? + _ => Err("Expected NPC say task to be NPCSay type")?, }; let say_info = match npc_say_info_by_npc_code_say_code().get(&(&npc_code, &say_code)) { None => { - info!("NPCSayTaskHandler can't find NPCSayInfo for npc {} say_code {}", - npc_code, say_code); + info!( + "NPCSayTaskHandler can't find NPCSayInfo for npc {} say_code {}", + npc_code, say_code + ); return Ok(None); } - Some(r) => r + Some(r) => r, }; let npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { info!("NPCSayTaskHandler can't find NPC {}", npc_code); return Ok(None); } - Some(r) => r + Some(r) => r, }; if npc_item.death_data.is_some() { @@ -279,22 +304,34 @@ impl TaskHandler for NPCSayTaskHandler { let mut rng = thread_rng(); match l[..].choose(&mut rng) { None => { - info!("NPCSayTaskHandler NPCSayInfo for npc {} say_code {} has no choices", - npc_code, say_code); + info!( + "NPCSayTaskHandler NPCSayInfo for npc {} say_code {} has no choices", + npc_code, say_code + ); return Ok(None); } - Some(r) => r.clone() + Some(r) => r.clone(), } } }; - - match say_to_room(ctx.trans, &npc_item, &npc_item.location, say_what, is_explicit).await { + + match say_to_room( + ctx.trans, + &npc_item, + &npc_item.location, + say_what, + is_explicit, + ) + .await + { Ok(()) => {} Err(CommandHandlingError::UserError(e)) => { - info!("NPCSayHandler couldn't send for npc {} say_code {}: {}", - npc_code, say_code, e); + info!( + "NPCSayHandler couldn't send for npc {} say_code {}: {}", + npc_code, say_code, e + ); } - Err(CommandHandlingError::SystemError(e)) => Err(e)? + Err(CommandHandlingError::SystemError(e)) => Err(e)?, } Ok(None) } @@ -307,28 +344,34 @@ impl TaskHandler for NPCWanderTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let npc_code = match &ctx.task.details { TaskDetails::NPCWander { npc_code } => npc_code.clone(), - _ => Err("Expected NPCWander type")? + _ => Err("Expected NPCWander type")?, }; let npc = match npc_by_code().get(npc_code.as_str()) { None => { - info!("NPC {} is gone / not yet in static items, ignoring in wander handler", &npc_code); - return Ok(None) - }, - Some(r) => r + info!( + "NPC {} is gone / not yet in static items, ignoring in wander handler", + &npc_code + ); + return Ok(None); + } + Some(r) => r, }; let item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { - info!("NPC {} is gone / not yet in DB, ignoring in wander handler", &npc_code); - return Ok(None) - }, - Some(r) => r + info!( + "NPC {} is gone / not yet in DB, ignoring in wander handler", + &npc_code + ); + return Ok(None); + } + Some(r) => r, }; if item.death_data.is_some() { - return Ok(None) + return Ok(None); } let (ltype, lcode) = match item.location.split_once("/") { None => return Ok(None), - Some(r) => r + Some(r) => r, }; if ltype != "room" { let mut new_item = (*item).clone(); @@ -342,22 +385,31 @@ impl TaskHandler for NPCWanderTaskHandler { new_item.location = npc.spawn_location.to_owned(); ctx.trans.save_item_model(&new_item).await?; return Ok(None); - }, - Some(r) => r - }; - let ex_iter = room.exits - .iter() - .filter( - |ex| resolve_exit(room, ex).map( - |new_room| npc.wander_zones.contains(&new_room.zone) && - !new_room.repel_npc).unwrap_or(false) - ); - let dir_opt = ex_iter.choose(&mut thread_rng()).map(|ex| ex.direction.clone()).clone(); - if let Some(dir) = dir_opt { - match attempt_move_immediate(ctx.trans, &item, &dir, &mut None).await { - Ok(()) | Err(CommandHandlingError::UserError(_)) => {}, - Err(CommandHandlingError::SystemError(e)) => Err(e)? } + Some(r) => r, + }; + if !item.queue.is_empty() { + return Ok(None); + } + let ex_iter = room.exits.iter().filter(|ex| { + resolve_exit(room, ex) + .map(|new_room| npc.wander_zones.contains(&new_room.zone) && !new_room.repel_npc) + .unwrap_or(false) + }); + let dir_opt = ex_iter + .choose(&mut thread_rng()) + .map(|ex| ex.direction.clone()) + .clone(); + if let Some(dir) = dir_opt { + queue_command_for_npc_and_save( + &ctx.trans, + &item, + &QueueCommand::Movement { + direction: dir.clone(), + source: MovementSource::Command, + }, + ) + .await?; } Ok(None) } @@ -370,31 +422,43 @@ impl TaskHandler for NPCAggroTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let npc_code = match &ctx.task.details { TaskDetails::NPCAggro { npc_code } => npc_code.clone(), - _ => Err("Expected NPCAggro type")? + _ => Err("Expected NPCAggro type")?, }; let item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { None => { - info!("NPC {} is gone / not yet in DB, ignoring in aggro handler", &npc_code); - return Ok(None) - }, - Some(r) => r + info!( + "NPC {} is gone / not yet in DB, ignoring in aggro handler", + &npc_code + ); + return Ok(None); + } + Some(r) => r, }; - if item.death_data.is_some() || item.active_combat.as_ref().map(|ac| ac.attacking.is_some()).unwrap_or(false) { + if item.death_data.is_some() + || item + .active_combat + .as_ref() + .map(|ac| ac.attacking.is_some()) + .unwrap_or(false) + { return Ok(None); } let items_loc = ctx.trans.find_items_by_location(&item.location).await?; let vic_opt = items_loc .iter() - .filter(|it| (it.item_type == "player" || it.item_type == "npc") && - it.death_data.is_none() && (it.item_type != item.item_type || it.item_code != item.item_code)) + .filter(|it| { + (it.item_type == "player" || it.item_type == "npc") + && it.death_data.is_none() + && (it.item_type != item.item_type || it.item_code != item.item_code) + }) .choose(&mut thread_rng()); if let Some(victim) = vic_opt { match start_attack(ctx.trans, &item, victim).await { Ok(()) | Err(CommandHandlingError::UserError(_)) => {} - Err(CommandHandlingError::SystemError(e)) => Err(e)? + Err(CommandHandlingError::SystemError(e)) => Err(e)?, } } - + Ok(None) } } @@ -406,18 +470,18 @@ impl TaskHandler for NPCRecloneTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let npc_code = match &ctx.task.details { TaskDetails::RecloneNPC { npc_code } => npc_code.clone(), - _ => Err("Expected RecloneNPC type")? + _ => Err("Expected RecloneNPC type")?, }; let mut npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? { - None => { return Ok(None) }, - Some(r) => (*r).clone() + None => return Ok(None), + Some(r) => (*r).clone(), }; let npc = match npc_by_code().get(npc_code.as_str()) { - None => { return Ok(None) }, - Some(r) => r + None => return Ok(None), + Some(r) => r, }; - + if npc_item.death_data.is_none() { return Ok(None); } diff --git a/blastmud_game/src/static_content/npc/melbs_citizen.rs b/blastmud_game/src/static_content/npc/melbs_citizen.rs index 4ed41ca..8a7d08f 100644 --- a/blastmud_game/src/static_content/npc/melbs_citizen.rs +++ b/blastmud_game/src/static_content/npc/melbs_citizen.rs @@ -1,8 +1,5 @@ -use super::{NPC, NPCSayInfo, NPCSayType}; -use crate::models::{ - item::Pronouns, - consent::ConsentType, -}; +use super::{NPCSayInfo, NPCSayType, NPC}; +use crate::models::{consent::ConsentType, item::Pronouns}; pub fn npc_list() -> Vec { use NPCSayType::FromFixedList; @@ -20,7 +17,7 @@ pub fn npc_list() -> Vec { (false, "The damn vampire movement... they are all so sneaky, and I never know when they are going to come for my blood."), )) }; - + macro_rules! citizen { ($code: expr, $name: expr, $spawn: expr, $pronouns: expr) => { NPC { @@ -38,66 +35,341 @@ pub fn npc_list() -> Vec { } } - vec!( - citizen!("1", "Matthew Thomas", "kingst_latrobest", Pronouns::default_male()), - citizen!("2", "Matthew Perez", "kingst_20", Pronouns::default_male()), - citizen!("3", "Kimberly Jackson", "kingst_40", Pronouns::default_female()), - citizen!("4", "Michael Sanchez", "kingst_50", Pronouns::default_male()), - citizen!("5", "Jessica Davis", "kingst_bourkest", Pronouns::default_female()), - citizen!("6", "Robert Davis", "kingst_70", Pronouns::default_male()), - citizen!("7", "Paul Lewis", "kingst_90", Pronouns::default_male()), - citizen!("8", "Andrew Moore", "kingst_collinsst", Pronouns::default_male()), - citizen!("9", "Betty Thomas", "kingst_100", Pronouns::default_female()), - citizen!("10", "Mary Robinson", "kingst_110", Pronouns::default_female()), - citizen!("11", "Lisa Lopez", "kingst_flinderst", Pronouns::default_female()), - citizen!("12", "Kimberly Martinez", "flindersst_200", Pronouns::default_female()), - citizen!("13", "Anthony Nguyen", "flindersst_190", Pronouns::default_male()), - citizen!("14", "Joshua Green", "flindersst_180", Pronouns::default_male()), - citizen!("15", "Emily Wright", "flindersst_170", Pronouns::default_female()), - citizen!("16", "Ashley Thomas", "lonsdalest_130", Pronouns::default_male()), - citizen!("17", "Jessica Miller", "kingst_80", Pronouns::default_female()), - citizen!("18", "Anthony Lopez", "lonsdalest_140", Pronouns::default_male()), - citizen!("19", "John Lopez", "elizabethst_lonsdalest", Pronouns::default_male()), - citizen!("20", "Thomas Garcia", "williamsst_120", Pronouns::default_male()), - citizen!("21", "Donna Thompson", "elizabethst_60", Pronouns::default_female()), - citizen!("22", "Matthew Davis", "williamsst_100", Pronouns::default_male()), - citizen!("23", "Steven Jones", "swanstonst_120", Pronouns::default_male()), - citizen!("24", "Linda Smith", "swanstonst_lonsdalest", Pronouns::default_male()), - citizen!("25", "Karen Rodriguez", "bourkest_180", Pronouns::default_female()), - citizen!("26", "Paul Scott", "swanstonst_70", Pronouns::default_male()), - citizen!("27", "Ashley Thomas", "lonsdalest_130", Pronouns::default_male()), - citizen!("28", "Sandra Scott", "elizabethst_30", Pronouns::default_female()), - citizen!("29", "Michael Rodriguez", "swanstonst_70", Pronouns::default_male()), - citizen!("30", "Donald Miller", "elizabethst_30", Pronouns::default_male()), - citizen!("31", "Charles Moore", "lonsdalest_160", Pronouns::default_male()), - citizen!("32", "Ashley Sanchez", "kingst_100", Pronouns::default_male()), - citizen!("33", "Margaret Lewis", "flindersst_180", Pronouns::default_female()), - citizen!("34", "Sandra Thompson", "swanstonst_80", Pronouns::default_female()), - citizen!("35", "Sandra King", "lonsdalest_150", Pronouns::default_female()), - citizen!("36", "Lisa Anderson", "lonsdalest_210", Pronouns::default_female()), - citizen!("37", "Kimberly Martin", "kingst_80", Pronouns::default_female()), - citizen!("38", "Susan Smith", "latrobest_190", Pronouns::default_female()), - citizen!("39", "Susan Martin", "collinsst_150", Pronouns::default_female()), - citizen!("40", "Linda Scott", "williamsst_30", Pronouns::default_female()), - citizen!("41", "Donald Miller", "elizabethst_80", Pronouns::default_male()), - citizen!("42", "Mark Hill", "collinsst_120", Pronouns::default_male()), - citizen!("43", "William Perez", "queenst_90", Pronouns::default_male()), - citizen!("44", "Donald Perez", "queenst_lonsdalest", Pronouns::default_male()), - citizen!("45", "Lisa Rodriguez", "collinsst_100", Pronouns::default_female()), - citizen!("46", "James Adams", "latrobest_150", Pronouns::default_male()), - citizen!("47", "James Moore", "latrobest_130", Pronouns::default_male()), - citizen!("48", "Joseph Martin", "bourkest_150", Pronouns::default_male()), - citizen!("49", "Matthew Jones", "kingst_60", Pronouns::default_male()), - citizen!("50", "Michael Sanchez", "queenst_100", Pronouns::default_male()), - citizen!("51", "Donna Torres", "flindersst_150", Pronouns::default_female()), - citizen!("52", "Barbara Garcia", "swanstonst_50", Pronouns::default_female()), - citizen!("53", "Daniel Miller", "bourkest_110", Pronouns::default_male()), - citizen!("54", "Robert Young", "kingst_collinsst", Pronouns::default_male()), - citizen!("55", "Donald Flores", "swanstonst_40", Pronouns::default_male()), - citizen!("56", "Charles Thomas", "flindersst_110", Pronouns::default_male()), - citizen!("57", "William Torres", "swanstonst_60", Pronouns::default_male()), - citizen!("58", "Barbara Gonzalez", "collinsst_190", Pronouns::default_female()), - citizen!("59", "Mary Smith", "bourkest_180", Pronouns::default_female()), - citizen!("60", "Michael John", "williamsst_110", Pronouns::default_male()), - ) + vec![ + citizen!( + "1", + "Matthew Thomas", + "kingst_latrobest", + Pronouns::default_male() + ), + citizen!("2", "Matthew Perez", "kingst_20", Pronouns::default_male()), + citizen!( + "3", + "Kimberly Jackson", + "kingst_40", + Pronouns::default_female() + ), + citizen!( + "4", + "Michael Sanchez", + "kingst_50", + Pronouns::default_male() + ), + citizen!( + "5", + "Jessica Davis", + "kingst_bourkest", + Pronouns::default_female() + ), + citizen!("6", "Robert Davis", "kingst_70", Pronouns::default_male()), + citizen!("7", "Paul Lewis", "kingst_90", Pronouns::default_male()), + citizen!( + "8", + "Andrew Moore", + "kingst_collinsst", + Pronouns::default_male() + ), + citizen!( + "9", + "Betty Thomas", + "kingst_100", + Pronouns::default_female() + ), + citizen!( + "10", + "Mary Robinson", + "kingst_110", + Pronouns::default_female() + ), + citizen!( + "11", + "Lisa Lopez", + "kingst_flinderst", + Pronouns::default_female() + ), + citizen!( + "12", + "Kimberly Martinez", + "flindersst_200", + Pronouns::default_female() + ), + citizen!( + "13", + "Anthony Nguyen", + "flindersst_190", + Pronouns::default_male() + ), + citizen!( + "14", + "Joshua Green", + "flindersst_180", + Pronouns::default_male() + ), + citizen!( + "15", + "Emily Wright", + "flindersst_170", + Pronouns::default_female() + ), + citizen!( + "16", + "Ashley Thomas", + "lonsdalest_130", + Pronouns::default_male() + ), + citizen!( + "17", + "Jessica Miller", + "kingst_80", + Pronouns::default_female() + ), + citizen!( + "18", + "Anthony Lopez", + "lonsdalest_140", + Pronouns::default_male() + ), + citizen!( + "19", + "John Lopez", + "elizabethst_lonsdalest", + Pronouns::default_male() + ), + citizen!( + "20", + "Thomas Garcia", + "williamsst_120", + Pronouns::default_male() + ), + citizen!( + "21", + "Donna Thompson", + "elizabethst_60", + Pronouns::default_female() + ), + citizen!( + "22", + "Matthew Davis", + "williamsst_100", + Pronouns::default_male() + ), + citizen!( + "23", + "Steven Jones", + "swanstonst_120", + Pronouns::default_male() + ), + citizen!( + "24", + "Linda Smith", + "swanstonst_lonsdalest", + Pronouns::default_male() + ), + citizen!( + "25", + "Karen Rodriguez", + "bourkest_180", + Pronouns::default_female() + ), + citizen!( + "26", + "Paul Scott", + "swanstonst_70", + Pronouns::default_male() + ), + citizen!( + "27", + "Ashley Thomas", + "lonsdalest_130", + Pronouns::default_male() + ), + citizen!( + "28", + "Sandra Scott", + "elizabethst_30", + Pronouns::default_female() + ), + citizen!( + "29", + "Michael Rodriguez", + "swanstonst_70", + Pronouns::default_male() + ), + citizen!( + "30", + "Donald Miller", + "elizabethst_30", + Pronouns::default_male() + ), + citizen!( + "31", + "Charles Moore", + "lonsdalest_160", + Pronouns::default_male() + ), + citizen!( + "32", + "Ashley Sanchez", + "kingst_100", + Pronouns::default_male() + ), + citizen!( + "33", + "Margaret Lewis", + "flindersst_180", + Pronouns::default_female() + ), + citizen!( + "34", + "Sandra Thompson", + "swanstonst_80", + Pronouns::default_female() + ), + citizen!( + "35", + "Sandra King", + "lonsdalest_150", + Pronouns::default_female() + ), + citizen!( + "36", + "Lisa Anderson", + "lonsdalest_210", + Pronouns::default_female() + ), + citizen!( + "37", + "Kimberly Martin", + "kingst_80", + Pronouns::default_female() + ), + citizen!( + "38", + "Susan Smith", + "latrobest_190", + Pronouns::default_female() + ), + citizen!( + "39", + "Susan Martin", + "collinsst_150", + Pronouns::default_female() + ), + citizen!( + "40", + "Linda Scott", + "williamsst_30", + Pronouns::default_female() + ), + citizen!( + "41", + "Donald Miller", + "elizabethst_80", + Pronouns::default_male() + ), + citizen!("42", "Mark Hill", "collinsst_120", Pronouns::default_male()), + citizen!( + "43", + "William Perez", + "queenst_90", + Pronouns::default_male() + ), + citizen!( + "44", + "Donald Perez", + "queenst_lonsdalest", + Pronouns::default_male() + ), + citizen!( + "45", + "Lisa Rodriguez", + "collinsst_100", + Pronouns::default_female() + ), + citizen!( + "46", + "James Adams", + "latrobest_150", + Pronouns::default_male() + ), + citizen!( + "47", + "James Moore", + "latrobest_130", + Pronouns::default_male() + ), + citizen!( + "48", + "Joseph Martin", + "bourkest_150", + Pronouns::default_male() + ), + citizen!("49", "Matthew Jones", "kingst_60", Pronouns::default_male()), + citizen!( + "50", + "Michael Sanchez", + "queenst_100", + Pronouns::default_male() + ), + citizen!( + "51", + "Donna Torres", + "flindersst_150", + Pronouns::default_female() + ), + citizen!( + "52", + "Barbara Garcia", + "swanstonst_50", + Pronouns::default_female() + ), + citizen!( + "53", + "Daniel Miller", + "bourkest_110", + Pronouns::default_male() + ), + citizen!( + "54", + "Robert Young", + "kingst_collinsst", + Pronouns::default_male() + ), + citizen!( + "55", + "Donald Flores", + "swanstonst_40", + Pronouns::default_male() + ), + citizen!( + "56", + "Charles Thomas", + "flindersst_110", + Pronouns::default_male() + ), + citizen!( + "57", + "William Torres", + "swanstonst_60", + Pronouns::default_male() + ), + citizen!( + "58", + "Barbara Gonzalez", + "collinsst_190", + Pronouns::default_female() + ), + citizen!( + "59", + "Mary Smith", + "bourkest_180", + Pronouns::default_female() + ), + citizen!( + "60", + "Michael John", + "williamsst_110", + Pronouns::default_male() + ), + ] } diff --git a/blastmud_game/src/static_content/npc/melbs_dog.rs b/blastmud_game/src/static_content/npc/melbs_dog.rs index c0f409c..39f0337 100644 --- a/blastmud_game/src/static_content/npc/melbs_dog.rs +++ b/blastmud_game/src/static_content/npc/melbs_dog.rs @@ -1,12 +1,6 @@ -use super::{NPC, KillBonus}; -use crate::models::{ - item::Pronouns, - consent::ConsentType, -}; -use crate::static_content::{ - possession_type::PossessionType, - species::SpeciesType -}; +use super::{KillBonus, NPC}; +use crate::models::{consent::ConsentType, item::Pronouns}; +use crate::static_content::{possession_type::PossessionType, species::SpeciesType}; macro_rules! dog { ($code:expr, $adj:expr, $spawn: expr) => { @@ -32,66 +26,66 @@ macro_rules! dog { } pub fn npc_list() -> Vec { - vec!( - dog!("1", "smelly black", "melbs_williamsst_80"), - dog!("2", "howling black", "melbs_swanstonst_100"), - dog!("3", "smelly black", "melbs_collinsst_160"), - dog!("4", "growling light brown", "melbs_kingst_40"), - dog!("5", "ferocious white", "melbs_swanstonst_110"), - dog!("6", "mangy grey", "melbs_kingst_30"), - dog!("7", "reeking light brown", "melbs_flindersst_210"), - dog!("8", "feral brown", "melbs_elizabethst_40"), - dog!("9", "reeking grey", "melbs_collinsst_190"), - dog!("10", "ferocious grey", "melbs_kingst_60"), - dog!("11", "howling brown", "melbs_collinsst_140"), - dog!("12", "feral black", "melbs_flindersst_160"), - dog!("13", "smelly grey", "melbs_queenst_80"), - dog!("14", "howling grey", "melbs_kingst_70"), - dog!("15", "smelly grey", "melbs_flindersst_110"), - dog!("16", "feral black", "melbs_queenst_latrobest"), - dog!("17", "howling grey", "melbs_swanstonst_110"), - dog!("18", "mangy grey", "melbs_swanstonst_80"), - dog!("19", "reeking light brown", "melbs_latrobest_180"), - dog!("20", "smelly white", "melbs_flindersst_130"), - dog!("21", "reeking grey", "melbs_flindersst_180"), - dog!("22", "growling brown", "melbs_williamsst_80"), - dog!("23", "howling black", "melbs_lonsdalest_100"), - dog!("24", "growling grey", "melbs_latrobest_140"), - dog!("25", "howling light brown", "melbs_queenst_30"), - dog!("26", "howling black", "melbs_latrobest_160"), - dog!("27", "howling grey", "melbs_collinsst_170"), - dog!("28", "growling brown", "melbs_elizabethst_latrobest"), - dog!("29", "mangy brown", "melbs_kingst_70"), - dog!("30", "growling black", "melbs_swanstonst_120"), - dog!("31", "reeking light brown", "melbs_latrobest_130"), - dog!("32", "howling white", "melbs_bourkest_160"), - dog!("33", "growling black", "melbs_elizabethst_50"), - dog!("34", "mangy black", "melbs_swanstonst_110"), - dog!("35", "ferocious grey", "melbs_collinsst_100"), - dog!("36", "mangy grey", "melbs_flindersst_100"), - dog!("37", "growling brown", "melbs_swanstonst_flindersst"), - dog!("38", "mangy light brown", "melbs_lonsdalest_200"), - dog!("39", "howling light brown", "melbs_flindersst_210"), - dog!("40", "mangy light brown", "melbs_queenst_flindersst"), - dog!("41", "reeking white", "melbs_collinsst_130"), - dog!("42", "growling light brown", "melbs_lonsdalest_130"), - dog!("43", "reeking light brown", "melbs_elizabethst_70"), - dog!("44", "mangy brown", "melbs_swanstonst_30"), - dog!("45", "growling light brown", "melbs_swanstonst_lonsdalest"), - dog!("46", "smelly brown", "melbs_queenst_lonsdalest"), - dog!("47", "growling white", "melbs_elizabethst_bourkest"), - dog!("48", "feral brown", "melbs_collinsst_140"), - dog!("49", "ferocious black", "melbs_lonsdalest_150"), - dog!("50", "mangy grey", "melbs_kingst_collinsst"), - dog!("51", "ferocious brown", "melbs_kingst_120"), - dog!("52", "growling white", "melbs_elizabethst_10"), - dog!("53", "ferocious white", "melbs_lonsdalest_190"), - dog!("54", "smelly grey", "melbs_kingst_collinsst"), - dog!("55", "reeking light brown", "melbs_elizabethst_90"), - dog!("56", "reeking grey", "melbs_swanstonst_20"), - dog!("57", "feral brown", "melbs_flindersst_180"), - dog!("58", "reeking brown", "melbs_bourkest_130"), - dog!("59", "mangy light brown", "melbs_queenst_50"), - dog!("60", "growling white", "melbs_kingst_110"), - ) + vec![ + dog!("1", "smelly black", "melbs_williamsst_80"), + dog!("2", "howling black", "melbs_swanstonst_100"), + dog!("3", "smelly black", "melbs_collinsst_160"), + dog!("4", "growling light brown", "melbs_kingst_40"), + dog!("5", "ferocious white", "melbs_swanstonst_110"), + dog!("6", "mangy grey", "melbs_kingst_30"), + dog!("7", "reeking light brown", "melbs_flindersst_210"), + dog!("8", "feral brown", "melbs_elizabethst_40"), + dog!("9", "reeking grey", "melbs_collinsst_190"), + dog!("10", "ferocious grey", "melbs_kingst_60"), + dog!("11", "howling brown", "melbs_collinsst_140"), + dog!("12", "feral black", "melbs_flindersst_160"), + dog!("13", "smelly grey", "melbs_queenst_80"), + dog!("14", "howling grey", "melbs_kingst_70"), + dog!("15", "smelly grey", "melbs_flindersst_110"), + dog!("16", "feral black", "melbs_queenst_latrobest"), + dog!("17", "howling grey", "melbs_swanstonst_110"), + dog!("18", "mangy grey", "melbs_swanstonst_80"), + dog!("19", "reeking light brown", "melbs_latrobest_180"), + dog!("20", "smelly white", "melbs_flindersst_130"), + dog!("21", "reeking grey", "melbs_flindersst_180"), + dog!("22", "growling brown", "melbs_williamsst_80"), + dog!("23", "howling black", "melbs_lonsdalest_100"), + dog!("24", "growling grey", "melbs_latrobest_140"), + dog!("25", "howling light brown", "melbs_queenst_30"), + dog!("26", "howling black", "melbs_latrobest_160"), + dog!("27", "howling grey", "melbs_collinsst_170"), + dog!("28", "growling brown", "melbs_elizabethst_latrobest"), + dog!("29", "mangy brown", "melbs_kingst_70"), + dog!("30", "growling black", "melbs_swanstonst_120"), + dog!("31", "reeking light brown", "melbs_latrobest_130"), + dog!("32", "howling white", "melbs_bourkest_160"), + dog!("33", "growling black", "melbs_elizabethst_50"), + dog!("34", "mangy black", "melbs_swanstonst_110"), + dog!("35", "ferocious grey", "melbs_collinsst_100"), + dog!("36", "mangy grey", "melbs_flindersst_100"), + dog!("37", "growling brown", "melbs_swanstonst_flindersst"), + dog!("38", "mangy light brown", "melbs_lonsdalest_200"), + dog!("39", "howling light brown", "melbs_flindersst_210"), + dog!("40", "mangy light brown", "melbs_queenst_flindersst"), + dog!("41", "reeking white", "melbs_collinsst_130"), + dog!("42", "growling light brown", "melbs_lonsdalest_130"), + dog!("43", "reeking light brown", "melbs_elizabethst_70"), + dog!("44", "mangy brown", "melbs_swanstonst_30"), + dog!("45", "growling light brown", "melbs_swanstonst_lonsdalest"), + dog!("46", "smelly brown", "melbs_queenst_lonsdalest"), + dog!("47", "growling white", "melbs_elizabethst_bourkest"), + dog!("48", "feral brown", "melbs_collinsst_140"), + dog!("49", "ferocious black", "melbs_lonsdalest_150"), + dog!("50", "mangy grey", "melbs_kingst_collinsst"), + dog!("51", "ferocious brown", "melbs_kingst_120"), + dog!("52", "growling white", "melbs_elizabethst_10"), + dog!("53", "ferocious white", "melbs_lonsdalest_190"), + dog!("54", "smelly grey", "melbs_kingst_collinsst"), + dog!("55", "reeking light brown", "melbs_elizabethst_90"), + dog!("56", "reeking grey", "melbs_swanstonst_20"), + dog!("57", "feral brown", "melbs_flindersst_180"), + dog!("58", "reeking brown", "melbs_bourkest_130"), + dog!("59", "mangy light brown", "melbs_queenst_50"), + dog!("60", "growling white", "melbs_kingst_110"), + ] } diff --git a/blastmud_game/src/static_content/npc/statbot.rs b/blastmud_game/src/static_content/npc/statbot.rs index 921faa2..a11e872 100644 --- a/blastmud_game/src/static_content/npc/statbot.rs +++ b/blastmud_game/src/static_content/npc/statbot.rs @@ -1,19 +1,20 @@ +use super::super::room::{Exit, ExitBlocker}; use super::NPCMessageHandler; -use super::super::room::{ExitBlocker, Exit}; -use crate::message_handler::user_commands::{ - VerbContext, UResult, - get_user_or_fail, - get_user_or_fail_mut, - parsing::parse_to_space -}; -use async_trait::async_trait; -use crate::models::{ - item::{Item, Sex, Pronouns, StatType}, - user::{User}, - session::Session -}; use crate::services::skills::calculate_total_stats_skills_for_user; +use crate::{ + message_handler::user_commands::{ + get_user_or_fail, get_user_or_fail_mut, parsing::parse_to_space, CommandHandlingError, + UResult, VerbContext, + }, + models::{ + item::{Item, Pronouns, Sex, StatType}, + session::Session, + user::User, + }, + regular_tasks::queued_command::QueuedCommandContext, +}; use ansi::ansi; +use async_trait::async_trait; use nom::character::complete::u8; pub struct StatbotMessageHandler; @@ -29,24 +30,19 @@ pub enum StatbotState { FixTotals, AssignSex, SetDescription, - Done + Done, } async fn reply(ctx: &VerbContext<'_>, msg: &str) -> UResult<()> { - ctx.trans.queue_for_session( - ctx.session, - Some(&format!(ansi!("Statbot replies in a mechanical voice: \"{}\"\n"), - msg)) - ).await?; - Ok(()) -} - -async fn shout(ctx: &VerbContext<'_>, msg: &str) -> UResult<()> { - ctx.trans.queue_for_session( - ctx.session, - Some(&format!(ansi!("Statbot shouts in a stern mechanical voice: \"{}\"\n"), - msg)) - ).await?; + ctx.trans + .queue_for_session( + ctx.session, + Some(&format!( + ansi!("Statbot replies in a mechanical voice: \"{}\"\n"), + msg + )), + ) + .await?; Ok(()) } @@ -82,26 +78,67 @@ fn work_out_state(user: &User, item: &Item) -> StatbotState { } fn points_left(user: &User) -> f64 { - let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8.0); - let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8.0); + let brn = user + .raw_stats + .get(&StatType::Brains) + .cloned() + .unwrap_or(8.0); + let sen = user + .raw_stats + .get(&StatType::Senses) + .cloned() + .unwrap_or(8.0); let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8.0); - let refl = user.raw_stats.get(&StatType::Reflexes).cloned().unwrap_or(8.0); - let end = user.raw_stats.get(&StatType::Endurance).cloned().unwrap_or(8.0); + let refl = user + .raw_stats + .get(&StatType::Reflexes) + .cloned() + .unwrap_or(8.0); + let end = user + .raw_stats + .get(&StatType::Endurance) + .cloned() + .unwrap_or(8.0); let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8.0); (62 - (brn + sen + brw + refl + end + col) as i16).max(0) as f64 } fn next_action_text(session: &Session, user: &User, item: &Item) -> String { - let brn = user.raw_stats.get(&StatType::Brains).cloned().unwrap_or(8.0); - let sen = user.raw_stats.get(&StatType::Senses).cloned().unwrap_or(8.0); + let brn = user + .raw_stats + .get(&StatType::Brains) + .cloned() + .unwrap_or(8.0); + let sen = user + .raw_stats + .get(&StatType::Senses) + .cloned() + .unwrap_or(8.0); let brw = user.raw_stats.get(&StatType::Brawn).cloned().unwrap_or(8.0); - let refl = user.raw_stats.get(&StatType::Reflexes).cloned().unwrap_or(8.0); - let end = user.raw_stats.get(&StatType::Endurance).cloned().unwrap_or(8.0); + let refl = user + .raw_stats + .get(&StatType::Reflexes) + .cloned() + .unwrap_or(8.0); + let end = user + .raw_stats + .get(&StatType::Endurance) + .cloned() + .unwrap_or(8.0); let col = user.raw_stats.get(&StatType::Cool).cloned().unwrap_or(8.0); - let summary = format!("Brains: {}, Senses: {}, Brawn: {}, Reflexes: {}, Endurance: {}, Cool: {}. To spend: {}", brn, sen, brw, refl, end, col, points_left(user)); + let summary = format!( + "Brains: {}, Senses: {}, Brawn: {}, Reflexes: {}, Endurance: {}, Cool: {}. To spend: {}", + brn, + sen, + brw, + refl, + end, + col, + points_left(user) + ); let st = work_out_state(user, item); - + match st { StatbotState::Brains => ansi!( "The base body has 8 each of brains, senses, \ @@ -184,28 +221,53 @@ fn next_action_text(session: &Session, user: &User, item: &Item) -> String { } } -async fn stat_command(ctx: &mut VerbContext<'_>, item: &Item, - stat: &StatType, arg: &str) -> UResult<()> { +async fn stat_command( + ctx: &mut VerbContext<'_>, + item: &Item, + stat: &StatType, + arg: &str, +) -> UResult<()> { match u8::<&str, nom::error::Error<&str>>(arg) { - Err(_) => { reply(ctx, "I'll need a number after the stat name.").await?; } + Err(_) => { + reply(ctx, "I'll need a number after the stat name.").await?; + } Ok((rest, _)) if rest.trim() != "" => { - reply(ctx, "SYNTAX ERROR - who dares to put extra text after the stat number!").await?; + reply( + ctx, + "SYNTAX ERROR - who dares to put extra text after the stat number!", + ) + .await?; } Ok((_, statno)) if statno < 8 => { reply(ctx, "8 is the minimum, you can't go lower").await?; } Ok((_, statno)) if statno > 15 => { - reply(ctx, "15 is the maximum, you can't go higher even if you have points").await?; + reply( + ctx, + "15 is the maximum, you can't go higher even if you have points", + ) + .await?; } Ok((_, statno)) => { let points = { let user = get_user_or_fail(ctx)?; - points_left(get_user_or_fail(ctx)?) + (user.raw_stats.get(stat).cloned().unwrap_or(8.0) - 8.0) + points_left(get_user_or_fail(ctx)?) + + (user.raw_stats.get(stat).cloned().unwrap_or(8.0) - 8.0) }; if (statno as f64 - 8.0) > points { - reply(ctx, &if points == 0.0 { "You have no points left".to_owned() } else { - format!("You only have {} point{} left", points, if points == 1.0 { "" } else { "s" }) - }).await?; + reply( + ctx, + &if points == 0.0 { + "You have no points left".to_owned() + } else { + format!( + "You only have {} point{} left", + points, + if points == 1.0 { "" } else { "s" } + ) + }, + ) + .await?; return Ok(()); } { @@ -217,34 +279,45 @@ async fn stat_command(ctx: &mut VerbContext<'_>, item: &Item, let mut item_updated = item.clone(); item_updated.total_stats = user.raw_stats.clone(); ctx.trans.save_item_model(&item_updated).await?; - reply(ctx, &next_action_text(&ctx.session_dat, user, &item_updated)).await?; + reply( + ctx, + &next_action_text(&ctx.session_dat, user, &item_updated), + ) + .await?; } } Ok(()) } -async fn sex_command(ctx: &mut VerbContext<'_>, item: &Item, - arg: &str) -> UResult<()> { +async fn sex_command(ctx: &mut VerbContext<'_>, item: &Item, arg: &str) -> UResult<()> { let choice = match arg.trim().to_lowercase().as_str() { "male" | "man" => Sex::Male, "female" | "woman" => Sex::Female, _ => { - reply(ctx, "You want to be a what? The empire values all its subjects, \ + reply( + ctx, + "You want to be a what? The empire values all its subjects, \ but the body factory makes only male and female. Pick one \ - of those.").await?; + of those.", + ) + .await?; return Ok(()); } }; let mut item_updated = item.clone(); item_updated.pronouns = match choice { Sex::Male => Pronouns::default_male(), - Sex::Female => Pronouns::default_female() + Sex::Female => Pronouns::default_female(), }; item_updated.sex = Some(choice); let user: &User = get_user_or_fail(ctx)?; ctx.trans.save_item_model(&item_updated).await?; - reply(ctx, &next_action_text(&ctx.session_dat, user, &item_updated)).await?; + reply( + ctx, + &next_action_text(&ctx.session_dat, user, &item_updated), + ) + .await?; Ok(()) } @@ -255,19 +328,29 @@ impl NPCMessageHandler for StatbotMessageHandler { ctx: &mut VerbContext, source: &Item, _target: &Item, - message: &str + message: &str, ) -> UResult<()> { let (command, arg) = parse_to_space(message); match command.to_lowercase().as_str() { - "brains" | "brn" | "brain" => stat_command(ctx, source, &StatType::Brains, &arg).await?, - "senses" | "sen" | "sense" => stat_command(ctx, source, &StatType::Senses, &arg).await?, + "brains" | "brn" | "brain" => { + stat_command(ctx, source, &StatType::Brains, &arg).await? + } + "senses" | "sen" | "sense" => { + stat_command(ctx, source, &StatType::Senses, &arg).await? + } "brawn" | "brw" => stat_command(ctx, source, &StatType::Brawn, &arg).await?, - "reflexes" | "ref" | "reflex" => stat_command(ctx, source, &StatType::Reflexes, &arg).await?, + "reflexes" | "ref" | "reflex" => { + stat_command(ctx, source, &StatType::Reflexes, &arg).await? + } "endurance" | "end" => stat_command(ctx, source, &StatType::Endurance, &arg).await?, "cool" | "col" => stat_command(ctx, source, &StatType::Cool, &arg).await?, "sex" => sex_command(ctx, source, &arg).await?, _ => { - reply(ctx, &next_action_text(&ctx.session_dat, get_user_or_fail(ctx)?, source)).await?; + reply( + ctx, + &next_action_text(&ctx.session_dat, get_user_or_fail(ctx)?, source), + ) + .await?; } } Ok(()) @@ -280,17 +363,36 @@ impl ExitBlocker for ChoiceRoomBlocker { // True if they will be allowed to pass the exit, false otherwise. async fn attempt_exit( self: &Self, - ctx: &mut VerbContext, - player: &mut Item, - _exit: &Exit + ctx: &mut QueuedCommandContext, + _exit: &Exit, ) -> UResult { - let user = get_user_or_fail(ctx)?; - if work_out_state(user, player) == StatbotState::Done { - calculate_total_stats_skills_for_user(player, &user); + if ctx.item.item_type != "player" { + return Ok(false); + } + let user = ctx + .trans + .find_by_username(&ctx.item.item_code) + .await? + .ok_or_else(|| CommandHandlingError::UserError("No user exists".to_owned()))?; + if work_out_state(&user, ctx.item) == StatbotState::Done { + calculate_total_stats_skills_for_user(ctx.item, &user); Ok(true) } else { - shout(ctx, &format!(ansi!("YOU SHALL NOT PASS UNTIL YOU DO AS I SAY! {}"), - &next_action_text(&ctx.session_dat, user, player))).await?; + if let Some((sess, sess_dat)) = ctx + .trans + .find_session_for_player(&ctx.item.item_code) + .await? + { + ctx.trans + .queue_for_session( + &sess, + Some(&format!( + ansi!("Statbot shouts in a stern mechanical voice: \"YOU SHALL NOT PASS UNTIL YOU DO AS I SAY! {}\"\n"), + &next_action_text(&sess_dat, &user, ctx.item) + )), + ) + .await?; + } Ok(false) } } diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index acdc828..23bab13 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -2,6 +2,7 @@ use crate::{ message_handler::user_commands::{UResult, VerbContext}, models::consent::ConsentType, models::item::{Item, Pronouns, SkillType}, + regular_tasks::queued_command::QueuedCommandContext, static_content::{room::Direction, species::BodyPart}, }; use async_trait::async_trait; @@ -217,6 +218,11 @@ pub trait ArglessHandler { async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()>; } +#[async_trait] +pub trait LockcheckHandler { + async fn cmd(&self, ctx: &mut QueuedCommandContext, what: &Item) -> UResult<()>; +} + #[async_trait] pub trait InstallHandler { async fn install_cmd( @@ -250,7 +256,7 @@ pub struct PossessionData { pub becomes_on_spent: Option, pub weight: u64, pub install_handler: Option<&'static (dyn InstallHandler + Sync + Send)>, - pub lockcheck_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, + pub lockcheck_handler: Option<&'static (dyn LockcheckHandler + Sync + Send)>, pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, pub can_butcher: bool, diff --git a/blastmud_game/src/static_content/possession_type/lock.rs b/blastmud_game/src/static_content/possession_type/lock.rs index 6b22ceb..93a9045 100644 --- a/blastmud_game/src/static_content/possession_type/lock.rs +++ b/blastmud_game/src/static_content/possession_type/lock.rs @@ -1,7 +1,8 @@ -use super::{ArglessHandler, PossessionData, PossessionType}; +use super::{LockcheckHandler, PossessionData, PossessionType}; use crate::{ message_handler::user_commands::{user_error, UResult, VerbContext}, models::item::{Item, LocationActionType}, + regular_tasks::queued_command::QueuedCommandContext, services::{ capacity::{check_item_capacity, CapacityLevel}, comms::broadcast_to_room, @@ -13,11 +14,13 @@ use once_cell::sync::OnceCell; struct ScanLockLockcheck; #[async_trait] -impl ArglessHandler for ScanLockLockcheck { - async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()> { - if what.owner == Some(player.refstr()) { - ctx.trans.queue_for_session(&ctx.session, Some( - "The scanlock in the door emits a single high-pitched beep as it unlocks.\n")).await?; +impl LockcheckHandler for ScanLockLockcheck { + async fn cmd(&self, ctx: &mut QueuedCommandContext, what: &Item) -> UResult<()> { + if what.owner == Some(ctx.item.refstr()) { + if let Some((session, _)) = ctx.get_session().await? { + ctx.trans.queue_for_session(&session, Some( + "The scanlock in the door emits a single high-pitched beep as it unlocks.\n")).await?; + } } else { user_error( "The scanlock in the door emits a medium-pitched tone followed by a low tone, and remains locked.".to_owned())?; diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index 8ed4552..61305da 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -1,23 +1,19 @@ -use super::{ - StaticItem, - possession_type::PossessionType, -}; -use once_cell::sync::OnceCell; -use std::collections::BTreeMap; -use async_trait::async_trait; -use serde::{Serialize, Deserialize}; -use crate::message_handler::user_commands::{ - UResult, VerbContext -}; +use super::{possession_type::PossessionType, StaticItem}; use crate::{ - models::item::{Item, ItemFlag} + message_handler::user_commands::UResult, + models::item::{Item, ItemFlag}, + regular_tasks::queued_command::QueuedCommandContext, }; +use async_trait::async_trait; +use once_cell::sync::OnceCell; +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; -mod special; -mod repro_xv; -mod melbs; -mod cok_murl; mod chonkers; +mod cok_murl; +mod melbs; +mod repro_xv; +mod special; pub struct Zone { pub code: &'static str, @@ -27,47 +23,95 @@ pub struct Zone { static STATIC_ZONE_DETAILS: OnceCell> = OnceCell::new(); pub fn zone_details() -> &'static BTreeMap<&'static str, Zone> { - STATIC_ZONE_DETAILS.get_or_init( - || vec!( - Zone { code: "special", - display: "Outside of Time", - outdoors: false }, - Zone { code: "melbs", - display: "Melbs", - outdoors: true }, - Zone { code: "repro_xv", - display: "Reprolabs XV", - outdoors: false }, - Zone { code: "cok_murl", - display: "CoK-Murlison Complex", - outdoors: false }, - Zone { code: "chonkers", - display: "Chonker's Gym", - outdoors: false }, - ).into_iter().map(|x|(x.code, x)).collect()) + STATIC_ZONE_DETAILS.get_or_init(|| { + vec![ + Zone { + code: "special", + display: "Outside of Time", + outdoors: false, + }, + Zone { + code: "melbs", + display: "Melbs", + outdoors: true, + }, + Zone { + code: "repro_xv", + display: "Reprolabs XV", + outdoors: false, + }, + Zone { + code: "cok_murl", + display: "CoK-Murlison Complex", + outdoors: false, + }, + Zone { + code: "chonkers", + display: "Chonker's Gym", + outdoors: false, + }, + ] + .into_iter() + .map(|x| (x.code, x)) + .collect() + }) } #[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Debug)] pub struct GridCoords { pub x: i64, pub y: i64, - pub z: i64 + pub z: i64, } impl GridCoords { pub fn apply(self: &GridCoords, dir: &Direction) -> GridCoords { match dir { - Direction::NORTH => GridCoords {y: self.y - 1, ..*self}, - Direction::SOUTH => GridCoords {y: self.y + 1, ..*self}, - Direction::EAST => GridCoords {x: self.x + 1, ..*self}, - Direction::WEST => GridCoords {x: self.x - 1, ..*self}, - Direction::NORTHEAST => GridCoords {x: self.x + 1, y: self.y - 1, ..*self}, - Direction::SOUTHEAST => GridCoords {x: self.x + 1, y: self.y + 1, ..*self}, - Direction::NORTHWEST => GridCoords {x: self.x - 1, y: self.y - 1, ..*self}, - Direction::SOUTHWEST => GridCoords {x: self.x - 1, y: self.y + 1, ..*self}, - Direction::UP => GridCoords {z: self.z + 1, ..*self}, - Direction::DOWN => GridCoords {z: self.z - 1, ..*self}, - Direction::IN { .. } => self.clone() + Direction::NORTH => GridCoords { + y: self.y - 1, + ..*self + }, + Direction::SOUTH => GridCoords { + y: self.y + 1, + ..*self + }, + Direction::EAST => GridCoords { + x: self.x + 1, + ..*self + }, + Direction::WEST => GridCoords { + x: self.x - 1, + ..*self + }, + Direction::NORTHEAST => GridCoords { + x: self.x + 1, + y: self.y - 1, + ..*self + }, + Direction::SOUTHEAST => GridCoords { + x: self.x + 1, + y: self.y + 1, + ..*self + }, + Direction::NORTHWEST => GridCoords { + x: self.x - 1, + y: self.y - 1, + ..*self + }, + Direction::SOUTHWEST => GridCoords { + x: self.x - 1, + y: self.y + 1, + ..*self + }, + Direction::UP => GridCoords { + z: self.z + 1, + ..*self + }, + Direction::DOWN => GridCoords { + z: self.z - 1, + ..*self + }, + Direction::IN { .. } => self.clone(), } } } @@ -77,9 +121,8 @@ pub trait ExitBlocker { // True if they will be allowed to pass the exit, false otherwise. async fn attempt_exit( self: &Self, - ctx: &mut VerbContext, - player: &mut Item, - exit: &Exit + ctx: &mut QueuedCommandContext, + exit: &Exit, ) -> UResult; } @@ -101,21 +144,24 @@ pub enum Direction { SOUTHWEST, UP, DOWN, - IN { item: String } + IN { item: String }, } impl Serialize for Direction { fn serialize(&self, serializer: S) -> Result - where S: serde::Serializer { + where + S: serde::Serializer, + { Serialize::serialize(&self.describe(), serializer) } } -impl <'de> Deserialize<'de> for Direction { +impl<'de> Deserialize<'de> for Direction { fn deserialize(deserializer: D) -> Result - where D: serde::Deserializer<'de> { - Deserialize::deserialize(deserializer) - .and_then(|s: String| Self::parse(s.as_str()) - .ok_or( - serde::de::Error::custom("Invalid direction"))) + where + D: serde::Deserializer<'de>, + { + Deserialize::deserialize(deserializer).and_then(|s: String| { + Self::parse(s.as_str()).ok_or(serde::de::Error::custom("Invalid direction")) + }) } } @@ -132,7 +178,7 @@ impl Direction { Direction::SOUTHWEST => "southwest".to_owned(), Direction::UP => "up".to_owned(), Direction::DOWN => "down".to_owned(), - Direction::IN { item } => "in ".to_owned() + item + Direction::IN { item } => "in ".to_owned() + item, } } @@ -148,13 +194,15 @@ impl Direction { Direction::SOUTHWEST => format!("{} to the southwest", climb_dir), Direction::UP => "upwards".to_owned(), Direction::DOWN => "downwards".to_owned(), - Direction::IN { item } => format!("{} and in ", item) + Direction::IN { item } => format!("{} and in ", item), } } - + pub fn parse(input: &str) -> Option { if input.starts_with("in ") { - return Some(Direction::IN { item: input["in ".len()..].trim().to_owned() }) + return Some(Direction::IN { + item: input["in ".len()..].trim().to_owned(), + }); } match input { "north" | "n" => Some(Direction::NORTH), @@ -167,7 +215,7 @@ impl Direction { "southwest" | "sw" => Some(Direction::SOUTHWEST), "up" => Some(Direction::UP), "down" => Some(Direction::DOWN), - _ => None + _ => None, } } @@ -183,15 +231,15 @@ impl Direction { Direction::SOUTHWEST => Some(Direction::NORTHEAST), Direction::UP => Some(Direction::DOWN), Direction::DOWN => Some(Direction::UP), - Direction::IN { .. } => None + Direction::IN { .. } => None, } } } -#[derive(Eq,Ord,Debug,PartialEq,PartialOrd,Clone)] +#[derive(Eq, Ord, Debug, PartialEq, PartialOrd, Clone)] pub enum ExitTarget { UseGPS, - Custom(&'static str) + Custom(&'static str), } pub struct ExitClimb { @@ -222,19 +270,19 @@ pub struct SecondaryZoneRecord { pub zone: &'static str, pub short: &'static str, pub grid_coords: GridCoords, - pub caption: Option<&'static str> + pub caption: Option<&'static str>, } pub struct RoomStock { pub possession_type: PossessionType, - pub list_price: u64 + pub list_price: u64, } impl Default for RoomStock { fn default() -> Self { Self { possession_type: PossessionType::AntennaWhip, - list_price: 1000000000 + list_price: 1000000000, } } } @@ -251,7 +299,7 @@ pub enum MaterialType { Normal, WaterSurface, Underwater, - Soft { damage_modifier: f64 } + Soft { damage_modifier: f64 }, } pub struct Room { @@ -272,62 +320,64 @@ pub struct Room { pub stock_list: Vec, // What can be rented here... pub rentable_dynzone: Vec, - pub material_type: MaterialType + pub material_type: MaterialType, } impl Default for Room { fn default() -> Self { Self { zone: "default", - secondary_zones: vec!(), + secondary_zones: vec![], code: "default", name: "default", short: "DF", grid_coords: GridCoords { x: 0, y: 0, z: 0 }, description: "default", description_less_explicit: None, - exits: vec!(), + exits: vec![], should_caption: true, repel_npc: false, - item_flags: vec!(), - stock_list: vec!(), - rentable_dynzone: vec!(), + item_flags: vec![], + stock_list: vec![], + rentable_dynzone: vec![], material_type: MaterialType::Normal, } } - } static STATIC_ROOM_LIST: OnceCell> = OnceCell::new(); pub fn room_list() -> &'static Vec { - STATIC_ROOM_LIST.get_or_init( - || { - let mut rooms = repro_xv::room_list(); - rooms.append(&mut melbs::room_list()); - rooms.append(&mut cok_murl::room_list()); - rooms.append(&mut chonkers::room_list()); - rooms.append(&mut special::room_list()); - rooms.into_iter().collect() + STATIC_ROOM_LIST.get_or_init(|| { + let mut rooms = repro_xv::room_list(); + rooms.append(&mut melbs::room_list()); + rooms.append(&mut cok_murl::room_list()); + rooms.append(&mut chonkers::room_list()); + rooms.append(&mut special::room_list()); + rooms.into_iter().collect() }) } static STATIC_ROOM_MAP_BY_CODE: OnceCell> = OnceCell::new(); pub fn room_map_by_code() -> &'static BTreeMap<&'static str, &'static Room> { - STATIC_ROOM_MAP_BY_CODE.get_or_init( - || room_list().iter().map(|r| (r.code, r)).collect()) + STATIC_ROOM_MAP_BY_CODE.get_or_init(|| room_list().iter().map(|r| (r.code, r)).collect()) } -static STATIC_ROOM_MAP_BY_ZLOC: OnceCell> = OnceCell::new(); +static STATIC_ROOM_MAP_BY_ZLOC: OnceCell< + BTreeMap<(&'static str, &'static GridCoords), &'static Room>, +> = OnceCell::new(); pub fn room_map_by_zloc() -> &'static BTreeMap<(&'static str, &'static GridCoords), &'static Room> { - STATIC_ROOM_MAP_BY_ZLOC.get_or_init( - || room_list().iter() + STATIC_ROOM_MAP_BY_ZLOC.get_or_init(|| { + room_list() + .iter() .map(|r| ((r.zone, &r.grid_coords), r)) - .chain(room_list().iter() - .flat_map(|r| r.secondary_zones.iter() - .map(|sz| ((sz.zone, &sz.grid_coords), r)) - .collect::>())) - .collect()) + .chain(room_list().iter().flat_map(|r| { + r.secondary_zones + .iter() + .map(|sz| ((sz.zone, &sz.grid_coords), r)) + .collect::>() + })) + .collect() + }) } pub fn room_static_items() -> Box> { @@ -338,69 +388,74 @@ pub fn room_static_items() -> Box> { item_type: "room".to_owned(), display: r.name.to_owned(), details: Some(r.description.to_owned()), - details_less_explicit: r.description_less_explicit.map(|d|d.to_owned()), + details_less_explicit: r.description_less_explicit.map(|d| d.to_owned()), location: format!("zone/{}", r.zone), is_static: true, flags: r.item_flags.clone(), ..Item::default() - }) + }), })) } pub fn resolve_exit(room: &Room, exit: &Exit) -> Option<&'static Room> { match exit.target { - ExitTarget::Custom(t) => t.split_once("/").and_then( - |(t,c)| + ExitTarget::Custom(t) => t.split_once("/").and_then(|(t, c)| { if t != "room" { None } else { - room_map_by_code().get(c).map(|r|*r) - }), - ExitTarget::UseGPS => - room_map_by_zloc().get(&(room.zone, &room.grid_coords.apply(&exit.direction))).map(|r|*r) + room_map_by_code().get(c).map(|r| *r) + } + }), + ExitTarget::UseGPS => room_map_by_zloc() + .get(&(room.zone, &room.grid_coords.apply(&exit.direction))) + .map(|r| *r), } } #[cfg(test)] mod test { - use itertools::Itertools; use super::*; + use itertools::Itertools; #[test] fn room_zones_should_exist() { for room in room_list() { - zone_details().get(room.zone).expect( - &format!("zone {} for room {} should exist", room.zone, room.code)); + zone_details().get(room.zone).expect(&format!( + "zone {} for room {} should exist", + room.zone, room.code + )); } } #[test] fn room_map_by_code_should_have_repro_xv_chargen() { - assert_eq!(room_map_by_code().get("repro_xv_chargen").expect("repro_xv_chargen to exist").code, - "repro_xv_chargen"); + assert_eq!( + room_map_by_code() + .get("repro_xv_chargen") + .expect("repro_xv_chargen to exist") + .code, + "repro_xv_chargen" + ); } #[test] fn grid_coords_should_be_unique_in_zone() { let mut roomlist: Vec<&'static Room> = room_list().iter().collect(); - roomlist.sort_unstable_by( - |a,b| - a.grid_coords.cmp(&b.grid_coords) - .then(a.zone.cmp(&b.zone))); - let dups : Vec> = - roomlist.iter() + roomlist + .sort_unstable_by(|a, b| a.grid_coords.cmp(&b.grid_coords).then(a.zone.cmp(&b.zone))); + let dups: Vec> = roomlist + .iter() .group_by(|x| (&x.grid_coords, x.zone)) .into_iter() - .map(|((coord, zone), rg)| - rg.map(|r| (r.name, coord, zone)) - .collect::>()) + .map(|((coord, zone), rg)| { + rg.map(|r| (r.name, coord, zone)) + .collect::>() + }) .filter(|x| x.len() > 1) .collect(); - assert_eq!(dups, - Vec::>::new()); + assert_eq!(dups, Vec::>::new()); } - #[test] fn parse_direction_should_work() { assert_eq!(Direction::parse("southeast"), Some(Direction::SOUTHEAST)); @@ -408,7 +463,7 @@ mod test { #[test] fn parse_and_describe_direction_should_roundtrip() { - let examples = vec!( + let examples = vec![ "north", "south", "east", @@ -419,11 +474,13 @@ mod test { "southwest", "up", "down", - "in there" - ); + "in there", + ]; for example in examples { - assert_eq!(Direction::parse(example).map(|v| v.describe()), Some(example.to_owned())); + assert_eq!( + Direction::parse(example).map(|v| v.describe()), + Some(example.to_owned()) + ); } } - } diff --git a/blastmud_game/src/static_content/room/chonkers.rs b/blastmud_game/src/static_content/room/chonkers.rs index 4f3e5ff..379614d 100644 --- a/blastmud_game/src/static_content/room/chonkers.rs +++ b/blastmud_game/src/static_content/room/chonkers.rs @@ -1,6 +1,5 @@ use super::{ - Room, GridCoords, Exit, Direction, ExitTarget, MaterialType, - SecondaryZoneRecord, ExitClimb + Direction, Exit, ExitClimb, ExitTarget, GridCoords, MaterialType, Room, SecondaryZoneRecord, }; use ansi::ansi; pub fn room_list() -> Vec { diff --git a/blastmud_game/src/static_content/room/cok_murl.rs b/blastmud_game/src/static_content/room/cok_murl.rs index 159ca6a..f839f6a 100644 --- a/blastmud_game/src/static_content/room/cok_murl.rs +++ b/blastmud_game/src/static_content/room/cok_murl.rs @@ -1,7 +1,4 @@ -use super::{ - Room, GridCoords, Exit, Direction, ExitTarget, - SecondaryZoneRecord, RentInfo, -}; +use super::{Direction, Exit, ExitTarget, GridCoords, RentInfo, Room, SecondaryZoneRecord}; use crate::static_content::dynzone::DynzoneType; use ansi::ansi; pub fn room_list() -> Vec { @@ -97,12 +94,12 @@ pub fn room_list() -> Vec { grid_coords: GridCoords { x: 1, y: 1, z: 0 }, exits: vec!( Exit { - direction: Direction::NORTH, + direction: Direction::NORTH, ..Default::default() }, ), should_caption: true, ..Default::default() - }, + }, ) } diff --git a/blastmud_game/src/static_content/room/melbs.rs b/blastmud_game/src/static_content/room/melbs.rs index c09ddff..d53c779 100644 --- a/blastmud_game/src/static_content/room/melbs.rs +++ b/blastmud_game/src/static_content/room/melbs.rs @@ -1,11 +1,7 @@ use super::{ - Room, RoomStock, GridCoords, Exit, Direction, ExitTarget, ExitClimb, - SecondaryZoneRecord -}; -use crate::{ - models::item::ItemFlag, - static_content::possession_type::PossessionType + Direction, Exit, ExitClimb, ExitTarget, GridCoords, Room, RoomStock, SecondaryZoneRecord, }; +use crate::{models::item::ItemFlag, static_content::possession_type::PossessionType}; use ansi::ansi; pub fn room_list() -> Vec { @@ -29,7 +25,7 @@ pub fn room_list() -> Vec { ..Default::default() }, ), - should_caption: false, + should_caption: false, ..Default::default() }, Room { @@ -150,7 +146,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -171,7 +167,7 @@ pub fn room_list() -> Vec { item_flags: vec!(ItemFlag::NoSay, ItemFlag::NoSeeContents), repel_npc: true, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!( @@ -206,7 +202,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -949,7 +945,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -972,7 +968,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -1996,7 +1992,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -2019,7 +2015,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -2836,7 +2832,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -2859,7 +2855,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -3458,7 +3454,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -3481,7 +3477,7 @@ pub fn room_list() -> Vec { ), should_caption: false, ..Default::default() - }, + }, Room { zone: "melbs", secondary_zones: vec!(), diff --git a/blastmud_game/src/static_content/room/repro_xv.rs b/blastmud_game/src/static_content/room/repro_xv.rs index f74169b..609b14c 100644 --- a/blastmud_game/src/static_content/room/repro_xv.rs +++ b/blastmud_game/src/static_content/room/repro_xv.rs @@ -1,7 +1,4 @@ -use super::{ - Room, GridCoords, Exit, Direction, ExitTarget, ExitType, - SecondaryZoneRecord -}; +use super::{Direction, Exit, ExitTarget, ExitType, GridCoords, Room, SecondaryZoneRecord}; use crate::static_content::npc; use ansi::ansi; diff --git a/blastmud_game/src/static_content/room/special.rs b/blastmud_game/src/static_content/room/special.rs index b1ef499..8d90bf7 100644 --- a/blastmud_game/src/static_content/room/special.rs +++ b/blastmud_game/src/static_content/room/special.rs @@ -1,12 +1,10 @@ -use super::{ - Room, GridCoords, -}; +use super::{GridCoords, Room}; use ansi::ansi; // None of these are reachable except when the game or an admin puts something there. pub fn room_list() -> Vec { let holding_desc: &'static str = "The inside of a small pen or cage, with thick steel bars, suitable for holding an animal - or a person - securely, with no chance of escape. It is dimly lit and smells like urine, and is very cramped and indignifying. It looks like the only way out would be with the help of whoever locked you in here. [OOC: consider emailing staff@blastmud.org to discuss your situation]"; - vec!( + vec![ Room { zone: "special", code: "valhalla", @@ -15,7 +13,7 @@ pub fn room_list() -> Vec { description: "Where the valiant dead NPCs go to wait recloning", description_less_explicit: None, grid_coords: GridCoords { x: 0, y: 0, z: 0 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -26,7 +24,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 0, y: 0, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -37,7 +35,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 0, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -48,7 +46,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 2, y: 0, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -59,7 +57,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 3, y: 0, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -70,7 +68,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 0, y: 1, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -81,7 +79,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 1, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -92,7 +90,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 2, y: 1, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -103,7 +101,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 3, y: 1, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -114,7 +112,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 0, y: 2, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -125,7 +123,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 1, y: 2, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -136,7 +134,7 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 2, y: 2, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, Room { @@ -147,8 +145,8 @@ pub fn room_list() -> Vec { description: holding_desc, description_less_explicit: None, grid_coords: GridCoords { x: 3, y: 2, z: -1 }, - exits: vec!(), + exits: vec![], ..Default::default() }, - ) + ] } diff --git a/queued_command.rs b/queued_command.rs new file mode 100644 index 0000000..e69de29