From 62f5457d3aca54e99c910e6978ec41f9424d4d91 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sun, 16 Apr 2023 22:12:19 +1000 Subject: [PATCH] Open doors automatically on movement. --- .../src/message_handler/user_commands/look.rs | 51 ++++- .../message_handler/user_commands/movement.rs | 31 ++- .../src/message_handler/user_commands/open.rs | 204 ++++++++++-------- blastmud_game/src/models/item.rs | 11 + blastmud_game/src/static_content/dynzone.rs | 7 +- .../dynzone/cokmurl_apartment.rs | 2 +- blastmud_game/src/static_content/npc.rs | 2 +- 7 files changed, 201 insertions(+), 107 deletions(-) diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs index fe36aec..40a2f69 100644 --- a/blastmud_game/src/message_handler/user_commands/look.rs +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -1,11 +1,16 @@ -use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, - get_player_item_or_fail, search_item_for_user}; +use super::{ + VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, + get_player_item_or_fail, search_item_for_user, + map::{render_map, render_map_dyn}, + open::{is_door_in_direction, DoorSituation}, +}; use async_trait::async_trait; use ansi::{ansi, flow_around, word_wrap}; use crate::{ db::ItemSearchParams, models::{item::{ - Item, LocationActionType, Subattack, ItemFlag, ItemSpecialData + Item, LocationActionType, Subattack, ItemFlag, ItemSpecialData, + DoorState }}, static_content::{ room::{self, Direction}, @@ -15,7 +20,6 @@ use crate::{ language, services::combat::max_health, }; -use super::map::{render_map, render_map_dyn}; use itertools::Itertools; use std::sync::Arc; use mockall_double::double; @@ -176,6 +180,32 @@ pub async fn describe_dynroom(ctx: &VerbContext<'_>, Ok(()) } +async fn describe_door( + ctx: &VerbContext<'_>, + room_item: &Item, + state: &DoorState, + direction: &Direction, +) -> UResult<()> { + let mut msg = format!("That exit is blocked by {}.", + &state.description); + if let Some(lock) = ctx.trans.find_by_action_and_location( + &room_item.refstr(), + &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), + &lock_desc + )); + } + msg.push('\n'); + ctx.trans.queue_for_session( + ctx.session, + Some(&msg)).await?; + Ok(()) + +} + async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult { if item.flags.contains(&ItemFlag::NoSeeContents) { return Ok(" It is too foggy to see who or what else is here.".to_owned()); @@ -321,6 +351,19 @@ impl UserVerb for Verb { ctx.trans.find_item_by_type_code(heretype, herecode).await? .ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))? } else if let Some(dir) = Direction::parse(&rem_trim) { + match is_door_in_direction(&ctx.trans, &dir, &player_item).await? { + DoorSituation::NoDoor | + DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } | + DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } => {}, + DoorSituation::DoorIntoRoom { state, room_with_door, .. } => { + if let Some(rev_dir) = dir.reverse() { + return describe_door(ctx, &room_with_door, &state, &rev_dir).await; + } + }, + DoorSituation::DoorOutOfRoom { state, room_with_door, .. } => { + return describe_door(ctx, &room_with_door, &state, &dir).await; + } + } direction_to_item(&ctx.trans, use_location, &dir).await? .ok_or_else(|| UserError("There's nothing in that direction".to_owned()))? } else if rem_trim == "me" || rem_trim == "self" { diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index 718126c..a20ee67 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -2,6 +2,7 @@ use super::{ VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, get_player_item_or_fail, look, + open::{DoorSituation, is_door_in_direction, attempt_open_immediate}, }; use async_trait::async_trait; use crate::{ @@ -20,7 +21,8 @@ use crate::{ Item, ItemSpecialData, SkillType, - LocationActionType + LocationActionType, + DoorState, }, services::{ comms::broadcast_to_room, @@ -139,7 +141,9 @@ pub async fn attempt_move_immediate( trans: &DBTrans, orig_mover: &Item, direction: &Direction, - mut player_ctx: Option<&mut VerbContext<'_>> + // 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<'_>> ) -> UResult<()> { let use_location = if orig_mover.is_dead { if orig_mover.item_type != "player" { @@ -150,6 +154,27 @@ pub async fn attempt_move_immediate( &orig_mover.location }; + match is_door_in_direction(trans, direction, orig_mover).await? { + DoorSituation::NoDoor | + DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } | + DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => {}, + _ => { + 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(()); + } + } + } + } + let mut mover = (*orig_mover).clone(); let (new_loc, new_loc_item) = move_to_where(trans, use_location, direction, &mut mover, &mut player_ctx).await?; @@ -253,7 +278,7 @@ impl QueueCommandHandler for QueueHandler { _ => user_error("Unexpected command".to_owned())? }; let player_item = get_player_item_or_fail(ctx).await?; - attempt_move_immediate(ctx.trans, &player_item, direction, Some(ctx)).await?; + attempt_move_immediate(ctx.trans, &player_item, direction, &mut Some(ctx)).await?; Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/open.rs b/blastmud_game/src/message_handler/user_commands/open.rs index 747410a..4b836da 100644 --- a/blastmud_game/src/message_handler/user_commands/open.rs +++ b/blastmud_game/src/message_handler/user_commands/open.rs @@ -20,7 +20,7 @@ use crate::{ possession_type::possession_data, }, models::{ - item::{Item, LocationActionType}, + item::{Item, LocationActionType, DoorState}, task::{Task, TaskMeta, TaskDetails} }, services::comms::broadcast_to_room, @@ -28,6 +28,8 @@ use crate::{ use std::sync::Arc; use std::time; use chrono::{self, Utc}; +use mockall_double::double; +#[double] use crate::db::DBTrans; use log::info; #[derive(Clone)] @@ -37,7 +39,6 @@ pub static SWING_SHUT_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &Swing #[async_trait] impl TaskHandler for SwingShutHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { - info!("Starting swing shut"); let (room_str, direction) = match &ctx.task.details { TaskDetails::SwingShut { room_item, direction } => (room_item, direction), _ => { return Ok(None); } @@ -69,27 +70,118 @@ impl TaskHandler for SwingShutHandler { broadcast_to_room(&ctx.trans, &other_room.refstr(), None, &msg, Some(&msg)).await?; } - info!("Finishing swing shut"); Ok(None) } } +pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut VerbContext<'_>>, + who: &Item, direction: &Direction) -> UResult<()> { + if who.is_dead { + user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?; + } + let (room_1, dir_in_room, room_2) = match is_door_in_direction(trans, &direction, &who).await? { + DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?, + DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } | + DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => + user_error("The door is already open.".to_owned())?, + DoorSituation::DoorIntoRoom { room_with_door, current_room, .. } => { + let entering_room_loc = room_with_door.refstr(); + if let Some(revdir) = direction.reverse() { + if let Some(lock) = trans.find_by_action_and_location( + &entering_room_loc, + &LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await? + { + 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())?; + } + } + let mut entering_room_mut = (*room_with_door).clone(); + if let Some(door_map) = entering_room_mut.door_states.as_mut() { + if let Some(door) = door_map.get_mut(&revdir) { + (*door).open = true; + info!("Set door_map"); + } else { + info!("door_map missing direction"); + } + } else { + info!("door_map None"); + } + info!("Saving door map"); + 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())? + } + }, + DoorSituation::DoorOutOfRoom { room_with_door, new_room, .. } => { + let mut entering_room_mut = (*room_with_door).clone(); + if let Some(door_map) = entering_room_mut.door_states.as_mut() { + if let Some(door) = door_map.get_mut(&direction) { + (*door).open = true; + } + } + trans.save_item_model(&entering_room_mut).await?; + (room_with_door, direction.clone(), new_room) + } + }; + + for (loc, dir) in [(&room_1.refstr(), &dir_in_room.describe()), + (&room_2.refstr(), &dir_in_room.reverse().map(|d| d.describe()) + .unwrap_or_else(|| "outside".to_owned()))] { + broadcast_to_room( + &trans, + loc, + None, + &format!("{} opens the door to the {}.\n", + &who.display_for_sentence(true, 1, true), + dir + ), + Some( + &format!("{} opens the door to the {}.\n", + &who.display_for_sentence(false, 1, true), + dir + ) + ) + ).await?; + } + + trans.upsert_task(&Task { + meta: TaskMeta { + task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()), + next_scheduled: Utc::now() + chrono::Duration::seconds(120), + ..Default::default() + }, + details: TaskDetails::SwingShut { + room_item: room_1.refstr(), + direction: dir_in_room.clone() + } + }).await?; + + Ok(()) +} + pub struct QueueHandler; #[async_trait] impl QueueCommandHandler for QueueHandler { async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult { - info!("Starting open start_command"); let direction = match command { QueueCommand::OpenDoor { direction } => direction, _ => user_error("Unexpected command".to_owned())? }; - info!("Direction good"); let player_item = get_player_item_or_fail(ctx).await?; - info!("Got player"); - match is_door_in_direction(ctx, &direction, &player_item).await? { + match is_door_in_direction(&ctx.trans, &direction, &player_item).await? { DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?, - DoorSituation::DoorIntoRoom { is_open: true, .. } | DoorSituation::DoorOutOfRoom { is_open: true, .. } => + DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } | + DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => user_error("The door is already open.".to_owned())?, DoorSituation::DoorIntoRoom { room_with_door: entering_room, .. } => { let entering_room_loc = entering_room.refstr(); @@ -109,118 +201,42 @@ impl QueueCommandHandler for QueueHandler { _ => {} } - info!("Clean exit open start_command"); Ok(time::Duration::from_secs(1)) } #[allow(unreachable_patterns)] async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult<()> { - info!("Starting open finish_command"); let direction = match command { QueueCommand::OpenDoor { direction } => direction, _ => user_error("Unexpected command".to_owned())? }; - let player_item = get_player_item_or_fail(ctx).await?; - let (room_1, dir_in_room, room_2) = match is_door_in_direction(ctx, &direction, &player_item).await? { - DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?, - DoorSituation::DoorIntoRoom { is_open: true, .. } | - DoorSituation::DoorOutOfRoom { is_open: true, .. } => - user_error("The door is already open.".to_owned())?, - DoorSituation::DoorIntoRoom { room_with_door, current_room, .. } => { - let entering_room_loc = room_with_door.refstr(); - if let Some(revdir) = direction.reverse() { - if let Some(lock) = ctx.trans.find_by_action_and_location( - &entering_room_loc, - &LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await? - { - 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, &player_item, &lock).await? - } - } - let mut entering_room_mut = (*room_with_door).clone(); - if let Some(door_map) = entering_room_mut.door_states.as_mut() { - if let Some(door) = door_map.get_mut(&direction) { - (*door).open = true; - } - } - ctx.trans.save_item_model(&entering_room_mut).await?; - (room_with_door, revdir, current_room) + let player_item = get_player_item_or_fail(ctx).await?; + attempt_open_immediate(&ctx.trans, &mut Some(ctx), &player_item, &direction).await?; - } else { - user_error("There's no door possible there.".to_owned())? - } - }, - DoorSituation::DoorOutOfRoom { room_with_door, new_room, .. } => { - let mut entering_room_mut = (*room_with_door).clone(); - if let Some(door_map) = entering_room_mut.door_states.as_mut() { - if let Some(door) = door_map.get_mut(&direction) { - (*door).open = true; - } - } - ctx.trans.save_item_model(&entering_room_mut).await?; - (room_with_door, direction.clone(), new_room) - } - }; - - for (loc, dir) in [(&room_1.refstr(), &dir_in_room.describe()), - (&room_2.refstr(), &dir_in_room.reverse().map(|d| d.describe()) - .unwrap_or_else(|| "outside".to_owned()))] { - broadcast_to_room( - &ctx.trans, - loc, - None, - &format!("{} opens the door to the {}.\n", - &player_item.display_for_sentence(true, 1, true), - dir - ), - Some( - &format!("{} opens the door to the {}.\n", - &player_item.display_for_sentence(false, 1, true), - dir - ) - ) - ).await?; - } - - ctx.trans.upsert_task(&Task { - meta: TaskMeta { - task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()), - next_scheduled: Utc::now() + chrono::Duration::seconds(120), - ..Default::default() - }, - details: TaskDetails::SwingShut { - room_item: room_1.refstr(), - direction: dir_in_room.clone() - } - }).await?; - - info!("Clean exit open finish_command"); Ok(()) } } pub enum DoorSituation { NoDoor, - DoorIntoRoom { is_open: bool, room_with_door: Arc, current_room: Arc }, // Can be locked etc... - DoorOutOfRoom { is_open: bool, room_with_door: Arc, new_room: Arc } // No lockable. + DoorIntoRoom { state: DoorState, room_with_door: Arc, current_room: Arc }, // Can be locked etc... + DoorOutOfRoom { state: DoorState, room_with_door: Arc, new_room: Arc } // No lockable. } -pub async fn is_door_in_direction(ctx: &mut VerbContext<'_>, direction: &Direction, player_item: &Item) -> +pub async fn is_door_in_direction(trans: &DBTrans, direction: &Direction, player_item: &Item) -> UResult { let (loc_type_t, loc_type_c) = player_item.location.split_once("/") .ok_or_else(|| UserError("Invalid location".to_owned()))?; - let cur_loc_item = ctx.trans.find_item_by_type_code(loc_type_t, loc_type_c).await? + let cur_loc_item = trans.find_item_by_type_code(loc_type_t, loc_type_c).await? .ok_or_else(|| UserError("Can't find your current location anymore.".to_owned()))?; - let new_loc_item = direction_to_item(ctx.trans, &player_item.location, direction).await? + let new_loc_item = direction_to_item(trans, &player_item.location, direction).await? .ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?; if let Some(door_state) = cur_loc_item.door_states.as_ref() .and_then(|v| v.get(direction)) { return Ok(DoorSituation::DoorOutOfRoom { - is_open: door_state.open, + state: door_state.clone(), room_with_door: cur_loc_item, new_room: new_loc_item }); @@ -230,7 +246,7 @@ pub async fn is_door_in_direction(ctx: &mut VerbContext<'_>, direction: &Directi .and_then(|v| direction.reverse().as_ref() .and_then(|rev| v.get(rev).map(|door| door.clone()))) { return Ok(DoorSituation::DoorIntoRoom { - is_open: door_state.open, + state: door_state.clone(), room_with_door: new_loc_item, current_room: cur_loc_item }); @@ -245,9 +261,7 @@ impl UserVerb for Verb { async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { let dir = Direction::parse(remaining) .ok_or_else(|| UserError("Unknown direction".to_owned()))?; - info!("Queueing open"); queue_command(ctx, &QueueCommand::OpenDoor { direction: dir.clone() }).await?; - info!("Returning from open handler"); Ok(()) } } diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 82a5077..d3f9015 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -310,8 +310,19 @@ pub struct DynamicEntrance { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] +#[serde(default)] pub struct DoorState { pub open: bool, + pub description: String +} + +impl Default for DoorState { + fn default() -> Self { + Self { + open: false, + description: "a solid looking wooden door".to_owned(), + } + } } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] diff --git a/blastmud_game/src/static_content/dynzone.rs b/blastmud_game/src/static_content/dynzone.rs index 68b7328..b2adbed 100644 --- a/blastmud_game/src/static_content/dynzone.rs +++ b/blastmud_game/src/static_content/dynzone.rs @@ -100,9 +100,10 @@ impl Dynzone { owner: Some(owner.clone()), door_states: Some(room.exits.iter() .filter_map(|ex| - if ex.exit_type == ExitType::Doored { + if let ExitType::Doored { description } = ex.exit_type { Some((ex.direction.clone(), DoorState { - open: false + open: false, + description: description.to_owned() })) } else { None @@ -133,7 +134,7 @@ impl Default for Dynzone { #[derive(Eq, Clone, PartialEq, Ord, PartialOrd)] pub enum ExitType { Doorless, - Doored, + Doored { description: &'static str }, } #[derive(Eq, Ord, Debug, PartialEq, PartialOrd, Clone)] diff --git a/blastmud_game/src/static_content/dynzone/cokmurl_apartment.rs b/blastmud_game/src/static_content/dynzone/cokmurl_apartment.rs index d4c7f03..c2e334d 100644 --- a/blastmud_game/src/static_content/dynzone/cokmurl_apartment.rs +++ b/blastmud_game/src/static_content/dynzone/cokmurl_apartment.rs @@ -49,7 +49,7 @@ pub fn zone() -> Dynzone { Exit { direction: Direction::WEST, target: ExitTarget::Intrazone { subcode: "doorstep" }, - exit_type: ExitType::Doored + exit_type: ExitType::Doored { description: "a reasonably sturdy looking fire-rated solid core beige painted door" } } ), grid_coords: GridCoords { x: 1, y: 0, z: 0 }, diff --git a/blastmud_game/src/static_content/npc.rs b/blastmud_game/src/static_content/npc.rs index 77736fc..39e9254 100644 --- a/blastmud_game/src/static_content/npc.rs +++ b/blastmud_game/src/static_content/npc.rs @@ -354,7 +354,7 @@ impl TaskHandler for NPCWanderTaskHandler { ); 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, None).await { + match attempt_move_immediate(ctx.trans, &item, &dir, &mut None).await { Ok(()) | Err(CommandHandlingError::UserError(_)) => {}, Err(CommandHandlingError::SystemError(e)) => Err(e)? }