diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 89837c95..ee940a64 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -739,7 +739,7 @@ impl DBTrans { self.pg_trans()?.execute("UPDATE items SET details=\ JSONB_SET(details, '{action_type}', $1) \ WHERE details->>'location' = $2 AND \ - details->>'action_type' = $3::JSONB::TEXT", + (details->'action_type')::TEXT = $3::JSONB::TEXT", &[&serde_json::to_value(other_item_action_type)?, &item.location, &serde_json::to_value(new_action_type)? @@ -759,7 +759,7 @@ impl DBTrans { if let Some(item) = self.pg_trans()?.query_opt( "SELECT details FROM items WHERE \ details->>'location' = $1 AND \ - details->>'action_type' = $2::JSONB::TEXT", + ((details->'action_type')::TEXT = $2::JSONB::TEXT)", &[&location, &serde_json::to_value(action_type)?]).await? { return Ok(Some(Arc::new(serde_json::from_value::(item.get("details"))?))); diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index f5fe5dbb..8f613b8d 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -23,6 +23,7 @@ pub mod get; mod describe; mod help; mod ignore; +mod install; mod inventory; mod less_explicit_mode; mod list; @@ -40,6 +41,7 @@ pub mod say; mod score; mod sign; mod status; +mod uninstall; pub mod use_cmd; mod vacate; mod whisper; @@ -130,6 +132,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "corp" => corp::VERB, "drop" => drop::VERB, "get" => get::VERB, + "install" => install::VERB, "inventory" => inventory::VERB, "inv" => inventory::VERB, "i" => inventory::VERB, @@ -170,7 +173,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "st" => status::VERB, "stat" => status::VERB, "status" => status::VERB, - + + "uninstall" => uninstall::VERB, "use" => use_cmd::VERB, "vacate" => vacate::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/close.rs b/blastmud_game/src/message_handler/user_commands/close.rs index 7bd7a7d5..472f22eb 100644 --- a/blastmud_game/src/message_handler/user_commands/close.rs +++ b/blastmud_game/src/message_handler/user_commands/close.rs @@ -101,6 +101,9 @@ impl QueueCommandHandler for QueueHandler { ) ).await?; } + + ctx.trans.delete_task("SwingShut", + &format!("{}/{}", &room_1.refstr(), &dir_in_room.describe())).await?; Ok(()) } diff --git a/blastmud_game/src/message_handler/user_commands/get.rs b/blastmud_game/src/message_handler/user_commands/get.rs index c6c56c32..c8383d78 100644 --- a/blastmud_game/src/message_handler/user_commands/get.rs +++ b/blastmud_game/src/message_handler/user_commands/get.rs @@ -136,12 +136,18 @@ impl UserVerb for Verb { if player_item.is_dead { user_error("You try to get it, but your ghostly hands slip through it uselessly".to_owned())?; } - for target in targets { + + let mut did_anything: bool = false; + for target in targets.iter().filter(|t| t.action_type.is_visible_in_look()) { if target.item_type != "possession" { user_error("You can't get that!".to_owned())?; } + did_anything = true; queue_command(ctx, &QueueCommand::Get { possession_id: target.item_code.clone() }).await?; } + if !did_anything { + user_error("I didn't find anything matching.".to_owned())?; + } Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/install.rs b/blastmud_game/src/message_handler/user_commands/install.rs new file mode 100644 index 00000000..a2c5b966 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/install.rs @@ -0,0 +1,66 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult, + UserError, + user_error, get_player_item_or_fail, search_item_for_user}; +use crate::{ + db::ItemSearchParams, + static_content::{ + possession_type::possession_data, + room::Direction, + }, + models::item::ItemFlag, +}; +use async_trait::async_trait; +use ansi::ansi; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + let (install_what_raw, what_dir_raw) = match remaining.rsplit_once(" on door to ") { + None => user_error(ansi!("Install where? Try install lock> on door to direction>").to_owned())?, + Some(v) => v + }; + let player_item = get_player_item_or_fail(ctx).await?; + if player_item.is_dead { + user_error("Apparently, you have to be alive to work as an installer.\ + So discriminatory!".to_owned())?; + } + let item = search_item_for_user(ctx, &ItemSearchParams { + include_contents: true, + ..ItemSearchParams::base(&player_item, install_what_raw.trim()) + }).await?; + if item.item_type != "possession" { + user_error("You can't install that!".to_owned())?; + } + + let handler = match item.possession_type.as_ref() + .and_then(|pt| possession_data().get(pt)) + .and_then(|pd| pd.install_handler) { + None => user_error("You can't install that!".to_owned())?, + Some(h) => h + }; + + let (loc_t, loc_c) = player_item.location.split_once("/") + .ok_or_else(|| UserError("Invalid current location".to_owned()))?; + let loc_item = ctx.trans.find_item_by_type_code(loc_t, loc_c).await? + .ok_or_else(|| UserError("Can't find your location".to_owned()))?; + if loc_item.owner.as_ref() != Some(&player_item.refstr()) || !loc_item.flags.contains(&ItemFlag::PrivatePlace) { + user_error("You can only install things while standing in a private room you own. \ + If you are outside, try installing from the inside." + .to_owned())?; + } + + let dir = Direction::parse(what_dir_raw.trim()).ok_or_else( + || UserError("Invalid direction.".to_owned()))?; + + loc_item.door_states.as_ref().and_then(|ds| ds.get(&dir)).ok_or_else( + || UserError("No door to that direction in this room - are you on the wrong side?".to_owned()) + )?; + + handler.install_cmd(ctx, &player_item, &item, &loc_item, &dir).await?; + + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs index 40a2f69e..c3e8abf8 100644 --- a/blastmud_game/src/message_handler/user_commands/look.rs +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -193,8 +193,7 @@ async fn describe_door( &LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await? { let lock_desc = lock.display_for_session(&ctx.session_dat); - msg.push_str(&format!(" The door is locked with {} {}", - &language::indefinite_article(&lock_desc), + msg.push_str(&format!(" The door is locked with {}", &lock_desc )); } @@ -217,6 +216,7 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe let all_groups: Vec>> = items .iter() + .filter(|i| i.action_type.is_visible_in_look()) .group_by(|i| i.display_for_sentence(true, 1, false)) .into_iter() .map(|(_, g)|g.collect::>>()) diff --git a/blastmud_game/src/message_handler/user_commands/uninstall.rs b/blastmud_game/src/message_handler/user_commands/uninstall.rs new file mode 100644 index 00000000..a7669f6b --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/uninstall.rs @@ -0,0 +1,67 @@ +use super::{VerbContext, UserVerb, UserVerbRef, UResult, + UserError, + user_error, get_player_item_or_fail, search_items_for_user}; +use crate::{ + db::ItemSearchParams, + static_content::{ + possession_type::possession_data, + room::Direction, + }, + models::item::ItemFlag, +}; +use async_trait::async_trait; +use ansi::ansi; + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { + let (uninstall_what_raw, what_dir_raw) = match remaining.rsplit_once(" from door to ") { + None => user_error(ansi!("Uninstall from where? Try uninstall lock> from door to direction>").to_owned())?, + Some(v) => v + }; + let player_item = get_player_item_or_fail(ctx).await?; + if player_item.is_dead { + user_error("Apparently, you have to be alive to work as an uninstaller. \ + So discriminatory!".to_owned())?; + } + + let (loc_t, loc_c) = player_item.location.split_once("/") + .ok_or_else(|| UserError("Invalid current location".to_owned()))?; + let loc_item = ctx.trans.find_item_by_type_code(loc_t, loc_c).await? + .ok_or_else(|| UserError("Can't find your location".to_owned()))?; + if loc_item.owner.as_ref() != Some(&player_item.refstr()) || !loc_item.flags.contains(&ItemFlag::PrivatePlace) { + user_error("You can only uninstall things while standing in a private room you own. \ + If you are outside, try uninstalling from the inside." + .to_owned())?; + } + + let dir = Direction::parse(what_dir_raw.trim()).ok_or_else( + || UserError("Invalid direction.".to_owned()))?; + + let cand_items = search_items_for_user(ctx, &ItemSearchParams { + include_loc_contents: true, + ..ItemSearchParams::base(&player_item, uninstall_what_raw.trim()) + }).await?; + let item = cand_items.iter().find(|it| it.action_type.is_in_direction(&dir)) + .ok_or_else( + || UserError( + "Sorry, I couldn't find anything matching installed on that door.".to_owned()))?; + if item.item_type != "possession" { + user_error("You can't uninstall that!".to_owned())?; + } + + let handler = match item.possession_type.as_ref() + .and_then(|pt| possession_data().get(pt)) + .and_then(|pd| pd.install_handler) { + None => user_error("You can't uninstall that!".to_owned())?, + Some(h) => h + }; + + handler.uninstall_cmd(ctx, &player_item, &item, &loc_item, &dir).await?; + + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index d3f9015f..5b0cc3a5 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -260,6 +260,23 @@ pub enum LocationActionType { InstalledOnDoorAsLock(Direction), } +impl LocationActionType { + pub fn is_visible_in_look(&self) -> bool { + use LocationActionType::*; + match self { + InstalledOnDoorAsLock(_) => false, + _ => true + } + } + pub fn is_in_direction(&self, dir: &Direction) -> bool { + use LocationActionType::*; + match self { + InstalledOnDoorAsLock(d) if d == dir => true, + _ => false + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum Sex { Male, diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 84e4efd8..28ee61f9 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -332,7 +332,7 @@ pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> { if let Some(item) = trans.find_by_action_and_location( - &format!("{}/{}", &who.item_type, &who.item_code), &LocationActionType::Wielded).await? { + &who.refstr(), &LocationActionType::Wielded).await? { if let Some(dat) = item.possession_type.as_ref() .and_then(|pt| possession_data().get(&pt)) .and_then(|pd| pd.weapon_data.as_ref()) { diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 27b3f400..96985fc1 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -3,6 +3,7 @@ use crate::{ models::item::{SkillType, Item, Pronouns}, models::consent::ConsentType, message_handler::user_commands::{UResult, VerbContext}, + static_content::room::Direction, }; use once_cell::sync::OnceCell; use std::collections::BTreeMap; @@ -122,6 +123,12 @@ pub trait ArglessHandler { async fn cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item) -> UResult<()>; } +#[async_trait] +pub trait InstallHandler { + async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, direction: &Direction) -> UResult<()>; + async fn uninstall_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, direction: &Direction) -> UResult<()>; +} + pub struct PossessionData { pub weapon_data: Option, pub display: &'static str, @@ -134,9 +141,10 @@ pub struct PossessionData { pub use_data: Option, pub becomes_on_spent: Option, pub weight: u64, - pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, - pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, + pub install_handler: Option<&'static (dyn InstallHandler + Sync + Send)>, pub lockcheck_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, + pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, + pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, } impl Default for PossessionData { @@ -153,9 +161,10 @@ impl Default for PossessionData { charge_data: None, becomes_on_spent: None, use_data: None, - write_handler: None, - sign_handler: None, + install_handler: None, lockcheck_handler: None, + sign_handler: None, + write_handler: None, } } } diff --git a/blastmud_game/src/static_content/possession_type/lock.rs b/blastmud_game/src/static_content/possession_type/lock.rs index b8dc57b3..31e8780f 100644 --- a/blastmud_game/src/static_content/possession_type/lock.rs +++ b/blastmud_game/src/static_content/possession_type/lock.rs @@ -1,11 +1,126 @@ -use super::PossessionData; +use super::{PossessionData, ArglessHandler}; +use crate::{ + models::item::{Item, LocationActionType}, + message_handler::user_commands::{user_error, VerbContext, UResult}, + static_content::{ + possession_type::InstallHandler, + room::Direction, + }, + services::{comms::broadcast_to_room, + capacity::{check_item_capacity, CapacityLevel}} +}; +use async_trait::async_trait; + +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?; + } else { + user_error( + "The scanlock in the door emits a medium-pitched tone followed by a low tone, and remains locked.".to_owned())?; + } + Ok(()) + } +} + +static LOCK_WEIGHT: u64 = 500; + +struct ScanLockInstall; +#[async_trait] +impl InstallHandler for ScanLockInstall { + async fn install_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, + direction: &Direction) -> UResult<()> { + if what.action_type != LocationActionType::Normal { + user_error("That scanlock is already in use.".to_owned())?; + } + + if ctx.trans.find_by_action_and_location( + &room.refstr(), + &LocationActionType::InstalledOnDoorAsLock(direction.clone()) + ).await?.is_some() { + user_error("There's already a lock on that door - uninstall it first.".to_owned())?; + } + + match check_item_capacity(&ctx.trans, &room.refstr(), LOCK_WEIGHT).await? { + CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => + user_error("That room has so much stuff, you can't install anything new." + .to_owned())?, + _ => {} + }; + let mut what_mut = (*what).clone(); + what_mut.location = room.refstr(); + what_mut.action_type = LocationActionType::InstalledOnDoorAsLock(direction.clone()); + what_mut.owner = room.owner.clone(); + ctx.trans.save_item_model(&what_mut).await?; + + broadcast_to_room( + &ctx.trans, &room.refstr(), None, + &format!("{} bangs the door to the {} as he installs {} on it.\n", + &player.display_for_sentence(true, 1, true), + &direction.describe(), + &what.display_for_sentence(true, 1, false)), + Some( + &format!("{} bangs the door to the {} as he installs {} on it.\n", + &player.display_for_sentence(false, 1, true), + &direction.describe(), + &what.display_for_sentence(false, 1, false)), + )).await?; + Ok(()) + } + + async fn uninstall_cmd(&self, ctx: &mut VerbContext, player: &Item, what: &Item, room: &Item, + direction: &Direction) -> UResult<()> { + if what.action_type != LocationActionType::InstalledOnDoorAsLock(direction.clone()) { + user_error("That scanlock is not installed as a lock.".to_owned())?; + } + + let mut what_mut = (*what).clone(); + + let extra_text = match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? { + CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => { + ", dropping it on the floor since he can't hold it." + }, + _ => { + what_mut.location = player.refstr(); + "" + } + }; + what_mut.action_type = LocationActionType::Normal; + what_mut.owner = None; + ctx.trans.save_item_model(&what_mut).await?; + + broadcast_to_room( + &ctx.trans, &room.refstr(), None, + &format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n", + &player.display_for_sentence(true, 1, true), + &direction.describe(), + &what.display_for_sentence(true, 1, false), + extra_text + ), + Some( + &format!("{} bangs the door to the {} as he uninstalls {} from it{}.\n", + &player.display_for_sentence(false, 1, true), + &direction.describe(), + &what.display_for_sentence(false, 1, false), + extra_text + ), + )).await?; + Ok(()) + } +} + pub fn scan() -> PossessionData { PossessionData { display: "scanlock", details: "A relatively basic lock with a fingerprint scanner built into it, made to ensure only the owner of something can enter or use it.", aliases: vec!("lock"), - weight: 500, + weight: LOCK_WEIGHT, + lockcheck_handler: Some(&ScanLockLockcheck), + install_handler: Some(&ScanLockInstall), ..Default::default() } }