Open doors automatically on movement.

This commit is contained in:
Condorra 2023-04-16 22:12:19 +10:00
parent 131512fbf6
commit 62f5457d3a
7 changed files with 201 additions and 107 deletions

View File

@ -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<String> {
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" {

View File

@ -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(())
}
}

View File

@ -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<Option<time::Duration>> {
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<time::Duration> {
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<Item>, current_room: Arc<Item> }, // Can be locked etc...
DoorOutOfRoom { is_open: bool, room_with_door: Arc<Item>, new_room: Arc<Item> } // No lockable.
DoorIntoRoom { state: DoorState, room_with_door: Arc<Item>, current_room: Arc<Item> }, // Can be locked etc...
DoorOutOfRoom { state: DoorState, room_with_door: Arc<Item>, new_room: Arc<Item> } // 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<DoorSituation> {
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(())
}
}

View File

@ -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)]

View File

@ -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)]

View File

@ -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 },

View File

@ -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)?
}