forked from blasthavers/blastmud
Also fix up bugs with navigation during death, and awarding payouts when you don't get any XP.
337 lines
14 KiB
Rust
337 lines
14 KiB
Rust
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::{
|
|
DResult,
|
|
language,
|
|
regular_tasks::queued_command::{
|
|
QueueCommandHandler,
|
|
QueueCommand,
|
|
queue_command
|
|
},
|
|
static_content::{
|
|
room::{self, Direction, ExitType},
|
|
dynzone::{dynzone_by_type, ExitTarget as DynExitTarget, DynzoneType},
|
|
},
|
|
models::{
|
|
item::{
|
|
Item,
|
|
ItemSpecialData,
|
|
SkillType,
|
|
LocationActionType,
|
|
DoorState,
|
|
},
|
|
consent::ConsentType,
|
|
},
|
|
services::{
|
|
comms::broadcast_to_room,
|
|
skills::skill_check_and_grind,
|
|
combat::stop_attacking_mut,
|
|
combat::handle_resurrect,
|
|
check_consent,
|
|
}
|
|
};
|
|
use std::sync::Arc;
|
|
use mockall_double::double;
|
|
#[double] use crate::db::DBTrans;
|
|
use std::time;
|
|
use ansi::ansi;
|
|
|
|
pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, arriving: &Item) -> DResult<()> {
|
|
let msg_leaving_exp = format!("{} departs towards {}\n",
|
|
&character.display_for_sentence(true, 1, true),
|
|
&arriving.display);
|
|
let msg_leaving_nonexp = format!("{} departs towards {}\n",
|
|
character.display_for_sentence(true, 1, false),
|
|
arriving.display_less_explicit
|
|
.as_ref()
|
|
.unwrap_or(&arriving.display));
|
|
broadcast_to_room(trans, &format!("{}/{}", &leaving.item_type, &leaving.item_code),
|
|
None, &msg_leaving_exp, Some(&msg_leaving_nonexp)).await?;
|
|
|
|
let msg_arriving_exp = format!("{} arrives from {}\n", &character.display_for_sentence(true, 1, true),
|
|
&leaving.display);
|
|
let msg_arriving_nonexp = format!("{} arrives from {}\n",
|
|
character.display_for_sentence(true, 1, false),
|
|
leaving.display_less_explicit
|
|
.as_ref()
|
|
.unwrap_or(&leaving.display));
|
|
broadcast_to_room(trans, &format!("{}/{}", &arriving.item_type, &arriving.item_code),
|
|
None, &msg_arriving_exp, Some(&msg_arriving_nonexp)).await?;
|
|
Ok(())
|
|
}
|
|
|
|
async fn move_to_where(
|
|
trans: &DBTrans,
|
|
use_location: &str,
|
|
direction: &Direction,
|
|
mover: &mut Item,
|
|
player_ctx: &mut Option<&mut VerbContext<'_>>
|
|
) -> UResult<(String, Option<Item>)> {
|
|
// 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? {
|
|
return Ok((format!("{}/{}",
|
|
&dynroom_result.item_type,
|
|
&dynroom_result.item_code), Some(dynroom_result)));
|
|
}
|
|
|
|
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen"));
|
|
|
|
if heretype == "dynroom" {
|
|
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
|
|
None => user_error("Your current room has vanished!".to_owned())?,
|
|
Some(v) => v
|
|
};
|
|
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
|
|
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code),
|
|
_ => user_error("Your current room is invalid!".to_owned())?
|
|
};
|
|
let dynzone = dynzone_by_type().get(&DynzoneType::from_str(dynzone_code)
|
|
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?)
|
|
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?;
|
|
let dynroom = dynzone.dyn_rooms.get(dynroom_code.as_str())
|
|
.ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
|
|
let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction)
|
|
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
|
|
return match exit.target {
|
|
DynExitTarget::ExitZone => {
|
|
let (zonetype, zonecode) = old_dynroom_item.location.split_once("/")
|
|
.ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?;
|
|
let zoneitem = trans.find_item_by_type_code(zonetype, zonecode).await?
|
|
.ok_or_else(|| UserError("Can't find your zone".to_owned()))?;
|
|
let zone_exit = match zoneitem.special_data.as_ref() {
|
|
Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) =>
|
|
user_error("That exit doesn't seem to go anywhere".to_owned())?,
|
|
Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit,
|
|
_ => user_error("The zone you are in has invalid data associated with it".to_owned())?,
|
|
};
|
|
Ok((zone_exit.to_string(), None))
|
|
},
|
|
DynExitTarget::Intrazone { subcode } => {
|
|
let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await?
|
|
.ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?;
|
|
Ok((format!("{}/{}", &to_item.item_type, &to_item.item_code), Some(to_item)))
|
|
}
|
|
}
|
|
}
|
|
if heretype != "room" {
|
|
user_error("Navigating outside rooms not yet supported.".to_owned())?
|
|
}
|
|
let room = room::room_map_by_code().get(herecode)
|
|
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
|
|
let exit = room.exits.iter().find(|ex| ex.direction == *direction)
|
|
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
|
|
|
|
match exit.exit_type {
|
|
ExitType::Free => {}
|
|
ExitType::Blocked(blocker) => {
|
|
if let Some(ctx) = player_ctx {
|
|
if !blocker.attempt_exit(*ctx, mover, exit).await? {
|
|
user_error("Stopping movement".to_owned())?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
let new_room =
|
|
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?;
|
|
Ok((format!("room/{}", new_room.code), None))
|
|
}
|
|
|
|
pub async fn check_room_access(trans: &DBTrans, player: &Item, room: &Item) -> UResult<()> {
|
|
let (owner_t, owner_c) = match room.owner.as_ref().and_then(|o| o.split_once("/")) {
|
|
None => return Ok(()),
|
|
Some(v) => v
|
|
};
|
|
if owner_t == &player.item_type && owner_c == &player.item_code {
|
|
return Ok(());
|
|
}
|
|
let owner = match trans.find_item_by_type_code(owner_t, owner_c).await? {
|
|
None => return Ok(()),
|
|
Some(v) => v
|
|
};
|
|
|
|
if check_consent(trans, "enter", &ConsentType::Visit, player, &owner).await? {
|
|
return Ok(());
|
|
}
|
|
|
|
let mut player_hypothet = (*player).clone();
|
|
// We are asking hypothetically if they entered the room, could they fight
|
|
// the owner? We won't save this yet.
|
|
player_hypothet.location = room.refstr();
|
|
if check_consent(trans, "enter", &ConsentType::Fight, &player_hypothet, &owner).await? {
|
|
return Ok(());
|
|
}
|
|
|
|
user_error(ansi!("<yellow>Your wristpad buzzes and your muscles lock up, stopping you entering.<reset> \
|
|
It seems this is private property and you haven't been invited here with \
|
|
<bold>allow visit<reset>, nor do you have a <bold>allow fight<reset> in force with \
|
|
the owner here.").to_owned())?
|
|
}
|
|
|
|
pub async fn attempt_move_immediate(
|
|
trans: &DBTrans,
|
|
orig_mover: &Item,
|
|
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<'_>>
|
|
) -> UResult<()> {
|
|
let use_location = if orig_mover.death_data.is_some() {
|
|
if orig_mover.item_type != "player" {
|
|
user_error("Dead players don't move".to_owned())?;
|
|
}
|
|
"room/repro_xv_respawn"
|
|
} else {
|
|
&orig_mover.location
|
|
};
|
|
|
|
match is_door_in_direction(trans, direction, use_location).await? {
|
|
DoorSituation::NoDoor |
|
|
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => {},
|
|
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, room_with_door, .. } => {
|
|
check_room_access(trans, orig_mover, &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(());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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?;
|
|
|
|
match mover.active_combat.as_ref().and_then(|ac| ac.attacking.clone()) {
|
|
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? {
|
|
let mut vitem_mut = (*vitem).clone();
|
|
stop_attacking_mut(trans, &mut mover, &mut vitem_mut, false).await?;
|
|
trans.save_item_model(&vitem_mut).await?
|
|
}
|
|
}
|
|
}
|
|
}
|
|
match mover.active_combat.clone().as_ref().map(|ac| &ac.attacked_by[..]) {
|
|
None | Some([]) => {}
|
|
Some(attackers) => {
|
|
let mut attacker_names = Vec::new();
|
|
let mut attacker_items = Vec::new();
|
|
if let Some(ctx) = player_ctx.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));
|
|
attacker_items.push(aitem);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let attacker_names_ref = attacker_names.iter().map(|n| n.as_str()).collect::<Vec<&str>>();
|
|
let attacker_names_str = language::join_words(&attacker_names_ref[..]);
|
|
if skill_check_and_grind(trans, &mut mover, &SkillType::Dodge, attackers.len() as f64 + 8.0).await? >= 0.0 {
|
|
if let Some(ctx) = player_ctx.as_ref() {
|
|
trans.queue_for_session(ctx.session,
|
|
Some(&format!("You successfully get away from {}\n",
|
|
&attacker_names_str))).await?;
|
|
}
|
|
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?;
|
|
}
|
|
} else {
|
|
if let Some(ctx) = player_ctx.as_ref() {
|
|
trans.queue_for_session(ctx.session,
|
|
Some(&format!("You try and fail to run past {}\n",
|
|
&attacker_names_str))).await?;
|
|
}
|
|
trans.save_item_model(&mover).await?;
|
|
return Ok(());
|
|
}
|
|
}
|
|
}
|
|
|
|
if mover.death_data.is_some() {
|
|
if !handle_resurrect(trans, &mut mover).await? {
|
|
user_error("You couldn't be resurrected.".to_string())?;
|
|
}
|
|
}
|
|
|
|
mover.location = new_loc.clone();
|
|
mover.action_type = LocationActionType::Normal;
|
|
mover.active_combat = None;
|
|
|
|
trans.save_item_model(&mover).await?;
|
|
|
|
if let Some(ctx) = player_ctx {
|
|
look::VERB.handle(ctx, "look", "").await?;
|
|
}
|
|
|
|
if let Some((old_loc_type, old_loc_code)) = use_location.split_once("/") {
|
|
if let Some(old_room_item) = 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.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?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub struct QueueHandler;
|
|
#[async_trait]
|
|
impl QueueCommandHandler for QueueHandler {
|
|
async fn start_command(&self, _ctx: &mut VerbContext<'_>, _command: &QueueCommand)
|
|
-> UResult<time::Duration> {
|
|
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,
|
|
_ => 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?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
pub struct Verb;
|
|
|
|
#[async_trait]
|
|
impl UserVerb for Verb {
|
|
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> {
|
|
let dir = Direction::parse(
|
|
&(verb.to_owned() + " " + remaining.trim()).trim())
|
|
.ok_or_else(|| UserError("Unknown direction".to_owned()))?;
|
|
queue_command(ctx, &QueueCommand::Movement { direction: dir.clone() }).await
|
|
}
|
|
}
|
|
static VERB_INT: Verb = Verb;
|
|
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|