Implement delete command to reset or destroy a character.

This commit is contained in:
Condorra 2023-06-07 22:38:46 +10:00
parent 862d7e3824
commit 3292dcc13b
49 changed files with 7267 additions and 4114 deletions

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,17 @@
use super::ListenerSession; use super::ListenerSession;
use crate::DResult; #[double]
use crate::db::DBTrans;
use crate::db::{DBPool, ItemSearchParams}; use crate::db::{DBPool, ItemSearchParams};
use mockall_double::double; use crate::models::{item::Item, session::Session, user::User};
#[double] use crate::db::DBTrans; use crate::DResult;
#[cfg(not(test))] use ansi::ansi; #[cfg(not(test))]
use phf::phf_map; use ansi::ansi;
use async_trait::async_trait; use async_trait::async_trait;
use crate::models::{session::Session, user::User, item::Item}; #[cfg(not(test))]
#[cfg(not(test))] use log::warn; use log::warn;
use mockall_double::double;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use phf::phf_map;
use std::sync::Arc; use std::sync::Arc;
mod agree; mod agree;
@ -20,10 +23,11 @@ mod c;
pub mod close; pub mod close;
pub mod corp; pub mod corp;
pub mod cut; pub mod cut;
pub mod delete;
mod describe;
pub mod drop; pub mod drop;
mod gear; mod gear;
pub mod get; pub mod get;
mod describe;
mod help; mod help;
mod ignore; mod ignore;
mod install; mod install;
@ -58,24 +62,27 @@ pub struct VerbContext<'l> {
pub session: &'l ListenerSession, pub session: &'l ListenerSession,
pub session_dat: &'l mut Session, pub session_dat: &'l mut Session,
pub user_dat: &'l mut Option<User>, pub user_dat: &'l mut Option<User>,
pub trans: &'l DBTrans pub trans: &'l DBTrans,
} }
pub enum CommandHandlingError { pub enum CommandHandlingError {
UserError(String), UserError(String),
SystemError(Box<dyn std::error::Error + Send + Sync>) SystemError(Box<dyn std::error::Error + Send + Sync>),
} }
use CommandHandlingError::*; use CommandHandlingError::*;
#[async_trait] #[async_trait]
pub trait UserVerb { pub trait UserVerb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()>; async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str)
-> UResult<()>;
} }
pub type UResult<A> = Result<A, CommandHandlingError>; pub type UResult<A> = Result<A, CommandHandlingError>;
impl<T> From<T> for CommandHandlingError
impl<T> From<T> for CommandHandlingError where T: Into<Box<dyn std::error::Error + Send + Sync>> { where
T: Into<Box<dyn std::error::Error + Send + Sync>>,
{
fn from(input: T) -> CommandHandlingError { fn from(input: T) -> CommandHandlingError {
SystemError(input.into()) SystemError(input.into())
} }
@ -85,7 +92,6 @@ pub fn user_error<A>(msg: String) -> UResult<A> {
Err(UserError(msg)) Err(UserError(msg))
} }
/* Verb registries list types of commands available in different circumstances. */ /* Verb registries list types of commands available in different circumstances. */
pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send); pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send);
type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>; type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>;
@ -137,6 +143,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"close" => close::VERB, "close" => close::VERB,
"corp" => corp::VERB, "corp" => corp::VERB,
"cut" => cut::VERB, "cut" => cut::VERB,
"delete" => delete::VERB,
"drop" => drop::VERB, "drop" => drop::VERB,
"gear" => gear::VERB, "gear" => gear::VERB,
"get" => get::VERB, "get" => get::VERB,
@ -224,95 +231,129 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
None => { None => {
// If the session has been cleaned up from the database, there is // If the session has been cleaned up from the database, there is
// nowhere to go from here, so just ignore it. // nowhere to go from here, so just ignore it.
warn!("Got command from session not in database: {}", session.session); warn!(
"Got command from session not in database: {}",
session.session
);
return Ok(()); return Ok(());
} }
Some(v) => v Some(v) => v,
}; };
let mut ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat, let mut ctx = VerbContext {
user_dat: &mut user_dat }; session,
trans: &trans,
session_dat: &mut session_dat,
user_dat: &mut user_dat,
};
let handler_opt = resolve_handler(&ctx, cmd); let handler_opt = resolve_handler(&ctx, cmd);
match handler_opt { match handler_opt {
None => { None => {
trans.queue_for_session(session, trans
.queue_for_session(
session,
Some(ansi!( Some(ansi!(
"That's not a command I know. Try <bold>help<reset>\r\n" "That's not a command I know. Try <bold>help<reset>\r\n"
)) )),
).await?; )
.await?;
trans.commit().await?; trans.commit().await?;
} }
Some(handler) => { Some(handler) => match handler.handle(&mut ctx, cmd, params).await {
match handler.handle(&mut ctx, cmd, params).await {
Ok(()) => { Ok(()) => {
trans.commit().await?; trans.commit().await?;
} }
Err(UserError(err_msg)) => { Err(UserError(err_msg)) => {
pool.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?; pool.queue_for_session(session, Some(&(err_msg + "\r\n")))
} .await?;
Err(SystemError(e)) => Err(e)?
}
} }
Err(SystemError(e)) => Err(e)?,
},
} }
pool.bump_session_time(&session).await?; pool.bump_session_time(&session).await?;
Ok(()) Ok(())
} }
#[cfg(test)] #[cfg(test)]
pub async fn handle(_session: &ListenerSession, _msg: &str, _pool: &DBPool) -> DResult<()> { pub async fn handle(_session: &ListenerSession, _msg: &str, _pool: &DBPool) -> DResult<()> {
unimplemented!(); unimplemented!();
} }
pub fn is_likely_explicit(msg: &str) -> bool { pub fn is_likely_explicit(msg: &str) -> bool {
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> = static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
OnceCell::new(); let markers = EXPLICIT_MARKER_WORDS.get_or_init(|| {
let markers = EXPLICIT_MARKER_WORDS.get_or_init(|| vec![
vec!("fuck", "sex", "cock", "cunt", "dick", "pussy", "whore", "fuck", "sex", "cock", "cunt", "dick", "pussy", "whore", "orgasm", "erection",
"orgasm", "erection", "nipple", "boob", "tit")); "nipple", "boob", "tit",
]
});
for word in markers { for word in markers {
if msg.contains(word) { if msg.contains(word) {
return true return true;
} }
} }
false false
} }
pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> { pub fn get_user_or_fail<'l>(ctx: &'l VerbContext) -> UResult<&'l User> {
ctx.user_dat.as_ref() ctx.user_dat
.as_ref()
.ok_or_else(|| UserError("Not logged in".to_owned())) .ok_or_else(|| UserError("Not logged in".to_owned()))
} }
pub fn get_user_or_fail_mut<'l>(ctx: &'l mut VerbContext) -> UResult<&'l mut User> { pub fn get_user_or_fail_mut<'l>(ctx: &'l mut VerbContext) -> UResult<&'l mut User> {
ctx.user_dat.as_mut() ctx.user_dat
.as_mut()
.ok_or_else(|| UserError("Not logged in".to_owned())) .ok_or_else(|| UserError("Not logged in".to_owned()))
} }
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> { pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
Ok(ctx.trans.find_item_by_type_code( Ok(ctx
"player", &get_user_or_fail(ctx)?.username.to_lowercase()).await? .trans
.ok_or_else(|| UserError("Your character is gone, you'll need to re-register or ask an admin".to_owned()))?) .find_item_by_type_code("player", &get_user_or_fail(ctx)?.username.to_lowercase())
.await?
.ok_or_else(|| {
UserError(
"Your character is gone, you'll need to re-register or ask an admin".to_owned(),
)
})?)
} }
pub async fn search_item_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l ItemSearchParams<'l>) -> pub async fn search_item_for_user<'l>(
UResult<Arc<Item>> { ctx: &'l VerbContext<'l>,
Ok(match &ctx.trans.resolve_items_by_display_name_for_player(search).await?[..] { search: &'l ItemSearchParams<'l>,
) -> UResult<Arc<Item>> {
Ok(
match &ctx
.trans
.resolve_items_by_display_name_for_player(search)
.await?[..]
{
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?, [] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
[match_it] => match_it.clone(), [match_it] => match_it.clone(),
[item1, ..] => [item1, ..] => item1.clone(),
item1.clone(), },
}) )
} }
pub async fn search_items_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l ItemSearchParams<'l>) -> pub async fn search_items_for_user<'l>(
UResult<Vec<Arc<Item>>> { ctx: &'l VerbContext<'l>,
Ok(match &ctx.trans.resolve_items_by_display_name_for_player(search).await?[..] { search: &'l ItemSearchParams<'l>,
) -> UResult<Vec<Arc<Item>>> {
Ok(
match &ctx
.trans
.resolve_items_by_display_name_for_player(search)
.await?[..]
{
[] => user_error("Sorry, I couldn't find anything matching.".to_owned())?, [] => user_error("Sorry, I couldn't find anything matching.".to_owned())?,
v => v.into_iter().map(|it| it.clone()).collect(), v => v.into_iter().map(|it| it.clone()).collect(),
}) },
)
} }
#[cfg(test)] mod test { #[cfg(test)]
mod test {
use crate::db::MockDBTrans; use crate::db::MockDBTrans;
#[test] #[test]
@ -323,11 +364,12 @@ pub async fn search_items_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l Ite
let sess: ListenerSession = Default::default(); let sess: ListenerSession = Default::default();
let mut user_dat: Option<User> = None; let mut user_dat: Option<User> = None;
let mut session_dat: Session = Default::default(); let mut session_dat: Session = Default::default();
let ctx = VerbContext { session: &sess, let ctx = VerbContext {
session: &sess,
trans: &trans, trans: &trans,
session_dat: &mut session_dat, session_dat: &mut session_dat,
user_dat: &mut user_dat }; user_dat: &mut user_dat,
assert_eq!(resolve_handler(&ctx, "less_explicit_mode").is_some(), };
true); assert_eq!(resolve_handler(&ctx, "less_explicit_mode").is_some(), true);
} }
} }

View File

@ -1,7 +1,7 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error}; use super::{user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::models::user::{User, UserTermData}; use crate::models::user::{User, UserTermData};
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
pub struct Verb; pub struct Verb;
@ -40,33 +40,39 @@ static REQUIRED_AGREEMENTS: [&str;4] = [
fn user_mut<'a>(ctx: &'a mut VerbContext) -> UResult<&'a mut User> { fn user_mut<'a>(ctx: &'a mut VerbContext) -> UResult<&'a mut User> {
match ctx.user_dat.as_mut() { match ctx.user_dat.as_mut() {
None => Err("Checked agreements before user logged in, which is a logic error")?, None => Err("Checked agreements before user logged in, which is a logic error")?,
Some(user_dat) => Ok(user_dat) Some(user_dat) => Ok(user_dat),
} }
} }
fn terms<'a>(ctx: &'a VerbContext<'a>) -> UResult<&'a UserTermData> { fn terms<'a>(ctx: &'a VerbContext<'a>) -> UResult<&'a UserTermData> {
match ctx.user_dat.as_ref() { match ctx.user_dat.as_ref() {
None => Err("Checked agreements before user logged in, which is a logic error")?, None => Err("Checked agreements before user logged in, which is a logic error")?,
Some(user_dat) => Ok(&user_dat.terms) Some(user_dat) => Ok(&user_dat.terms),
} }
} }
fn first_outstanding_agreement(ctx: &VerbContext) -> UResult<Option<(String, String)>> { fn first_outstanding_agreement(ctx: &VerbContext) -> UResult<Option<(String, String)>> {
let existing_terms = &terms(ctx)?.accepted_terms; let existing_terms = &terms(ctx)?.accepted_terms;
for agreement in REQUIRED_AGREEMENTS { for agreement in REQUIRED_AGREEMENTS {
let shortcode = let shortcode = base64::encode(ring::digest::digest(
base64::encode(ring::digest::digest(&ring::digest::SHA256, &ring::digest::SHA256,
agreement.as_bytes()))[0..20].to_owned(); agreement.as_bytes(),
))[0..20]
.to_owned();
match existing_terms.get(&shortcode) { match existing_terms.get(&shortcode) {
None => { return Ok(Some((agreement.to_owned(), shortcode))); } None => {
return Ok(Some((agreement.to_owned(), shortcode)));
}
Some(_) => {} Some(_) => {}
} }
} }
Ok(None) Ok(None)
} }
pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool> where 'b: 'a { pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> UResult<bool>
where
'b: 'a,
{
match first_outstanding_agreement(ctx)? { match first_outstanding_agreement(ctx)? {
None => { None => {
let user = user_mut(ctx)?; let user = user_mut(ctx)?;
@ -78,12 +84,20 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U
let user = user_mut(ctx)?; let user = user_mut(ctx)?;
user.terms.terms_complete = false; user.terms.terms_complete = false;
user.terms.last_presented_term = Some(hash); user.terms.last_presented_term = Some(hash);
ctx.trans.queue_for_session(ctx.session, Some(&format!(ansi!( ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!(
"Please review the following:\r\n\ "Please review the following:\r\n\
\t{}\r\n\ \t{}\r\n\
Type <green><bold>agree<reset> to accept. If you can't or don't agree, you \ Type <green><bold>agree<reset> to accept. If you can't or don't agree, you \
unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"), unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"
text))).await?; ),
text
)),
)
.await?;
Ok(false) Ok(false)
} }
} }
@ -91,7 +105,12 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let user = user_mut(ctx)?; let user = user_mut(ctx)?;
match user.terms.last_presented_term.as_ref() { match user.terms.last_presented_term.as_ref() {
None => { None => {
@ -99,16 +118,26 @@ impl UserVerb for Verb {
user_error("There was nothing pending your agreement.".to_owned())?; user_error("There was nothing pending your agreement.".to_owned())?;
} }
Some(last_term) => { Some(last_term) => {
user.terms.accepted_terms.insert(last_term.to_owned(), Utc::now()); user.terms
.accepted_terms
.insert(last_term.to_owned(), Utc::now());
drop(user); drop(user);
if check_and_notify_accepts(ctx).await? { if check_and_notify_accepts(ctx).await? {
ctx.trans.queue_for_session(ctx.session, Some( ctx.trans
ansi!("That was the last of the terms to agree to - welcome onboard!\r\n\ .queue_for_session(
Hint: Try <bold>l<reset> to look around.\r\n"))).await?; ctx.session,
Some(ansi!(
"That was the last of the terms to agree to - welcome onboard!\r\n\
Hint: Try <bold>l<reset> to look around.\r\n"
)),
)
.await?;
} }
} }
} }
ctx.trans.save_user_model(ctx.user_dat.as_ref().unwrap()).await?; ctx.trans
.save_user_model(ctx.user_dat.as_ref().unwrap())
.await?;
Ok(()) Ok(())
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,47 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, use super::{
get_player_item_or_fail, search_item_for_user}; get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
use async_trait::async_trait; VerbContext,
use ansi::ansi;
use crate::{
services::{
combat::start_attack,
check_consent,
},
models::{
consent::ConsentType,
item::ItemFlag,
},
db::ItemSearchParams,
}; };
use crate::{
db::ItemSearchParams,
models::{consent::ConsentType, item::ItemFlag},
services::{check_consent, combat::start_attack},
};
use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("It doesn't really seem fair, but you realise you won't be able to attack anyone while you're dead!".to_string())?; user_error("It doesn't really seem fair, but you realise you won't be able to attack anyone while you're dead!".to_string())?;
} }
let attack_whom = search_item_for_user(ctx, &ItemSearchParams { let attack_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, remaining) ..ItemSearchParams::base(&player_item, remaining)
}).await?; },
)
.await?;
let (loctype, loccode) = match player_item.location.split_once("/") { let (loctype, loccode) = match player_item.location.split_once("/") {
None => user_error("Your current location is invalid!".to_owned())?, None => user_error("Your current location is invalid!".to_owned())?,
Some(l) => l Some(l) => l,
}; };
let player_loc = match ctx.trans.find_item_by_type_code(loctype, loccode).await? { let player_loc = match ctx.trans.find_item_by_type_code(loctype, loccode).await? {
None => user_error("Your current location is invalid!".to_owned())?, None => user_error("Your current location is invalid!".to_owned())?,
Some(l) => l Some(l) => l,
}; };
if player_loc.flags.contains(&ItemFlag::NoSeeContents) { if player_loc.flags.contains(&ItemFlag::NoSeeContents) {
user_error("It is too foggy to even see who is here, let alone attack!".to_owned())?; user_error("It is too foggy to even see who is here, let alone attack!".to_owned())?;
@ -44,15 +49,25 @@ impl UserVerb for Verb {
match attack_whom.item_type.as_str() { match attack_whom.item_type.as_str() {
"npc" => {} "npc" => {}
"player" => {}, "player" => {}
_ => user_error("Only characters (players / NPCs) can be attacked".to_string())? _ => user_error("Only characters (players / NPCs) can be attacked".to_string())?,
} }
if attack_whom.item_code == player_item.item_code && attack_whom.item_type == player_item.item_type { if attack_whom.item_code == player_item.item_code
&& attack_whom.item_type == player_item.item_type
{
user_error("That's you, silly!".to_string())? user_error("That's you, silly!".to_string())?
} }
if !check_consent(ctx.trans, "attack", &ConsentType::Fight, &player_item, &attack_whom).await? { if !check_consent(
ctx.trans,
"attack",
&ConsentType::Fight,
&player_item,
&attack_whom,
)
.await?
{
user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an consented is very much functional. [Try <bold>help allow<reset>]").to_string())? user_error(ansi!("<blue>Your wristpad vibrates and blocks you from doing that.<reset> You get a feeling that while the empire might be gone, the system to stop subjects with working wristpads from fighting each unless they have an consented is very much functional. [Try <bold>help allow<reset>]").to_string())?
} }

View File

@ -1,44 +1,54 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, cut::ensure_has_butcher_tool, get_player_item_or_fail, search_item_for_user, user_error,
get_player_item_or_fail, user_error, search_item_for_user, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
cut::ensure_has_butcher_tool, };
use crate::{
db::ItemSearchParams,
models::item::DeathData,
regular_tasks::queued_command::{queue_command, QueueCommand},
services::combat::corpsify_item,
static_content::possession_type::possession_data,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use crate::{
models::item::DeathData,
db::ItemSearchParams,
static_content::possession_type::{possession_data},
services::{
combat::corpsify_item,
},
regular_tasks::queued_command::{
QueueCommand,
queue_command
},
};
use std::sync::Arc; use std::sync::Arc;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You butcher things while they are dead, not while YOU are dead!".to_owned())? user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?
} }
let possible_corpse = search_item_for_user(ctx, &ItemSearchParams { let possible_corpse = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
dead_first: true, dead_first: true,
..ItemSearchParams::base(&player_item, remaining.trim()) ..ItemSearchParams::base(&player_item, remaining.trim())
}).await?; },
)
.await?;
let possession_types = match possible_corpse.death_data.as_ref() { let possession_types = match possible_corpse.death_data.as_ref() {
None => user_error(format!("You can't do that while {} is still alive!", possible_corpse.pronouns.subject))?, None => user_error(format!(
Some(DeathData { parts_remaining, ..}) => "You can't do that while {} is still alive!",
parts_remaining possible_corpse.pronouns.subject
}.clone(); ))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining,
}
.clone();
let corpse = if possible_corpse.item_type == "corpse" { let corpse = if possible_corpse.item_type == "corpse" {
possible_corpse possible_corpse
@ -48,7 +58,8 @@ impl UserVerb for Verb {
"room/valhalla" "room/valhalla"
} else { } else {
"room/repro_xv_respawn" "room/repro_xv_respawn"
}.to_owned(); }
.to_owned();
Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?) Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?)
} else { } else {
user_error("You can't butcher that!".to_owned())? user_error("You can't butcher that!".to_owned())?
@ -57,11 +68,18 @@ impl UserVerb for Verb {
ensure_has_butcher_tool(&ctx.trans, &player_item).await?; ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
for possession_type in possession_types { for possession_type in possession_types {
let possession_data = possession_data().get(&possession_type) let possession_data = possession_data()
.get(&possession_type)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?; .ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
queue_command(ctx, &QueueCommand::Cut { from_corpse: corpse.item_code.clone(), queue_command(
what_part: possession_data.display.to_owned() }).await?; ctx,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,33 +1,43 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, parsing::parse_offset, user_error, UResult, UserVerb, UserVerbRef,
get_player_item_or_fail, VerbContext,
parsing::parse_offset,
}; };
use crate::{ use crate::{
static_content::room,
static_content::possession_type::possession_data,
models::item::Item, models::item::Item,
services::capacity::{check_item_capacity, CapacityLevel}, services::capacity::{check_item_capacity, CapacityLevel},
static_content::possession_type::possession_data,
static_content::room,
}; };
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Nobody seems to listen when you try to buy... possibly because you're dead.".to_owned())? user_error(
"Nobody seems to listen when you try to buy... possibly because you're dead."
.to_owned(),
)?
} }
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" { if heretype != "room" {
user_error("Can't buy anything because you're not in a shop.".to_owned())?; user_error("Can't buy anything because you're not in a shop.".to_owned())?;
} }
let room = match room::room_map_by_code().get(herecode) { let room = match room::room_map_by_code().get(herecode) {
None => user_error("Can't find that shop.".to_owned())?, None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r Some(r) => r,
}; };
if room.stock_list.is_empty() { if room.stock_list.is_empty() {
user_error("Can't buy anything because you're not in a shop.".to_owned())? user_error("Can't buy anything because you're not in a shop.".to_owned())?
@ -43,30 +53,53 @@ impl UserVerb for Verb {
for stock in &room.stock_list { for stock in &room.stock_list {
if let Some(possession_type) = possession_data().get(&stock.possession_type) { if let Some(possession_type) = possession_data().get(&stock.possession_type) {
if possession_type.display.to_lowercase().starts_with(&match_item) || if possession_type
possession_type.display_less_explicit.map(|d| d.to_lowercase().starts_with(&match_item)).unwrap_or(false) || .display
possession_type.aliases.iter().any(|al| al.starts_with(&match_item)) { .to_lowercase()
.starts_with(&match_item)
|| possession_type
.display_less_explicit
.map(|d| d.to_lowercase().starts_with(&match_item))
.unwrap_or(false)
|| possession_type
.aliases
.iter()
.any(|al| al.starts_with(&match_item))
{
if offset_remaining <= 1 { if offset_remaining <= 1 {
if let Some(mut user) = ctx.user_dat.as_mut() { if let Some(mut user) = ctx.user_dat.as_mut() {
if user.credits < stock.list_price { if user.credits < stock.list_price {
user_error("You don't have enough credits to buy that!".to_owned())?; user_error(
"You don't have enough credits to buy that!".to_owned(),
)?;
} }
user.credits -= stock.list_price; user.credits -= stock.list_price;
let player_item_str = format!("player/{}", &player_item.item_code); let player_item_str = format!("player/{}", &player_item.item_code);
let item_code = ctx.trans.alloc_item_code().await?; let item_code = ctx.trans.alloc_item_code().await?;
let loc = match check_item_capacity(ctx.trans, &player_item_str, let loc = match check_item_capacity(
possession_type.weight).await? { ctx.trans,
&player_item_str,
possession_type.weight,
)
.await?
{
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => { CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
match check_item_capacity(ctx.trans, &player_item.location, match check_item_capacity(
possession_type.weight).await? { ctx.trans,
CapacityLevel::AboveItemLimit => &player_item.location,
user_error( possession_type.weight,
)
.await?
{
CapacityLevel::AboveItemLimit => user_error(
"You can't carry it, and there is too much stuff \ "You can't carry it, and there is too much stuff \
here already".to_owned())?, here already"
_ => &player_item.location .to_owned(),
)?,
_ => &player_item.location,
} }
} }
_ => &player_item_str _ => &player_item_str,
}; };
let new_item = Item { let new_item = Item {
item_code: format!("{}", item_code), item_code: format!("{}", item_code),
@ -74,10 +107,15 @@ impl UserVerb for Verb {
..stock.possession_type.clone().into() ..stock.possession_type.clone().into()
}; };
ctx.trans.create_item(&new_item).await?; ctx.trans.create_item(&new_item).await?;
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
&ctx.session, &ctx.session,
Some(&format!("Your wristpad beeps for a deduction of {} credits.\n", stock.list_price)) Some(&format!(
).await?; "Your wristpad beeps for a deduction of {} credits.\n",
stock.list_price
)),
)
.await?;
} }
return Ok(()); return Ok(());
} else { } else {
@ -85,7 +123,6 @@ impl UserVerb for Verb {
} }
} }
} }
} }
user_error(ansi!("That doesn't seem to be for sale. Try <bold>list<reset>").to_owned()) user_error(ansi!("That doesn't seem to be for sale. Try <bold>list<reset>").to_owned())
} }

View File

@ -1,35 +1,45 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{
get_user_or_fail, get_player_item_or_fail, user_error}; get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserVerb, UserVerbRef,
use async_trait::async_trait; VerbContext,
use crate::{
models::corp::CorpCommType
}; };
use crate::models::corp::CorpCommType;
use ansi::{ansi, ignore_special_characters}; use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let user = get_user_or_fail(ctx)?; let user = get_user_or_fail(ctx)?;
let (corp_id, corp, msg) = if remaining.starts_with("@") { let (corp_id, corp, msg) = if remaining.starts_with("@") {
match remaining[1..].split_once(" ") { match remaining[1..].split_once(" ") {
None => user_error("Usage: c message (lowest ordered corp) or c @corpname message" None => user_error(
.to_owned())?, "Usage: c message (lowest ordered corp) or c @corpname message".to_owned(),
)?,
Some((corpname, msg)) => { Some((corpname, msg)) => {
let (corp_id, corp, _) = let (corp_id, corp, _) = match ctx
match ctx.trans.match_user_corp_by_name(corpname.trim(), &user.username).await? { .trans
None => user_error("You don't seem to belong to a matching corp!".to_owned())?, .match_user_corp_by_name(corpname.trim(), &user.username)
Some(c) => c .await?
{
None => {
user_error("You don't seem to belong to a matching corp!".to_owned())?
}
Some(c) => c,
}; };
(corp_id, corp, msg.trim()) (corp_id, corp, msg.trim())
} }
} }
} else { } else {
let (corp_id, corp) = let (corp_id, corp) = match ctx.trans.get_default_corp_for_user(&user.username).await? {
match ctx.trans.get_default_corp_for_user(&user.username).await? {
None => user_error("You're not a member of any corps.".to_owned())?, None => user_error("You're not a member of any corps.".to_owned())?,
Some(v) => v Some(v) => v,
}; };
(corp_id, corp, remaining.trim()) (corp_id, corp, remaining.trim())
}; };
@ -41,12 +51,19 @@ impl UserVerb for Verb {
} }
let player = get_player_item_or_fail(ctx).await?; let player = get_player_item_or_fail(ctx).await?;
ctx.trans.broadcast_to_corp(&corp_id, &CorpCommType::Chat, ctx.trans
.broadcast_to_corp(
&corp_id,
&CorpCommType::Chat,
Some(&user.username), Some(&user.username),
&format!(ansi!("<yellow>{} (to {}): <reset><bold>\"{}\"<reset>\n"), &format!(
ansi!("<yellow>{} (to {}): <reset><bold>\"{}\"<reset>\n"),
&player.display_for_sentence(false, 1, true), &player.display_for_sentence(false, 1, true),
&corp.name, &corp.name,
&msg)).await?; &msg
),
)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,23 +1,13 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
get_player_item_or_fail, get_player_item_or_fail,
open::{is_door_in_direction, DoorSituation}, open::{is_door_in_direction, DoorSituation},
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
}; };
use crate::{ use crate::{
regular_tasks::{ models::item::DoorState,
queued_command::{ regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
QueueCommandHandler,
QueueCommand,
queue_command
},
},
static_content::{
room::Direction,
},
models::{
item::DoorState,
},
services::comms::broadcast_to_room, services::comms::broadcast_to_room,
static_content::room::Direction,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::time; use std::time;
@ -25,11 +15,14 @@ use std::time;
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let direction = match command { let direction = match command {
QueueCommand::CloseDoor { direction } => direction, QueueCommand::CloseDoor { direction } => direction,
_ => user_error("Unexpected queued command".to_owned())? _ => user_error("Unexpected queued command".to_owned())?,
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let use_location = if player_item.death_data.is_some() { let use_location = if player_item.death_data.is_some() {
@ -39,9 +32,14 @@ impl QueueCommandHandler for QueueHandler {
}; };
match is_door_in_direction(&ctx.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 close.".to_owned())?, DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?,
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } | DoorSituation::DoorIntoRoom {
DoorSituation::DoorOutOfRoom { state: DoorState { open: false, .. }, .. } => state: DoorState { open: false, .. },
user_error("The door is already closed.".to_owned())?, ..
}
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: false, .. },
..
} => user_error("The door is already closed.".to_owned())?,
_ => {} _ => {}
} }
@ -49,11 +47,14 @@ impl QueueCommandHandler for QueueHandler {
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let direction = match command { let direction = match command {
QueueCommand::CloseDoor { direction } => direction, QueueCommand::CloseDoor { direction } => direction,
_ => user_error("Unexpected queued command".to_owned())? _ => user_error("Unexpected queued command".to_owned())?,
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let use_location = if player_item.death_data.is_some() { let use_location = if player_item.death_data.is_some() {
@ -61,12 +62,22 @@ impl QueueCommandHandler for QueueHandler {
} else { } else {
&player_item.location &player_item.location
}; };
let (room_1, dir_in_room, room_2) = match is_door_in_direction(&ctx.trans, &direction, use_location).await? { let (room_1, dir_in_room, room_2) =
match is_door_in_direction(&ctx.trans, &direction, use_location).await? {
DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?, DoorSituation::NoDoor => user_error("There is no door to close.".to_owned())?,
DoorSituation::DoorIntoRoom { state: DoorState { open: false, .. }, .. } | DoorSituation::DoorIntoRoom {
DoorSituation::DoorOutOfRoom { state: DoorState { open: false, .. }, .. } => state: DoorState { open: false, .. },
user_error("The door is already closed.".to_owned())?, ..
DoorSituation::DoorIntoRoom { room_with_door, current_room, .. } => { }
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: false, .. },
..
} => user_error("The door is already closed.".to_owned())?,
DoorSituation::DoorIntoRoom {
room_with_door,
current_room,
..
} => {
if let Some(revdir) = direction.reverse() { if let Some(revdir) = direction.reverse() {
let mut entering_room_mut = (*room_with_door).clone(); 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_map) = entering_room_mut.door_states.as_mut() {
@ -79,8 +90,12 @@ impl QueueCommandHandler for QueueHandler {
} else { } else {
user_error("There's no door possible there.".to_owned())? user_error("There's no door possible there.".to_owned())?
} }
}, }
DoorSituation::DoorOutOfRoom { room_with_door, new_room, .. } => { DoorSituation::DoorOutOfRoom {
room_with_door,
new_room,
..
} => {
let mut entering_room_mut = (*room_with_door).clone(); 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_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&direction) { if let Some(door) = door_map.get_mut(&direction) {
@ -92,28 +107,40 @@ impl QueueCommandHandler for QueueHandler {
} }
}; };
for (loc, dir) in [(&room_1.refstr(), &dir_in_room.describe()), for (loc, dir) in [
(&room_2.refstr(), &dir_in_room.reverse().map(|d| d.describe()) (&room_1.refstr(), &dir_in_room.describe()),
.unwrap_or_else(|| "outside".to_owned()))] { (
&room_2.refstr(),
&dir_in_room
.reverse()
.map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned()),
),
] {
broadcast_to_room( broadcast_to_room(
&ctx.trans, &ctx.trans,
loc, loc,
None, None,
&format!("{} closes the door to the {}.\n", &format!(
"{} closes the door to the {}.\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
dir dir
), ),
Some( Some(&format!(
&format!("{} closes the door to the {}.\n", "{} closes the door to the {}.\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
dir dir
)),
) )
) .await?;
).await?;
} }
ctx.trans.delete_task("SwingShut", ctx.trans
&format!("{}/{}", &room_1.refstr(), &dir_in_room.describe())).await?; .delete_task(
"SwingShut",
&format!("{}/{}", &room_1.refstr(), &dir_in_room.describe()),
)
.await?;
Ok(()) Ok(())
} }
@ -122,10 +149,21 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
let dir = Direction::parse(remaining) self: &Self,
.ok_or_else(|| UserError("Unknown direction".to_owned()))?; ctx: &mut VerbContext,
queue_command(ctx, &QueueCommand::CloseDoor { direction: dir.clone() }).await?; _verb: &str,
remaining: &str,
) -> UResult<()> {
let dir =
Direction::parse(remaining).ok_or_else(|| UserError("Unknown direction".to_owned()))?;
queue_command(
ctx,
&QueueCommand::CloseDoor {
direction: dir.clone(),
},
)
.await?;
Ok(()) Ok(())
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,179 +1,267 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, use super::{
get_player_item_or_fail, user_error, search_item_for_user}; get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
use async_trait::async_trait; UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
models::{
item::{Item, DeathData, SkillType},
},
db::ItemSearchParams, db::ItemSearchParams,
static_content::possession_type::{possession_data, can_butcher_possessions},
language::join_words, language::join_words,
models::item::{DeathData, Item, SkillType},
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{ services::{
capacity::{check_item_capacity, CapacityLevel},
combat::corpsify_item,
comms::broadcast_to_room,
destroy_container, destroy_container,
skills::skill_check_and_grind, skills::skill_check_and_grind,
comms::broadcast_to_room,
combat::corpsify_item,
capacity::{CapacityLevel, check_item_capacity}},
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
}, },
static_content::possession_type::{can_butcher_possessions, possession_data},
}; };
use ansi::ansi; use ansi::ansi;
use std::{time, sync::Arc}; use async_trait::async_trait;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; use std::{sync::Arc, time};
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You butcher things while they are dead, not while YOU are dead!".to_owned())?; 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 command {
QueueCommand::Cut { from_corpse, what_part } => (from_corpse, what_part), QueueCommand::Cut {
_ => user_error("Unexpected command".to_owned())? from_corpse,
what_part,
} => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?,
}; };
let corpse = match ctx.trans.find_item_by_type_code("corpse", &from_corpse_id).await? { let corpse = match ctx
.trans
.find_item_by_type_code("corpse", &from_corpse_id)
.await?
{
None => user_error("The corpse seems to be gone".to_owned())?, None => user_error("The corpse seems to be gone".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if corpse.location != player_item.location { if corpse.location != player_item.location {
user_error( user_error(format!(
format!("You try to cut {} but realise it is no longer there.", "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(!ctx.session_dat.less_explicit_mode, 1, false)
) ))?
)?
} }
ensure_has_butcher_tool(&ctx.trans, &player_item).await?; ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
match corpse.death_data.as_ref() { match corpse.death_data.as_ref() {
None => user_error(format!("You can't do that while {} is still alive!", corpse.pronouns.subject))?, None => user_error(format!(
Some(DeathData { parts_remaining, ..}) => "You can't do that while {} is still alive!",
if !parts_remaining.iter().any( corpse.pronouns.subject
|pt| possession_data().get(pt) ))?,
Some(DeathData {
parts_remaining, ..
}) => {
if !parts_remaining.iter().any(|pt| {
possession_data()
.get(pt)
.map(|pd| &pd.display == &what_part) .map(|pd| &pd.display == &what_part)
== Some(true)) { == Some(true)
user_error(format!("That part is now gone. Parts you can cut: {}", }) {
&join_words(&parts_remaining.iter().filter_map(|pt| possession_data().get(pt)) user_error(format!(
.map(|pd| pd.display).collect::<Vec<&'static str>>()) "That part is now gone. Parts you can cut: {}",
&join_words(
&parts_remaining
.iter()
.filter_map(|pt| possession_data().get(pt))
.map(|pd| pd.display)
.collect::<Vec<&'static str>>()
)
))?; ))?;
} }
}
}; };
let msg_exp = format!("{} prepares to cut {} from {}\n", let msg_exp = format!(
"{} prepares to cut {} from {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&what_part, &what_part,
&corpse.display_for_sentence(true, 1, false)); &corpse.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} prepares to cut {} from {}\n", );
let msg_nonexp = format!(
"{} prepares to cut {} from {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&what_part, &what_part,
&corpse.display_for_sentence(false, 1, false)); &corpse.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You butcher things while they are dead, not while YOU are dead!".to_owned())?; 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 command {
QueueCommand::Cut { from_corpse, what_part } => (from_corpse, what_part), QueueCommand::Cut {
_ => user_error("Unexpected command".to_owned())? 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, &player_item).await?;
let corpse = match ctx.trans.find_item_by_type_code("corpse", &from_corpse_id).await? { let corpse = match ctx
.trans
.find_item_by_type_code("corpse", &from_corpse_id)
.await?
{
None => user_error("The corpse seems to be gone".to_owned())?, None => user_error("The corpse seems to be gone".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if corpse.location != player_item.location { if corpse.location != player_item.location {
user_error( user_error(format!(
format!("You try to cut {} but realise it is no longer there.", "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(!ctx.session_dat.less_explicit_mode, 1, false)
) ))?
)?
} }
let possession_type = match corpse.death_data.as_ref() { let possession_type = match corpse.death_data.as_ref() {
None => user_error(format!("You can't do that while {} is still alive!", corpse.pronouns.subject))?, None => user_error(format!(
Some(DeathData { parts_remaining, ..}) => "You can't do that while {} is still alive!",
parts_remaining.iter().find( corpse.pronouns.subject
|pt| possession_data().get(pt) ))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining
.iter()
.find(|pt| {
possession_data()
.get(pt)
.map(|pd| &pd.display == &what_part) .map(|pd| &pd.display == &what_part)
== Some(true)).ok_or_else( == Some(true)
|| UserError(format!("Parts you can cut: {}", })
&join_words(&parts_remaining.iter().filter_map(|pt| possession_data().get(pt)) .ok_or_else(|| {
.map(|pd| pd.display).collect::<Vec<&'static str>>()) UserError(format!(
)))? "Parts you can cut: {}",
&join_words(
&parts_remaining
.iter()
.filter_map(|pt| possession_data().get(pt))
.map(|pd| pd.display)
.collect::<Vec<&'static str>>()
)
))
})?,
}; };
let possession_data = possession_data().get(possession_type) let possession_data = possession_data()
.get(possession_type)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?; .ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
let mut corpse_mut = (*corpse).clone(); let mut corpse_mut = (*corpse).clone();
match corpse_mut.death_data.as_mut() { match corpse_mut.death_data.as_mut() {
None => {}, None => {}
Some(dd) => { Some(dd) => {
dd.parts_remaining = dd.parts_remaining = dd
dd
.parts_remaining .parts_remaining
.iter().take_while(|pt| pt != &possession_type) .iter()
.chain(dd.parts_remaining.iter().skip_while(|pt| pt != &possession_type).skip(1)) .take_while(|pt| pt != &possession_type)
.chain(
dd.parts_remaining
.iter()
.skip_while(|pt| pt != &possession_type)
.skip(1),
)
.map(|pt| (*pt).clone()) .map(|pt| (*pt).clone())
.collect() .collect()
} }
} }
match check_item_capacity(&ctx.trans, &player_item.refstr(), possession_data.weight).await? { match check_item_capacity(&ctx.trans, &player_item.refstr(), possession_data.weight).await?
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("You have too much stuff to take that on!".to_owned())?, CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("You have too much stuff to take that on!".to_owned())?
}
_ => {} _ => {}
} }
if corpse_mut.death_data.as_ref().map(|dd| dd.parts_remaining.is_empty()) == Some(true) { if corpse_mut
.death_data
.as_ref()
.map(|dd| dd.parts_remaining.is_empty())
== Some(true)
{
destroy_container(&ctx.trans, &corpse_mut).await?; destroy_container(&ctx.trans, &corpse_mut).await?;
} else { } else {
ctx.trans.save_item_model(&corpse_mut).await?; ctx.trans.save_item_model(&corpse_mut).await?;
} }
let mut player_item_mut = (*player_item).clone(); 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, &mut player_item_mut, &SkillType::Craft, 10.0).await?
broadcast_to_room(&ctx.trans, &player_item.location, None, < 0.0
&format!("{} tries to cut the {} from {}, but only leaves a mutilated mess.\n", {
broadcast_to_room(
&ctx.trans,
&player_item.location,
None,
&format!(
"{} tries to cut the {} from {}, but only leaves a mutilated mess.\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
possession_data.display, possession_data.display,
corpse.display_for_sentence(true, 1, false) corpse.display_for_sentence(true, 1, false)
), ),
Some(&format!("{} tries to cut the {} from {}, but only leaves a mutilated mess.\n", Some(&format!(
"{} tries to cut the {} from {}, but only leaves a mutilated mess.\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
possession_data.display, possession_data.display,
corpse.display_for_sentence(true, 1, false) corpse.display_for_sentence(true, 1, false)
)) )),
).await?; )
.await?;
} else { } else {
let mut new_item: Item = (*possession_type).clone().into(); let mut new_item: Item = (*possession_type).clone().into();
new_item.item_code = format!("{}", ctx.trans.alloc_item_code().await?); new_item.item_code = format!("{}", ctx.trans.alloc_item_code().await?);
new_item.location = player_item.refstr(); new_item.location = player_item.refstr();
ctx.trans.save_item_model(&new_item).await?; ctx.trans.save_item_model(&new_item).await?;
broadcast_to_room(&ctx.trans, &player_item.location, None, broadcast_to_room(
&format!("{} expertly cuts the {} from {}.\n", &ctx.trans,
&player_item.location,
None,
&format!(
"{} expertly cuts the {} from {}.\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
possession_data.display, possession_data.display,
corpse.display_for_sentence(true, 1, false) corpse.display_for_sentence(true, 1, false)
), ),
Some(&format!("{} expertly cuts the {} from {}.\n", Some(&format!(
"{} expertly cuts the {} from {}.\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
possession_data.display, possession_data.display,
corpse.display_for_sentence(true, 1, false) corpse.display_for_sentence(true, 1, false)
)) )),
).await?; )
.await?;
} }
ctx.trans.save_item_model(&player_item_mut).await?; ctx.trans.save_item_model(&player_item_mut).await?;
@ -183,7 +271,11 @@ impl QueueCommandHandler for QueueHandler {
} }
pub async fn ensure_has_butcher_tool(trans: &DBTrans, player_item: &Item) -> UResult<()> { pub async fn ensure_has_butcher_tool(trans: &DBTrans, player_item: &Item) -> UResult<()> {
if trans.count_matching_possessions(&player_item.refstr(), &can_butcher_possessions()).await? < 1 { if trans
.count_matching_possessions(&player_item.refstr(), &can_butcher_possessions())
.await?
< 1
{
user_error("You have nothing sharp on you suitable for butchery!".to_owned())?; user_error("You have nothing sharp on you suitable for butchery!".to_owned())?;
} }
Ok(()) Ok(())
@ -192,38 +284,67 @@ pub async fn ensure_has_butcher_tool(trans: &DBTrans, player_item: &Item) -> URe
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (what_raw, corpse_raw) = match remaining.split_once(" from ") { let (what_raw, corpse_raw) = match remaining.split_once(" from ") {
None => user_error(ansi!("Usage: <bold>cut<reset> thing <bold>from<reset> corpse").to_owned())?, None => user_error(
Some(v) => v ansi!("Usage: <bold>cut<reset> thing <bold>from<reset> corpse").to_owned(),
)?,
Some(v) => v,
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You butcher things while they are dead, not while YOU are dead!".to_owned())? user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?
} }
let possible_corpse = search_item_for_user(ctx, &ItemSearchParams { let possible_corpse = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
dead_first: true, dead_first: true,
..ItemSearchParams::base(&player_item, corpse_raw.trim()) ..ItemSearchParams::base(&player_item, corpse_raw.trim())
}).await?; },
)
.await?;
let what_norm = what_raw.trim().to_lowercase(); let what_norm = what_raw.trim().to_lowercase();
let possession_type = match possible_corpse.death_data.as_ref() { let possession_type = match possible_corpse.death_data.as_ref() {
None => user_error(format!("You can't do that while {} is still alive!", possible_corpse.pronouns.subject))?, None => user_error(format!(
Some(DeathData { parts_remaining, ..}) => "You can't do that while {} is still alive!",
parts_remaining.iter().find( possible_corpse.pronouns.subject
|pt| possession_data().get(pt) ))?,
.map(|pd| pd.display.to_lowercase() == what_norm || Some(DeathData {
pd.aliases.iter().any(|a| a.to_lowercase() == what_norm)) parts_remaining, ..
== Some(true)).ok_or_else( }) => parts_remaining
|| UserError(format!("Parts you can cut: {}", .iter()
&join_words(&parts_remaining.iter().filter_map(|pt| possession_data().get(pt)) .find(|pt| {
.map(|pd| pd.display).collect::<Vec<&'static str>>()) possession_data().get(pt).map(|pd| {
)))? pd.display.to_lowercase() == what_norm
}.clone(); || pd.aliases.iter().any(|a| a.to_lowercase() == what_norm)
}) == Some(true)
})
.ok_or_else(|| {
UserError(format!(
"Parts you can cut: {}",
&join_words(
&parts_remaining
.iter()
.filter_map(|pt| possession_data().get(pt))
.map(|pd| pd.display)
.collect::<Vec<&'static str>>()
)
))
})?,
}
.clone();
let corpse = if possible_corpse.item_type == "corpse" { let corpse = if possible_corpse.item_type == "corpse" {
possible_corpse possible_corpse
@ -233,19 +354,27 @@ impl UserVerb for Verb {
"room/valhalla" "room/valhalla"
} else { } else {
"room/repro_xv_respawn" "room/repro_xv_respawn"
}.to_owned(); }
.to_owned();
Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?) Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?)
} else { } else {
user_error("You can't butcher that!".to_owned())? user_error("You can't butcher that!".to_owned())?
}; };
let possession_data = possession_data().get(&possession_type) let possession_data = possession_data()
.get(&possession_type)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?; .ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
ensure_has_butcher_tool(&ctx.trans, &player_item).await?; ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
queue_command(ctx, &QueueCommand::Cut { from_corpse: corpse.item_code.clone(), queue_command(
what_part: possession_data.display.to_owned() }).await?; ctx,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -0,0 +1,202 @@
use std::collections::BTreeMap;
use super::{
get_player_item_or_fail, get_user_or_fail, look, rent::recursively_destroy_or_move_item,
user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
models::task::{Task, TaskDetails, TaskMeta},
regular_tasks::{TaskHandler, TaskRunContext},
services::{
combat::{corpsify_item, handle_death},
destroy_container,
skills::calculate_total_stats_skills_for_user,
},
DResult,
};
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
use rand::{distributions::Alphanumeric, Rng};
use std::time;
async fn verify_code(
ctx: &mut VerbContext<'_>,
input: &str,
base_command: &str,
action_text: &str,
) -> UResult<bool> {
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
let code = match &user_dat.danger_code {
None => {
let new_code = rand::thread_rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect::<String>();
user_dat.danger_code = Some(new_code.clone());
new_code
}
Some(code) => code.clone(),
};
let input_tr = input.trim();
if input_tr == "" || !input_tr.starts_with("code ") {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("To verify you want to {}, type <bold>delete {} code {}<reset>\n"),
action_text, base_command, code
)),
)
.await?;
ctx.trans.save_user_model(&user_dat).await?;
return Ok(false);
}
if input_tr["code ".len()..].trim() != code {
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!(
"Your confirmation code didn't match! \
To verify you want to {}, type <bold>delete {} code {}<reset>\n"
),
action_text, base_command, code
)),
)
.await?;
ctx.trans.save_user_model(&user_dat).await?;
return Ok(false);
}
Ok(true)
}
async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> {
let mut player_item = (*get_player_item_or_fail(ctx).await?).clone();
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
handle_death(&ctx.trans, &mut player_item).await?;
corpsify_item(&ctx.trans, &player_item).await?;
player_item.death_data = None;
player_item.location = "room/repro_xv_chargen".to_owned();
player_item.total_xp = ((player_item.total_xp as i64)
- user_dat.experience.xp_change_for_this_reroll)
.max(0) as u64;
user_dat.experience.xp_change_for_this_reroll = 0;
user_dat.raw_stats = BTreeMap::new();
user_dat.raw_skills = BTreeMap::new();
calculate_total_stats_skills_for_user(&mut player_item, &user_dat);
ctx.trans.save_user_model(&user_dat).await?;
ctx.trans.save_item_model(&player_item).await?;
look::VERB.handle(ctx, "look", "").await?;
Ok(())
}
#[derive(Clone)]
pub struct DestroyUserHandler;
pub static DESTROY_USER_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DestroyUserHandler;
#[async_trait]
impl TaskHandler for DestroyUserHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let username = match &ctx.task.details {
TaskDetails::DestroyUser { username } => username.clone(),
_ => {
return Ok(None);
}
};
let _user = match ctx.trans.find_by_username(&username).await? {
None => return Ok(None),
Some(u) => u,
};
let player_item = match ctx
.trans
.find_item_by_type_code("player", &username.to_lowercase())
.await?
{
None => return Ok(None),
Some(p) => p,
};
destroy_container(&ctx.trans, &player_item).await?;
for dynzone in ctx.trans.find_dynzone_for_user(&username).await? {
recursively_destroy_or_move_item(ctx, &dynzone).await?;
}
ctx.trans.delete_user(&username).await?;
Ok(None)
}
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let rtrim = remaining.trim();
let username = get_user_or_fail(ctx)?.username.clone();
if rtrim.starts_with("character forever") {
if !verify_code(ctx, &rtrim["character forever".len()..], "character forever",
"permanently destroy your character (after a one week reflection period), making the name available for other players").await? {
return Ok(());
}
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: username.clone(),
next_scheduled: Utc::now() + chrono::Duration::days(7),
..Default::default()
},
details: TaskDetails::DestroyUser { username },
})
.await?;
ctx.trans
.queue_for_session(
ctx.session,
Some(
"Puny human, your permanent destruction has been scheduled \
for one week from now! If you change your mind, just log \
in again before the week is up. After one week, your username \
will be available for others to take, and you will need to start \
again with a new character. This character will count towards the \
character limit for the week, but will not once it is deleted. \
Goodbye forever!\n",
),
)
.await?;
ctx.trans.queue_for_session(ctx.session, None).await?;
} else if rtrim.starts_with("stats") {
if !verify_code(ctx, &rtrim["stats".len()..], "stats",
"kill your character, reset your stats and non-journal XP, and pick new stats to reclone with").await? {
return Ok(());
}
reset_stats(ctx).await?;
} else {
user_error(
ansi!("Try <bold>delete character forever<reset> or <bold>delete stats<reset>")
.to_owned(),
)?
}
let user_dat = ctx
.user_dat
.as_mut()
.ok_or(UserError("Please log in first".to_owned()))?;
user_dat.danger_code = None;
ctx.trans.save_user_model(&user_dat).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -1,19 +1,19 @@
use super::{ use super::{
get_player_item_or_fail, parsing::parse_to_space, user_error, UResult, UserVerb, UserVerbRef,
VerbContext, VerbContext,
UserVerb,
UserVerbRef,
UResult,
parsing::parse_to_space,
user_error,
get_player_item_or_fail
}; };
use async_trait::async_trait;
use ansi::{ansi, ignore_special_characters}; use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (me, remaining) = parse_to_space(remaining); let (me, remaining) = parse_to_space(remaining);
let (as_word, remaining) = parse_to_space(remaining); let (as_word, remaining) = parse_to_space(remaining);
let remaining = ignore_special_characters(remaining.trim()); let remaining = ignore_special_characters(remaining.trim());
@ -22,17 +22,28 @@ impl UserVerb for Verb {
} }
if remaining.len() < 40 { if remaining.len() < 40 {
user_error(format!("That's too short by {} characters.", 40 - remaining.len()))?; user_error(format!(
"That's too short by {} characters.",
40 - remaining.len()
))?;
} }
if remaining.len() > 255 { if remaining.len() > 255 {
user_error(format!("That's too short by {} characters.", remaining.len() - 255))?; user_error(format!(
"That's too short by {} characters.",
remaining.len() - 255
))?;
} }
let mut item = (*get_player_item_or_fail(ctx).await?).clone(); let mut item = (*get_player_item_or_fail(ctx).await?).clone();
item.details = Some(remaining); item.details = Some(remaining);
ctx.trans.save_item_model(&item).await?; ctx.trans.save_item_model(&item).await?;
ctx.trans.queue_for_session(ctx.session, Some(ansi!("<green>Character description updated.<reset>\n"))).await?; ctx.trans
.queue_for_session(
ctx.session,
Some(ansi!("<green>Character description updated.<reset>\n")),
)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,44 +1,30 @@
use super::{ use super::{
VerbContext, get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
UserVerb, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_items_for_user,
parsing::parse_count,
}; };
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
static_content::possession_type::possession_data, models::{
regular_tasks::{ item::{Item, ItemFlag, LocationActionType},
queued_command::{ task::{Task, TaskDetails, TaskMeta},
QueueCommandHandler,
QueueCommand,
queue_command
}, },
TaskHandler, regular_tasks::{
TaskRunContext, queued_command::{queue_command, QueueCommand, QueueCommandHandler},
TaskHandler, TaskRunContext,
}, },
services::{ services::{
capacity::{check_item_capacity, CapacityLevel},
comms::broadcast_to_room, comms::broadcast_to_room,
capacity::{
check_item_capacity,
CapacityLevel,
}
}, },
static_content::possession_type::possession_data,
DResult, DResult,
models::{
item::{LocationActionType, Item, ItemFlag},
task::{Task, TaskMeta, TaskDetails},
},
}; };
use async_trait::async_trait;
use std::time;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; use std::time;
pub struct ExpireItemTaskHandler; pub struct ExpireItemTaskHandler;
#[async_trait] #[async_trait]
@ -46,17 +32,21 @@ impl TaskHandler for ExpireItemTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let item_code = match &mut ctx.task.details { let item_code = match &mut ctx.task.details {
TaskDetails::ExpireItem { item_code } => item_code, TaskDetails::ExpireItem { item_code } => item_code,
_ => Err("Expected ExpireItem type")? _ => Err("Expected ExpireItem type")?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", item_code).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", item_code)
.await?
{
None => { None => {
return Ok(None); return Ok(None);
} }
Some(it) => it Some(it) => it,
}; };
let (loc_type, loc_code) = match item.location.split_once("/") { let (loc_type, loc_code) = match item.location.split_once("/") {
None => return Ok(None), None => return Ok(None),
Some(p) => p Some(p) => p,
}; };
if loc_type != "room" { if loc_type != "room" {
@ -65,7 +55,7 @@ impl TaskHandler for ExpireItemTaskHandler {
let loc_item = match ctx.trans.find_item_by_type_code(loc_type, loc_code).await? { let loc_item = match ctx.trans.find_item_by_type_code(loc_type, loc_code).await? {
None => return Ok(None), None => return Ok(None),
Some(i) => i Some(i) => i,
}; };
if loc_item.flags.contains(&ItemFlag::DroppedItemsDontExpire) { if loc_item.flags.contains(&ItemFlag::DroppedItemsDontExpire) {
@ -82,34 +72,34 @@ pub static EXPIRE_ITEM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &Expi
pub async fn consider_expire_job_for_item(trans: &DBTrans, item: &Item) -> DResult<()> { pub async fn consider_expire_job_for_item(trans: &DBTrans, item: &Item) -> DResult<()> {
let (loc_type, loc_code) = match item.location.split_once("/") { let (loc_type, loc_code) = match item.location.split_once("/") {
None => return Ok(()), None => return Ok(()),
Some(p) => p Some(p) => p,
}; };
if loc_type != "room" { if loc_type != "room" {
return Ok(()) return Ok(());
} }
let loc_item = match trans.find_item_by_type_code(loc_type, loc_code).await? { let loc_item = match trans.find_item_by_type_code(loc_type, loc_code).await? {
None => return Ok(()), None => return Ok(()),
Some(i) => i Some(i) => i,
}; };
if loc_item.flags.contains(&ItemFlag::DroppedItemsDontExpire) { if loc_item.flags.contains(&ItemFlag::DroppedItemsDontExpire) {
return Ok(()); return Ok(());
} }
trans.upsert_task( trans
&Task { .upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: format!("{}/{}", item.item_type, item.item_code), task_code: format!("{}/{}", item.item_type, item.item_code),
next_scheduled: Utc::now() + chrono::Duration::hours(1), next_scheduled: Utc::now() + chrono::Duration::hours(1),
..Default::default() ..Default::default()
}, },
details: TaskDetails::ExpireItem { details: TaskDetails::ExpireItem {
item_code: item.item_code.clone() item_code: item.item_code.clone(),
} },
} })
).await?; .await?;
Ok(()) Ok(())
} }
@ -117,87 +107,135 @@ pub async fn consider_expire_job_for_item(trans: &DBTrans, item: &Item) -> DResu
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to drop it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Drop { possession_id } => possession_id, QueueCommand::Drop { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != format!("{}/{}", &player_item.item_type, &player_item.item_code) { if item.location != format!("{}/{}", &player_item.item_type, &player_item.item_code) {
user_error( user_error(format!(
format!("You try to drop {} but realise you no longer have it", "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.session_dat.less_explicit_mode, 1, false)
) ))?
)?
} }
if item.action_type == LocationActionType::Worn { if item.action_type == LocationActionType::Worn {
user_error( user_error(
ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned())?; ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned(),
)?;
} }
let msg_exp = format!("{} prepares to drop {}\n", let msg_exp = format!(
"{} prepares to drop {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} prepares to drop {}\n", );
let msg_nonexp = format!(
"{} prepares to drop {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to get it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Drop { possession_id } => possession_id, QueueCommand::Drop { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != format!("{}/{}", &player_item.item_type, &player_item.item_code) { if item.location != format!("{}/{}", &player_item.item_type, &player_item.item_code) {
user_error(format!("You try to drop {} but realise you no longer have it!", user_error(format!(
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? "You try to drop {} but realise you no longer have it!",
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))?
} }
if item.action_type == LocationActionType::Worn { if item.action_type == LocationActionType::Worn {
user_error( user_error(
ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned())?; ansi!("You're wearing it - try using <bold>remove<reset> first").to_owned(),
)?;
} }
let possession_data = match item.possession_type.as_ref().and_then(|pt| possession_data().get(&pt)) { let possession_data = match item
None => user_error("That item no longer exists in the game so can't be handled".to_owned())?, .possession_type
Some(pd) => pd .as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
}; };
match check_item_capacity(ctx.trans, &player_item.location, possession_data.weight).await? { match check_item_capacity(ctx.trans, &player_item.location, possession_data.weight).await? {
CapacityLevel::AboveItemLimit => user_error( CapacityLevel::AboveItemLimit => user_error(format!(
format!("You can't drop {}, because it is so cluttered here there is no where to put it!", "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.session_dat.less_explicit_mode, 1, false)
), ))?,
)?, _ => (),
_ => ()
} }
let msg_exp = format!("{} drops {}\n", let msg_exp = format!(
"{} drops {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} drops {}\n", );
let msg_nonexp = format!(
"{} drops {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
item_mut.location = player_item.location.clone(); item_mut.location = player_item.location.clone();
consider_expire_job_for_item(ctx.trans, &item_mut).await?; consider_expire_job_for_item(ctx.trans, &item_mut).await?;
@ -210,7 +248,12 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, mut remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1); let mut get_limit = Some(1);
@ -221,22 +264,34 @@ impl UserVerb for Verb {
get_limit = Some(n); get_limit = Some(n);
remaining = remaining2; remaining = remaining2;
} }
let targets = search_items_for_user(ctx, &ItemSearchParams { let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
item_type_only: Some("possession"), item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100), limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining) ..ItemSearchParams::base(&player_item, &remaining)
}).await?; },
)
.await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to drop it, but your ghostly hands slip through it uselessly".to_owned())?; user_error(
"You try to drop it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
} }
for target in targets { for target in targets {
if target.item_type != "possession" { if target.item_type != "possession" {
user_error("You can't drop that!".to_owned())?; user_error("You can't drop that!".to_owned())?;
} }
queue_command(ctx, &QueueCommand::Drop { possession_id: target.item_code.clone() }).await?; queue_command(
ctx,
&QueueCommand::Drop {
possession_id: target.item_code.clone(),
},
)
.await?;
} }
Ok(()) Ok(())
} }

View File

@ -1,29 +1,15 @@
use super::{ use super::{
VerbContext, get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
UserVerb, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_items_for_user,
parsing::parse_count
}; };
use crate::{ use crate::{
static_content::possession_type::possession_data,
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
services::{
comms::broadcast_to_room,
capacity::{
check_item_capacity,
CapacityLevel,
}
},
models::item::LocationActionType, models::item::LocationActionType,
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{
capacity::{check_item_capacity, CapacityLevel},
comms::broadcast_to_room,
},
static_content::possession_type::possession_data,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::time; use std::time;
@ -31,80 +17,133 @@ use std::time;
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to get it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Get { possession_id } => possession_id, QueueCommand::Get { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != player_item.location { if item.location != player_item.location {
user_error( user_error(format!(
format!("You try to get {} but realise it is no longer there", "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.session_dat.less_explicit_mode, 1, false)
) ))?
)?
} }
let msg_exp = format!("{} fumbles around trying to pick up {}\n", let msg_exp = format!(
"{} fumbles around trying to pick up {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} fumbles around trying to pick up {}\n", );
let msg_nonexp = format!(
"{} fumbles around trying to pick up {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to get it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Get { possession_id } => possession_id, QueueCommand::Get { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != player_item.location { if item.location != player_item.location {
user_error(format!("You try to get {} but realise it is no longer there", user_error(format!(
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? "You try to get {} but realise it is no longer there",
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))?
} }
let possession_data = match item.possession_type.as_ref().and_then(|pt| possession_data().get(&pt)) { let possession_data = match item
None => user_error("That item no longer exists in the game so can't be handled".to_owned())?, .possession_type
Some(pd) => pd .as_ref()
.and_then(|pt| possession_data().get(&pt))
{
None => {
user_error("That item no longer exists in the game so can't be handled".to_owned())?
}
Some(pd) => pd,
}; };
let player_as_loc = format!("{}/{}", &player_item.item_type, &player_item.item_code); let player_as_loc = format!("{}/{}", &player_item.item_type, &player_item.item_code);
match check_item_capacity(ctx.trans, &player_as_loc, possession_data.weight).await? { 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::AboveItemLimit => {
user_error("You just can't hold that many things!".to_owned())?
}
CapacityLevel::OverBurdened => user_error(format!( CapacityLevel::OverBurdened => user_error(format!(
"{} You drop {} because it is too heavy!", "{} You drop {} because it is too heavy!",
if ctx.session_dat.less_explicit_mode { "Rats!" } else { "Fuck!" }, if ctx.session_dat.less_explicit_mode {
"Rats!"
} else {
"Fuck!"
},
&player_item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) &player_item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))?, ))?,
_ => () _ => (),
} }
let msg_exp = format!("{} picks up {}\n", let msg_exp = format!(
"{} picks up {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} picks up {}\n", );
let msg_nonexp = format!(
"{} picks up {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
item_mut.location = player_as_loc; item_mut.location = player_as_loc;
item_mut.action_type = LocationActionType::Normal; item_mut.action_type = LocationActionType::Normal;
@ -116,7 +155,12 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, mut remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
// TODO: Parse "get target from container" variant // TODO: Parse "get target from container" variant
let mut get_limit = Some(1); let mut get_limit = Some(1);
@ -127,23 +171,38 @@ impl UserVerb for Verb {
get_limit = Some(n); get_limit = Some(n);
remaining = remaining2; remaining = remaining2;
} }
let targets = search_items_for_user(ctx, &ItemSearchParams { let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
item_type_only: Some("possession"), item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100), limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining) ..ItemSearchParams::base(&player_item, &remaining)
}).await?; },
)
.await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to get it, but your ghostly hands slip through it uselessly".to_owned())?; user_error(
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
} }
let mut did_anything: bool = false; let mut did_anything: bool = false;
for target in targets.iter().filter(|t| t.action_type.is_visible_in_look()) { for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type != "possession" { if target.item_type != "possession" {
user_error("You can't get that!".to_owned())?; user_error("You can't get that!".to_owned())?;
} }
did_anything = true; did_anything = true;
queue_command(ctx, &QueueCommand::Get { possession_id: target.item_code.clone() }).await?; queue_command(
ctx,
&QueueCommand::Get {
possession_id: target.item_code.clone(),
},
)
.await?;
} }
if !did_anything { if !did_anything {
user_error("I didn't find anything matching.".to_owned())?; user_error("I didn't find anything matching.".to_owned())?;

View File

@ -1,9 +1,6 @@
use super::{ use super::{CommandHandlingError::UserError, UResult, UserVerb, UserVerbRef, VerbContext};
VerbContext, UserVerb, UserVerbRef, UResult,
CommandHandlingError::UserError
};
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
use phf::phf_map; use phf::phf_map;
static ALWAYS_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { static ALWAYS_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
@ -114,6 +111,7 @@ static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
\t<bold>buy<reset> item to buy something from the shop you are in.\n\ \t<bold>buy<reset> item to buy something from the shop you are in.\n\
\t<bold>list<reset> to see the price list for the stop.\n\ \t<bold>list<reset> to see the price list for the stop.\n\
\t<bold>wield<reset> to hold a weapon in your inventory for use when you attack.\n\ \t<bold>wield<reset> to hold a weapon in your inventory for use when you attack.\n\
\t<bold>gear<reset> to see what you are wearing, and how much protection it offers.\n\
Hint: get and drop support an item name, but you can also prefix it \ Hint: get and drop support an item name, but you can also prefix it \
with a number - e.g. <bold>get 2 whip<reset>. Instead of a number, you can \ with a number - e.g. <bold>get 2 whip<reset>. Instead of a number, you can \
use <bold>all<reset>. You can also omit the item name to match any \ use <bold>all<reset>. You can also omit the item name to match any \
@ -230,6 +228,20 @@ static REGISTERED_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
ansi!("<bold>install<reset> item <bold>on door to<reset> direction\tInstalls hardware such as a lock on a door."), ansi!("<bold>install<reset> item <bold>on door to<reset> direction\tInstalls hardware such as a lock on a door."),
"uninstall" => "uninstall" =>
ansi!("<bold>uninstall<reset> item <bold>from door to<reset> direction\tRemoves installed hardware such as a lock on a door."), ansi!("<bold>uninstall<reset> item <bold>from door to<reset> direction\tRemoves installed hardware such as a lock on a door."),
"gear" => ansi!("<bold>gear<reset>\tSee equipment you have on, and what protection it offers you."),
"delete" => ansi!("Delete is the most destructive command in the game - used to reset your character, or even give it up \
forever. All commands below echo out a command including a single-use command you can use to confirm the \
command - they don't take effect without the code. This prevents accidentally pasting in dangerous commands.\n\
<bold>delete character forever<reset>\tDestroy your character permanently. The command starts a one week \
countdown at the end of which your account and character (and anything in their possession) are permanently \
destroyed (as in we irreversibly wipe your data from the server), and any places you are renting get evicted. \
The command disconnects you immediately. If you reconnect and log in again within the week, \
the countdown is stopped and the deletion aborted. After the one week, anyone can register again with the \
same username. At the end of the week, when your character is destroyed, it no longer counts towards the limit \
of 5 usernames per person.\n\
<bold>delete stats<reset>\tKills your character instantly (leaving anything you are carrying on your corpse) \
and sends you back to the choice room to pick new stats. Any XP gained, apart from XP from journals, is reset \
back to 0.")
}; };
static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! { static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
@ -269,11 +281,16 @@ static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let mut help = None; let mut help = None;
let is_unregistered = match ctx.user_dat { let is_unregistered = match ctx.user_dat {
None => true, None => true,
Some(user_dat) => !user_dat.terms.terms_complete Some(user_dat) => !user_dat.terms.terms_complete,
}; };
let remaining = remaining.trim(); let remaining = remaining.trim();
if is_unregistered { if is_unregistered {
@ -285,11 +302,10 @@ impl UserVerb for Verb {
} }
} }
help = help.or_else(|| ALWAYS_HELP_PAGES.get(remaining)); help = help.or_else(|| ALWAYS_HELP_PAGES.get(remaining));
let help_final = help.ok_or( let help_final = help.ok_or(UserError("No help available on that".to_string()))?;
UserError("No help available on that".to_string()))?; ctx.trans
ctx.trans.queue_for_session(ctx.session, .queue_for_session(ctx.session, Some(&(help_final.to_string() + "\n")))
Some(&(help_final.to_string() + "\n")) .await?;
).await?;
Ok(()) Ok(())
} }
} }

View File

@ -1,10 +1,15 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult}; use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use async_trait::async_trait; use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, _ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
_ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
Ok(()) Ok(())
} }
} }

View File

@ -1,63 +1,94 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{
UserError, get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
user_error, get_player_item_or_fail, search_item_for_user}; UserVerbRef, VerbContext,
};
use crate::{ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
static_content::{
possession_type::possession_data,
room::Direction,
},
models::item::ItemFlag, models::item::ItemFlag,
static_content::{possession_type::possession_data, room::Direction},
}; };
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { 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 ") { let (install_what_raw, what_dir_raw) = match remaining.rsplit_once(" on door to ") {
None => user_error(ansi!("Install where? Try <bold>install<reset> <lt>lock> <bold>on door to<reset> <lt>direction>").to_owned())?, None => user_error(ansi!("Install where? Try <bold>install<reset> <lt>lock> <bold>on door to<reset> <lt>direction>").to_owned())?,
Some(v) => v Some(v) => v
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Apparently, you have to be alive to work as an installer.\ user_error(
So discriminatory!".to_owned())?; "Apparently, you have to be alive to work as an installer.\
So discriminatory!"
.to_owned(),
)?;
} }
let item = search_item_for_user(ctx, &ItemSearchParams { let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
..ItemSearchParams::base(&player_item, install_what_raw.trim()) ..ItemSearchParams::base(&player_item, install_what_raw.trim())
}).await?; },
)
.await?;
if item.item_type != "possession" { if item.item_type != "possession" {
user_error("You can't install that!".to_owned())?; user_error("You can't install that!".to_owned())?;
} }
let handler = match item.possession_type.as_ref() let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.install_handler) { .and_then(|pd| pd.install_handler)
{
None => user_error("You can't install that!".to_owned())?, None => user_error("You can't install that!".to_owned())?,
Some(h) => h Some(h) => h,
}; };
let (loc_t, loc_c) = player_item.location.split_once("/") let (loc_t, loc_c) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid current location".to_owned()))?; .ok_or_else(|| UserError("Invalid current location".to_owned()))?;
let loc_item = ctx.trans.find_item_by_type_code(loc_t, loc_c).await? 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()))?; .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) { if loc_item.owner.as_ref() != Some(&player_item.refstr())
user_error("You can only install things while standing in a private room you own. \ || !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." If you are outside, try installing from the inside."
.to_owned())?; .to_owned(),
)?;
} }
let dir = Direction::parse(what_dir_raw.trim()).ok_or_else( let dir = Direction::parse(what_dir_raw.trim())
|| UserError("Invalid direction.".to_owned()))?; .ok_or_else(|| UserError("Invalid direction.".to_owned()))?;
loc_item.door_states.as_ref().and_then(|ds| ds.get(&dir)).ok_or_else( loc_item
|| UserError("No door to that direction in this room - are you on the wrong side?".to_owned()) .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?; handler
.install_cmd(ctx, &player_item, &item, &loc_item, &dir)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,14 +1,19 @@
use super::{ use super::{UResult, UserVerb, UserVerbRef, VerbContext};
VerbContext, UserVerb, UserVerbRef, UResult
};
use async_trait::async_trait; use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
(*ctx.session_dat).less_explicit_mode = true; (*ctx.session_dat).less_explicit_mode = true;
ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?; ctx.trans
.save_session_model(ctx.session, ctx.session_dat)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -1,50 +1,69 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
get_player_item_or_fail}; use crate::{language, static_content::possession_type::possession_data, static_content::room};
use crate::{
static_content::room,
static_content::possession_type::possession_data,
language
};
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Nobody seems to offer you any prices... possibly because you're dead.".to_owned())? user_error(
"Nobody seems to offer you any prices... possibly because you're dead.".to_owned(),
)?
} }
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" { if heretype != "room" {
user_error("Can't list stock because you're not in a shop.".to_owned())?; user_error("Can't list stock because you're not in a shop.".to_owned())?;
} }
let room = match room::room_map_by_code().get(herecode) { let room = match room::room_map_by_code().get(herecode) {
None => user_error("Can't find that shop.".to_owned())?, None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r Some(r) => r,
}; };
if room.stock_list.is_empty() { if room.stock_list.is_empty() {
user_error("Can't list stock because you're not in a shop.".to_owned())? user_error("Can't list stock because you're not in a shop.".to_owned())?
} }
let mut msg = String::new(); let mut msg = String::new();
msg.push_str(&format!(ansi!("<bold><bgblue><white>| {:20} | {:15} |<reset>\n"), msg.push_str(&format!(
ansi!("<bold><bgblue><white>| {:20} | {:15} |<reset>\n"),
ansi!("Item"), ansi!("Item"),
ansi!("Price"))); ansi!("Price")
));
for stock in &room.stock_list { for stock in &room.stock_list {
if let Some(possession_type) = possession_data().get(&stock.possession_type) { if let Some(possession_type) = possession_data().get(&stock.possession_type) {
let display = if ctx.session_dat.less_explicit_mode { let display = if ctx.session_dat.less_explicit_mode {
possession_type.display_less_explicit.as_ref().unwrap_or(&possession_type.display) possession_type
} else { &possession_type.display }; .display_less_explicit
msg.push_str(&format!("| {:20} | {:15.2} |\n", .as_ref()
&language::caps_first(&display), &stock.list_price)) .unwrap_or(&possession_type.display)
} else {
&possession_type.display
};
msg.push_str(&format!(
"| {:20} | {:15.2} |\n",
&language::caps_first(&display),
&stock.list_price
))
} }
} }
msg.push_str(ansi!("\nUse <bold>buy<reset> item to purchase something.\n")); msg.push_str(ansi!(
ctx.trans.queue_for_session(&ctx.session, Some(&msg)).await?; "\nUse <bold>buy<reset> item to purchase something.\n"
));
ctx.trans
.queue_for_session(&ctx.session, Some(&msg))
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -1,30 +1,59 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use super::look; use super::look;
use super::{user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use async_trait::async_trait; use async_trait::async_trait;
use tokio::time; use tokio::time;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (username, password) = match remaining.split_whitespace().collect::<Vec<&str>>()[..] { let (username, password) = match remaining.split_whitespace().collect::<Vec<&str>>()[..] {
[] | [_] => user_error("Too few options to login".to_owned())?, [] | [_] => user_error("Too few options to login".to_owned())?,
[username, password] => (username, password), [username, password] => (username, password),
_ => user_error("Too many options to login".to_owned())?, _ => user_error("Too many options to login".to_owned())?,
}; };
match ctx.trans.find_by_username(username).await? { let username_exact = match ctx.trans.find_by_username(username).await? {
None => user_error("No such user.".to_owned())?, None => user_error("No such user.".to_owned())?,
Some(user) => { Some(user) => {
time::sleep(time::Duration::from_secs(5)).await; time::sleep(time::Duration::from_secs(5)).await;
if !bcrypt::verify(password, &user.password_hash)? { if !bcrypt::verify(password, &user.password_hash)? {
user_error("Invalid password.".to_owned())? user_error("Invalid password.".to_owned())?
} }
let username_exact = user.username.clone();
*ctx.user_dat = Some(user); *ctx.user_dat = Some(user);
username_exact
} }
};
ctx.trans
.attach_user_to_session(username, ctx.session)
.await?;
if ctx
.trans
.check_task_by_type_code("DestroyUser", &username_exact)
.await?
{
ctx.trans
.queue_for_session(
ctx.session,
Some(
"Your username was scheduled to self-destruct - that has now been \
cancelled because you logged in.\n",
),
)
.await?;
ctx.trans
.delete_task("DestroyUser", &username_exact)
.await?;
} }
ctx.trans.attach_user_to_session(username, ctx.session).await?;
super::agree::check_and_notify_accepts(ctx).await?; super::agree::check_and_notify_accepts(ctx).await?;
if let Some(user) = ctx.user_dat { if let Some(user) = ctx.user_dat {
ctx.trans.save_user_model(user).await?; ctx.trans.save_user_model(user).await?;

View File

@ -1,40 +1,42 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, get_player_item_or_fail,
get_player_item_or_fail, search_item_for_user,
map::{render_map, render_map_dyn}, map::{render_map, render_map_dyn},
open::{is_door_in_direction, DoorSituation}, open::{is_door_in_direction, DoorSituation},
search_item_for_user, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
}; };
use async_trait::async_trait; #[double]
use ansi::{ansi, flow_around, word_wrap}; use crate::db::DBTrans;
use crate::{ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
models::{item::{
Item, LocationActionType, Subattack, ItemFlag, ItemSpecialData,
DoorState
}},
static_content::{
room::{self, Direction},
dynzone::self,
possession_type::possession_data,
species::{SpeciesType, species_info_map},
},
language, language,
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack},
services::combat::max_health, services::combat::max_health,
static_content::{
dynzone,
possession_type::possession_data,
room::{self, Direction},
species::{species_info_map, SpeciesType},
},
}; };
use ansi::{ansi, flow_around, word_wrap};
use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
use std::sync::Arc;
use std::collections::BTreeSet;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; use std::collections::BTreeSet;
use std::sync::Arc;
pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> { pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> {
let mut contents_desc = String::new(); let mut contents_desc = String::new();
let mut items = ctx.trans.find_items_by_location(&format!("{}/{}", let mut items = ctx
item.item_type, item.item_code)).await?; .trans
items.sort_unstable_by(|it1, it2| (&it1.action_type).cmp(&it2.action_type) .find_items_by_location(&format!("{}/{}", item.item_type, item.item_code))
.then((&it1.display).cmp(&it2.display))); .await?;
items.sort_unstable_by(|it1, it2| {
(&it1.action_type)
.cmp(&it2.action_type)
.then((&it1.display).cmp(&it2.display))
});
let all_groups: Vec<Vec<&Arc<Item>>> = items let all_groups: Vec<Vec<&Arc<Item>>> = items
.iter() .iter()
@ -53,8 +55,11 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
let mut phrases = Vec::<String>::new(); let mut phrases = Vec::<String>::new();
for group_items in all_groups { for group_items in all_groups {
let head = &group_items[0]; let head = &group_items[0];
let mut details = head.display_for_sentence(!ctx.session_dat.less_explicit_mode, let mut details = head.display_for_sentence(
group_items.len(), false); !ctx.session_dat.less_explicit_mode,
group_items.len(),
false,
);
match head.action_type { match head.action_type {
LocationActionType::Wielded => details.push_str(" (wielded)"), LocationActionType::Wielded => details.push_str(" (wielded)"),
LocationActionType::Worn => continue, LocationActionType::Worn => continue,
@ -66,29 +71,39 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n")); contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n"));
} }
let anything_worn = items.iter().any(|it| it.action_type == LocationActionType::Worn); let anything_worn = items
.iter()
.any(|it| it.action_type == LocationActionType::Worn);
if anything_worn { if anything_worn {
let mut any_part_text = false; let mut any_part_text = false;
let mut seen_clothes: BTreeSet<String> = BTreeSet::new(); let mut seen_clothes: BTreeSet<String> = BTreeSet::new();
for part in species_info_map().get(&item.species).map(|s| s.body_parts.clone()) for part in species_info_map()
.unwrap_or_else(|| vec!()) { .get(&item.species)
if let Some((top_item, covering_parts)) = items.iter() .map(|s| s.body_parts.clone())
.filter_map( .unwrap_or_else(|| vec![])
|it| {
if let Some((top_item, covering_parts)) = items
.iter()
.filter_map(|it| {
if it.action_type != LocationActionType::Worn { if it.action_type != LocationActionType::Worn {
None None
} else { } else {
it.possession_type.as_ref() it.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref()) .and_then(|pd| pd.wear_data.as_ref())
.and_then(|wd| if wd.covers_parts.contains(&part) { .and_then(|wd| {
if wd.covers_parts.contains(&part) {
Some((it, wd.covers_parts.clone())) Some((it, wd.covers_parts.clone()))
} else { } else {
None None
}
}) })
}
}) })
.filter_map(|(it, parts)| it.action_type_started.map(|st| ((it, parts), st))) .filter_map(|(it, parts)| it.action_type_started.map(|st| ((it, parts), st)))
.max_by_key(|(_it, st)| st.clone()).map(|(it, _)| it) .max_by_key(|(_it, st)| st.clone())
.map(|(it, _)| it)
{ {
any_part_text = true; any_part_text = true;
let display = top_item.display_for_session(&ctx.session_dat); let display = top_item.display_for_session(&ctx.session_dat);
@ -98,28 +113,37 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
"On {} {}, you see {}. ", "On {} {}, you see {}. ",
&item.pronouns.possessive, &item.pronouns.possessive,
&language::join_words( &language::join_words(
&covering_parts.iter().map(|p| p.display(None)) &covering_parts
.collect::<Vec<&'static str>>()), .iter()
.map(|p| p.display(None))
.collect::<Vec<&'static str>>()
),
&display &display
)); ));
} }
} else { } else {
if !ctx.session_dat.less_explicit_mode { if !ctx.session_dat.less_explicit_mode {
any_part_text = true; any_part_text = true;
contents_desc.push_str(&format!("{} {} {} completely bare. ", contents_desc.push_str(&format!(
"{} {} {} completely bare. ",
&language::caps_first(&item.pronouns.possessive), &language::caps_first(&item.pronouns.possessive),
part.display(item.sex.clone()), part.display(item.sex.clone()),
part.copula(item.sex.clone()))); part.copula(item.sex.clone())
));
} }
} }
} }
if any_part_text { if any_part_text {
contents_desc.push_str("\n"); contents_desc.push_str("\n");
} }
} else if item.species == SpeciesType::Human && !ctx.session_dat.less_explicit_mode { } else if (item.item_type == "npc" || item.item_type == "player")
contents_desc.push_str(&format!("{} is completely naked.\n", && item.species == SpeciesType::Human
&language::caps_first(&item.pronouns.possessive))); && !ctx.session_dat.less_explicit_mode
{
contents_desc.push_str(&format!(
"{} is completely naked.\n",
&language::caps_first(&item.pronouns.subject)
));
} }
let health_max = max_health(&item); let health_max = max_health(&item);
@ -127,123 +151,207 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
let health_ratio = (item.health as f64) / (health_max as f64); let health_ratio = (item.health as f64) / (health_max as f64);
if item.item_type == "player" || item.item_type == "npc" { if item.item_type == "player" || item.item_type == "npc" {
if health_ratio == 1.0 { if health_ratio == 1.0 {
contents_desc.push_str(&format!("{} is in perfect health.\n", &language::caps_first(&item.pronouns.subject))); contents_desc.push_str(&format!(
"{} is in perfect health.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.75 { } else if health_ratio >= 0.75 {
contents_desc.push_str(&format!("{} has some minor cuts and bruises.\n", &language::caps_first(&item.pronouns.subject))); contents_desc.push_str(&format!(
"{} has some minor cuts and bruises.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.5 { } else if health_ratio >= 0.5 {
contents_desc.push_str(&format!("{} has deep wounds all over {} body.\n", &language::caps_first(&item.pronouns.subject), &item.pronouns.possessive)); contents_desc.push_str(&format!(
"{} has deep wounds all over {} body.\n",
&language::caps_first(&item.pronouns.subject),
&item.pronouns.possessive
));
} else if health_ratio >= 0.25 { } else if health_ratio >= 0.25 {
contents_desc.push_str(&format!("{} looks seriously injured.\n", contents_desc.push_str(&format!(
&language::caps_first( "{} looks seriously injured.\n",
&item.pronouns.subject))); &language::caps_first(&item.pronouns.subject)
));
} else { } else {
contents_desc.push_str(&format!("{} looks like {}'s on death's door.\n", contents_desc.push_str(&format!(
&language::caps_first( "{} looks like {}'s on death's door.\n",
&item.pronouns.subject), &language::caps_first(&item.pronouns.subject),
&item.pronouns.possessive)); &item.pronouns.possessive
));
} }
if ctx.trans.check_task_by_type_code("DelayedHealth", if ctx
&format!("{}/{}/bandage", &item.item_type, &item.item_code) .trans
).await? { .check_task_by_type_code(
contents_desc.push_str(&format!("{} is wrapped up in bandages.\n", "DelayedHealth",
&language::caps_first(&item.pronouns.subject)) &format!("{}/{}/bandage", &item.item_type, &item.item_code),
); )
.await?
{
contents_desc.push_str(&format!(
"{} is wrapped up in bandages.\n",
&language::caps_first(&item.pronouns.subject)
));
} }
} else if item.item_type == "possession" { } else if item.item_type == "possession" {
if health_ratio == 1.0 { if health_ratio == 1.0 {
contents_desc.push_str(&format!("{}'s in perfect condition.\n", &language::caps_first(&item.pronouns.subject))); contents_desc.push_str(&format!(
"{}'s in perfect condition.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.75 { } else if health_ratio >= 0.75 {
contents_desc.push_str(&format!("{}'s slightly beaten up.\n", &language::caps_first(&item.pronouns.subject))); contents_desc.push_str(&format!(
"{}'s slightly beaten up.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.5 { } else if health_ratio >= 0.5 {
contents_desc.push_str(&format!("{}'s pretty beaten up.\n", &language::caps_first(&item.pronouns.subject))); contents_desc.push_str(&format!(
"{}'s pretty beaten up.\n",
&language::caps_first(&item.pronouns.subject)
));
} else if health_ratio >= 0.25 { } else if health_ratio >= 0.25 {
contents_desc.push_str(&format!("{}'s seriously damaged.\n", &language::caps_first(&item.pronouns.subject))); contents_desc.push_str(&format!(
"{}'s seriously damaged.\n",
&language::caps_first(&item.pronouns.subject)
));
} else { } else {
contents_desc.push_str(&format!("{}'s nearly completely destroyed.\n", contents_desc.push_str(&format!(
&language::caps_first(&item.pronouns.subject))); "{}'s nearly completely destroyed.\n",
&language::caps_first(&item.pronouns.subject)
));
} }
} }
} }
if item.item_type == "possession" { if item.item_type == "possession" {
if let Some(charge_data) = item.possession_type.as_ref() if let Some(charge_data) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.charge_data.as_ref()) { .and_then(|pd| pd.charge_data.as_ref())
{
let unit = if item.charges == 1 { let unit = if item.charges == 1 {
charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_suffix
charge_data.charge_name_suffix
} else { } else {
language::pluralise(charge_data.charge_name_prefix) + " " + language::pluralise(charge_data.charge_name_prefix)
charge_data.charge_name_suffix + " "
+ charge_data.charge_name_suffix
}; };
contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit)); contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit));
} }
} }
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&format!("{}\n{}\n{}", Some(&format!(
"{}\n{}\n{}",
&item.display_for_session(&ctx.session_dat), &item.display_for_session(&ctx.session_dat),
item.details_for_session(&ctx.session_dat).unwrap_or(""), item.details_for_session(&ctx.session_dat).unwrap_or(""),
contents_desc, contents_desc,
)) )),
).await?; )
.await?;
Ok(()) Ok(())
} }
fn exits_for(room: &room::Room) -> String { fn exits_for(room: &room::Room) -> String {
let exit_text: Vec<String> = let exit_text: Vec<String> = room
room.exits.iter().map(|ex| format!("{}{}", .exits
.iter()
.map(|ex| {
format!(
"{}{}",
if ex.exit_climb.is_some() { if ex.exit_climb.is_some() {
ansi!("<red>^") ansi!("<red>^")
} else { } else {
ansi!("<yellow>") ansi!("<yellow>")
}, },
ex.direction.describe())).collect(); ex.direction.describe()
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" ")) )
})
.collect();
format!(
ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"),
exit_text.join(" ")
)
} }
fn exits_for_dyn(dynroom: &dynzone::Dynroom) -> String { fn exits_for_dyn(dynroom: &dynzone::Dynroom) -> String {
let exit_text: Vec<String> = let exit_text: Vec<String> = dynroom
dynroom.exits.iter().map(|ex| format!(ansi!("<yellow>{}"), .exits
ex.direction.describe())).collect(); .iter()
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" ")) .map(|ex| format!(ansi!("<yellow>{}"), ex.direction.describe()))
.collect();
format!(
ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"),
exit_text.join(" ")
)
} }
pub async fn describe_room(ctx: &VerbContext<'_>, item: &Item, pub async fn describe_room(
room: &room::Room, contents: &str) -> UResult<()> { ctx: &VerbContext<'_>,
let zone = room::zone_details().get(room.zone).map(|z|z.display).unwrap_or("Outside of time"); item: &Item,
ctx.trans.queue_for_session( room: &room::Room,
contents: &str,
) -> UResult<()> {
let zone = room::zone_details()
.get(room.zone)
.map(|z| z.display)
.unwrap_or("Outside of time");
ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&flow_around(&render_map(room, 5, 5), 10, ansi!("<reset> "), Some(&flow_around(
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"), &render_map(room, 5, 5),
10,
ansi!("<reset> "),
&word_wrap(
&format!(
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
item.display_for_session(&ctx.session_dat), item.display_for_session(&ctx.session_dat),
zone, zone,
item.details_for_session( item.details_for_session(&ctx.session_dat).unwrap_or(""),
&ctx.session_dat).unwrap_or(""), contents,
contents, exits_for(room)), exits_for(room)
|row| if row >= 5 { 80 } else { 68 }), 68)) ),
).await?; |row| if row >= 5 { 80 } else { 68 },
),
68,
)),
)
.await?;
Ok(()) Ok(())
} }
pub async fn describe_dynroom(ctx: &VerbContext<'_>, pub async fn describe_dynroom(
ctx: &VerbContext<'_>,
item: &Item, item: &Item,
dynzone: &dynzone::Dynzone, dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom, dynroom: &dynzone::Dynroom,
contents: &str) -> UResult<()> { contents: &str,
ctx.trans.queue_for_session( ) -> UResult<()> {
ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&flow_around(&render_map_dyn(dynzone, dynroom, 5, 5), 10, ansi!("<reset> "), Some(&flow_around(
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"), &render_map_dyn(dynzone, dynroom, 5, 5),
10,
ansi!("<reset> "),
&word_wrap(
&format!(
ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
item.display_for_session(&ctx.session_dat), item.display_for_session(&ctx.session_dat),
dynzone.zonename, dynzone.zonename,
item.details_for_session( item.details_for_session(&ctx.session_dat).unwrap_or(""),
&ctx.session_dat).unwrap_or(""), contents,
contents, exits_for_dyn(dynroom)), exits_for_dyn(dynroom)
|row| if row >= 5 { 80 } else { 68 }), 68)) ),
).await?; |row| if row >= 5 { 80 } else { 68 },
),
68,
)),
)
.await?;
Ok(()) Ok(())
} }
@ -253,23 +361,22 @@ async fn describe_door(
state: &DoorState, state: &DoorState,
direction: &Direction, direction: &Direction,
) -> UResult<()> { ) -> UResult<()> {
let mut msg = format!("That exit is blocked by {}.", let mut msg = format!("That exit is blocked by {}.", &state.description);
&state.description); if let Some(lock) = ctx
if let Some(lock) = ctx.trans.find_by_action_and_location( .trans
.find_by_action_and_location(
&room_item.refstr(), &room_item.refstr(),
&LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await?.first() &LocationActionType::InstalledOnDoorAsLock((*direction).clone()),
)
.await?
.first()
{ {
let lock_desc = lock.display_for_session(&ctx.session_dat); let lock_desc = lock.display_for_session(&ctx.session_dat);
msg.push_str(&format!(" The door is locked with {}", msg.push_str(&format!(" The door is locked with {}", &lock_desc));
&lock_desc
));
} }
msg.push('\n'); msg.push('\n');
ctx.trans.queue_for_session( ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
ctx.session,
Some(&msg)).await?;
Ok(()) Ok(())
} }
async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> { async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> {
@ -277,8 +384,10 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
return Ok(" It is too foggy to see who or what else is here.".to_owned()); return Ok(" It is too foggy to see who or what else is here.".to_owned());
} }
let mut buf = String::new(); let mut buf = String::new();
let mut items = ctx.trans.find_items_by_location(&format!("{}/{}", let mut items = ctx
item.item_type, item.item_code)).await?; .trans
.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code))
.await?;
items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display)); items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display));
let all_groups: Vec<Vec<&Arc<Item>>> = items let all_groups: Vec<Vec<&Arc<Item>>> = items
@ -293,9 +402,16 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
let head = &group_items[0]; let head = &group_items[0];
let is_creature = head.item_type == "player" || head.item_type.starts_with("npc"); let is_creature = head.item_type == "player" || head.item_type.starts_with("npc");
buf.push(' '); buf.push(' ');
buf.push_str(&head.display_for_sentence(!ctx.session_dat.less_explicit_mode, buf.push_str(&head.display_for_sentence(
group_items.len(), true)); !ctx.session_dat.less_explicit_mode,
buf.push_str(if group_items.len() > 1 { " are " } else { " is "}); group_items.len(),
true,
));
buf.push_str(if group_items.len() > 1 {
" are "
} else {
" is "
});
match head.action_type { match head.action_type {
LocationActionType::Sitting => buf.push_str("sitting "), LocationActionType::Sitting => buf.push_str("sitting "),
LocationActionType::Reclining => buf.push_str("reclining "), LocationActionType::Reclining => buf.push_str("reclining "),
@ -315,22 +431,24 @@ async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> URe
Subattack::Feinting => buf.push_str(", feinting "), Subattack::Feinting => buf.push_str(", feinting "),
Subattack::Grabbing => buf.push_str(", grabbing "), Subattack::Grabbing => buf.push_str(", grabbing "),
Subattack::Wrestling => buf.push_str(", wrestling "), Subattack::Wrestling => buf.push_str(", wrestling "),
_ => buf.push_str(", attacking ") _ => buf.push_str(", attacking "),
} }
match &head.active_combat.as_ref() match &head
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.clone()) .and_then(|ac| ac.attacking.clone())
.or_else(|| head.presence_target.clone()) { .or_else(|| head.presence_target.clone())
{
None => buf.push_str("someone"), None => buf.push_str("someone"),
Some(who) => match who.split_once("/") { Some(who) => match who.split_once("/") {
None => buf.push_str("someone"), None => buf.push_str("someone"),
Some((ttype, tcode)) => Some((ttype, tcode)) => {
match ctx.trans.find_item_by_type_code(ttype, tcode).await? { match ctx.trans.find_item_by_type_code(ttype, tcode).await? {
None => buf.push_str("someone"), None => buf.push_str("someone"),
Some(it) => buf.push_str( Some(it) => buf.push_str(&it.display_for_session(&ctx.session_dat)),
&it.display_for_session(&ctx.session_dat)
)
} }
} }
},
} }
} }
buf.push('.'); buf.push('.');
@ -348,90 +466,152 @@ pub async fn direction_to_item(
return Ok(Some(Arc::new(dynroom_result))); return Ok(Some(Arc::new(dynroom_result)));
} }
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let (heretype, herecode) = use_location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype == "dynroom" { if heretype == "dynroom" {
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? { let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
None => user_error("Your current room has vanished!".to_owned())?, None => user_error("Your current room has vanished!".to_owned())?,
Some(v) => v Some(v) => v,
}; };
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() { let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code), Some(ItemSpecialData::DynroomData {
_ => user_error("Your current room is invalid!".to_owned())? dynzone_code,
dynroom_code,
}) => (dynzone_code, dynroom_code),
_ => user_error("Your current room is invalid!".to_owned())?,
}; };
let dynzone = dynzone::dynzone_by_type() let dynzone = dynzone::dynzone_by_type()
.get(&dynzone::DynzoneType::from_str(dynzone_code) .get(
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?) &dynzone::DynzoneType::from_str(dynzone_code).ok_or_else(|| {
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?; 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("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()))?; .ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction) let exit = dynroom
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
return match exit.target { return match exit.target {
dynzone::ExitTarget::ExitZone => { dynzone::ExitTarget::ExitZone => {
let (zonetype, zonecode) = old_dynroom_item.location.split_once("/") let (zonetype, zonecode) = old_dynroom_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?; .ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?;
let zoneitem = trans.find_item_by_type_code(zonetype, zonecode).await? let zoneitem = trans
.find_item_by_type_code(zonetype, zonecode)
.await?
.ok_or_else(|| UserError("Can't find your zone".to_owned()))?; .ok_or_else(|| UserError("Can't find your zone".to_owned()))?;
let zone_exit = match zoneitem.special_data.as_ref() { let zone_exit = match zoneitem.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) => Some(ItemSpecialData::DynzoneData {
user_error("That exit doesn't seem to go anywhere".to_owned())?, zone_exit: None, ..
Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit, }) => user_error("That exit doesn't seem to go anywhere".to_owned())?,
_ => user_error("The zone you are in has invalid data associated with it".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(),
)?,
}; };
let (zone_exit_type, zone_exit_code) = zone_exit.split_once("/").ok_or_else( let (zone_exit_type, zone_exit_code) =
|| UserError("Oops, that way out seems to be broken.".to_owned()))?; zone_exit.split_once("/").ok_or_else(|| {
Ok(trans.find_item_by_type_code(zone_exit_type, zone_exit_code).await?) UserError("Oops, that way out seems to be broken.".to_owned())
}, })?;
Ok(trans
.find_item_by_type_code(zone_exit_type, zone_exit_code)
.await?)
}
dynzone::ExitTarget::Intrazone { subcode } => { dynzone::ExitTarget::Intrazone { subcode } => {
let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await? let to_item = trans
.ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?; .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(Some(Arc::new(to_item))) Ok(Some(Arc::new(to_item)))
} }
} };
} }
if heretype != "room" { if heretype != "room" {
user_error("Navigating outside rooms not yet supported.".to_owned())? user_error("Navigating outside rooms not yet supported.".to_owned())?
} }
let room = room::room_map_by_code().get(herecode) let room = room::room_map_by_code()
.get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?; .ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room.exits.iter().find(|ex| ex.direction == *direction) let exit = room
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
let new_room = let new_room = room::resolve_exit(room, exit)
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?; .ok_or_else(|| UserError("Can't find that room".to_owned()))?;
Ok(trans.find_item_by_type_code("room", new_room.code).await?) Ok(trans.find_item_by_type_code("room", new_room.code).await?)
} }
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let rem_trim = remaining.trim().to_lowercase(); let rem_trim = remaining.trim().to_lowercase();
let use_location = if player_item.death_data.is_some() { "room/repro_xv_respawn" } else { let use_location = if player_item.death_data.is_some() {
"room/repro_xv_respawn"
} else {
&player_item.location &player_item.location
}; };
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let (heretype, herecode) = use_location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
let item: Arc<Item> = if rem_trim == "" { let item: Arc<Item> = if rem_trim == "" {
ctx.trans.find_item_by_type_code(heretype, herecode).await? ctx.trans
.find_item_by_type_code(heretype, herecode)
.await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))? .ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
} else if let Some(dir) = Direction::parse(&rem_trim) { } else if let Some(dir) = Direction::parse(&rem_trim) {
match is_door_in_direction(&ctx.trans, &dir, use_location).await? { match is_door_in_direction(&ctx.trans, &dir, use_location).await? {
DoorSituation::NoDoor | DoorSituation::NoDoor
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } | | DoorSituation::DoorOutOfRoom {
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } => {}, state: DoorState { open: true, .. },
DoorSituation::DoorIntoRoom { state, room_with_door, .. } => { ..
}
| DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. },
..
} => {}
DoorSituation::DoorIntoRoom {
state,
room_with_door,
..
} => {
if let Some(rev_dir) = dir.reverse() { if let Some(rev_dir) = dir.reverse() {
return describe_door(ctx, &room_with_door, &state, &rev_dir).await; return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
} }
}, }
DoorSituation::DoorOutOfRoom { state, room_with_door, .. } => { DoorSituation::DoorOutOfRoom {
state,
room_with_door,
..
} => {
return describe_door(ctx, &room_with_door, &state, &dir).await; return describe_door(ctx, &room_with_door, &state, &dir).await;
} }
} }
direction_to_item(&ctx.trans, use_location, &dir).await? direction_to_item(&ctx.trans, use_location, &dir)
.await?
.ok_or_else(|| UserError("There's nothing in that direction".to_owned()))? .ok_or_else(|| UserError("There's nothing in that direction".to_owned()))?
} else if rem_trim == "me" || rem_trim == "self" { } else if rem_trim == "me" || rem_trim == "self" {
player_item.clone() player_item.clone()
@ -443,27 +623,34 @@ impl UserVerb for Verb {
include_loc_contents: true, include_loc_contents: true,
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, &rem_trim) ..ItemSearchParams::base(&player_item, &rem_trim)
} },
).await? )
.await?
}; };
if item.item_type == "room" { if item.item_type == "room" {
let room = let room = room::room_map_by_code()
room::room_map_by_code().get(item.item_code.as_str()) .get(item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?; .ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
describe_room(ctx, &item, &room, &list_room_contents(ctx, &item).await?).await?; describe_room(ctx, &item, &room, &list_room_contents(ctx, &item).await?).await?;
} else if item.item_type == "dynroom" { } else if item.item_type == "dynroom" {
let (dynzone, dynroom) = match &item.special_data { let (dynzone, dynroom) = match &item.special_data {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => { Some(ItemSpecialData::DynroomData {
dynzone::DynzoneType::from_str(dynzone_code.as_str()) dynzone_code,
.and_then(|dz_t| dynroom_code,
dynzone::dynzone_by_type().get(&dz_t)) }) => dynzone::DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dz_t| dynzone::dynzone_by_type().get(&dz_t))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr))) .and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr)))
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))? .ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?,
}, _ => user_error("Expected dynroom to have DynroomData".to_owned())?,
_ => user_error("Expected dynroom to have DynroomData".to_owned())?
}; };
describe_dynroom(ctx, &item, &dynzone, &dynroom, describe_dynroom(
&list_room_contents(ctx, &item).await?).await?; ctx,
&item,
&dynzone,
&dynroom,
&list_room_contents(ctx, &item).await?,
)
.await?;
} else { } else {
describe_normal_item(ctx, &item).await?; describe_normal_item(ctx, &item).await?;
} }

View File

@ -1,14 +1,15 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, use super::{
get_player_item_or_fail}; get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
use async_trait::async_trait; };
use ansi::{ansi, flow_around};
use crate::{ use crate::{
models::item::{Item, ItemSpecialData}, models::item::{Item, ItemSpecialData},
static_content::{ static_content::{
dynzone,
room::{self, Direction, GridCoords}, room::{self, Direction, GridCoords},
dynzone::self },
}
}; };
use ansi::{ansi, flow_around};
use async_trait::async_trait;
use std::sync::Arc; use std::sync::Arc;
pub fn render_map(room: &room::Room, width: usize, height: usize) -> String { pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
@ -23,17 +24,22 @@ pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
if my_loc.x == x && my_loc.y == y { if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red>()<reset>")) buf.push_str(ansi!("<bgblue><red>()<reset>"))
} else { } else {
buf.push_str(room::room_map_by_zloc() buf.push_str(
room::room_map_by_zloc()
.get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z })) .get(&(&room.zone, &room::GridCoords { x, y, z: my_loc.z }))
.map(|r| if room.zone == r.zone { .map(|r| {
if room.zone == r.zone {
r.short r.short
} else { } else {
r.secondary_zones.iter() r.secondary_zones
.iter()
.find(|sz| sz.zone == room.zone) .find(|sz| sz.zone == room.zone)
.map(|sz| sz.short) .map(|sz| sz.short)
.expect("Secondary zone missing") .expect("Secondary zone missing")
}
}) })
.unwrap_or(" ")); .unwrap_or(" "),
);
} }
} }
buf.push('\n'); buf.push('\n');
@ -41,9 +47,12 @@ pub fn render_map(room: &room::Room, width: usize, height: usize) -> String {
buf buf
} }
pub fn render_map_dyn(dynzone: &dynzone::Dynzone, pub fn render_map_dyn(
dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom, dynroom: &dynzone::Dynroom,
width: usize, height: usize) -> String { width: usize,
height: usize,
) -> String {
let mut buf = String::new(); let mut buf = String::new();
let my_loc = &dynroom.grid_coords; let my_loc = &dynroom.grid_coords;
let min_x = my_loc.x - (width as i64) / 2; let min_x = my_loc.x - (width as i64) / 2;
@ -51,37 +60,46 @@ pub fn render_map_dyn(dynzone: &dynzone::Dynzone,
let min_y = my_loc.y - (height as i64) / 2; let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64); let max_y = min_y + (height as i64);
let main_exit: Option<GridCoords> = dynzone.dyn_rooms let main_exit: Option<GridCoords> = dynzone
.dyn_rooms
.iter()
.flat_map(|(_, dr)| {
dr.exits
.iter() .iter()
.flat_map(|(_, dr)|
dr.exits.iter()
.filter(|ex| match ex.target { .filter(|ex| match ex.target {
dynzone::ExitTarget::ExitZone => true, dynzone::ExitTarget::ExitZone => true,
_ => false _ => false,
}) })
.map(|ex| dr.grid_coords.apply(&ex.direction)) .map(|ex| dr.grid_coords.apply(&ex.direction))
).next(); })
.next();
for y in min_y..max_y { for y in min_y..max_y {
for x in min_x..max_x { for x in min_x..max_x {
if my_loc.x == x && my_loc.y == y { if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red>()<reset>")) buf.push_str(ansi!("<bgblue><red>()<reset>"))
} else { } else {
buf.push_str(dynzone.dyn_rooms.iter() buf.push_str(
.find( dynzone
|(_, dr)| dr.grid_coords.x == x && .dyn_rooms
dr.grid_coords.y == y && .iter()
dr.grid_coords.z == my_loc.z) .find(|(_, dr)| {
dr.grid_coords.x == x
&& dr.grid_coords.y == y
&& dr.grid_coords.z == my_loc.z
})
.map(|(_, r)| r.short) .map(|(_, r)| r.short)
.or_else(|| main_exit.as_ref().and_then( .or_else(|| {
|ex_pos| main_exit.as_ref().and_then(|ex_pos| {
if ex_pos.x == x && ex_pos.y == y && if ex_pos.x == x && ex_pos.y == y && ex_pos.z == my_loc.z {
ex_pos.z == my_loc.z {
Some("<<") Some("<<")
} else { } else {
None None
})) }
.unwrap_or(" ")); })
})
.unwrap_or(" "),
);
} }
} }
buf.push('\n'); buf.push('\n');
@ -89,8 +107,12 @@ pub fn render_map_dyn(dynzone: &dynzone::Dynzone,
buf buf
} }
pub fn render_lmap(room: &room::Room, width: usize, height: usize, pub fn render_lmap(
captions_needed: &mut Vec<(usize, &'static str, &'static str)>) -> String { room: &room::Room,
width: usize,
height: usize,
captions_needed: &mut Vec<(usize, &'static str, &'static str)>,
) -> String {
let mut buf = String::new(); let mut buf = String::new();
let my_loc = &room.grid_coords; let my_loc = &room.grid_coords;
let min_x = my_loc.x - (width as i64) / 2; let min_x = my_loc.x - (width as i64) / 2;
@ -100,25 +122,44 @@ pub fn render_lmap(room: &room::Room, width: usize, height: usize,
for y in min_y..max_y { for y in min_y..max_y {
for x in min_x..max_x { for x in min_x..max_x {
let coord = room::GridCoords { x, y, z: my_loc.z }; let coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room = room::room_map_by_zloc() let coord_room = room::room_map_by_zloc().get(&(&room.zone, &coord));
.get(&(&room.zone, &coord));
if my_loc.x == x && my_loc.y == y { if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red> () <reset>")) buf.push_str(ansi!("<bgblue><red> () <reset>"))
} else { } else {
let code_capt_opt = coord_room.map( let code_capt_opt = coord_room.map(|r| {
|r| if room.zone == r.zone { if room.zone == r.zone {
(r.short, if r.should_caption { (
Some((r.name, ((my_loc.x as i64 - r.grid_coords.x).abs() + r.short,
(my_loc.y as i64 - r.grid_coords.y).abs() if r.should_caption {
) as usize)) } else { None }) Some((
r.name,
((my_loc.x as i64 - r.grid_coords.x).abs()
+ (my_loc.y as i64 - r.grid_coords.y).abs())
as usize,
))
} else { } else {
r.secondary_zones.iter() None
},
)
} else {
r.secondary_zones
.iter()
.find(|sz| sz.zone == room.zone) .find(|sz| sz.zone == room.zone)
.map(|sz| (sz.short, sz.caption.map( .map(|sz| {
|c| (c, ((my_loc.x as i64 - r.grid_coords.x).abs() + (
(my_loc.y as i64 - r.grid_coords.y).abs()) sz.short,
as usize)))) sz.caption.map(|c| {
(
c,
((my_loc.x as i64 - r.grid_coords.x).abs()
+ (my_loc.y as i64 - r.grid_coords.y).abs())
as usize,
)
}),
)
})
.expect("Secondary zone missing") .expect("Secondary zone missing")
}
}); });
match code_capt_opt { match code_capt_opt {
None => buf.push_str(" "), None => buf.push_str(" "),
@ -132,29 +173,36 @@ pub fn render_lmap(room: &room::Room, width: usize, height: usize,
} }
} }
} }
match coord_room.and_then( match coord_room.and_then(|r| r.exits.iter().find(|ex| ex.direction == Direction::EAST))
|r| r.exits.iter().find(|ex| ex.direction == Direction::EAST)) { {
None => buf.push(' '), None => buf.push(' '),
Some(_) => buf.push('-') Some(_) => buf.push('-'),
} }
} }
for x in min_x..max_x { for x in min_x..max_x {
let mut coord = room::GridCoords { x, y, z: my_loc.z }; let mut coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room = room::room_map_by_zloc() let coord_room = room::room_map_by_zloc().get(&(&room.zone, &coord));
.get(&(&room.zone, &coord)); match coord_room
match coord_room.and_then( .and_then(|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTH))
|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTH)) { {
None => buf.push_str(" "), None => buf.push_str(" "),
Some(_) => buf.push_str(" | ") Some(_) => buf.push_str(" | "),
} }
let has_se = coord_room.and_then( let has_se = coord_room
|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTHEAST)) .and_then(|r| {
r.exits
.iter()
.find(|ex| ex.direction == Direction::SOUTHEAST)
})
.is_some(); .is_some();
coord.y += 1; coord.y += 1;
let coord_room_s = room::room_map_by_zloc() let coord_room_s = room::room_map_by_zloc().get(&(&room.zone, &coord));
.get(&(&room.zone, &coord)); let has_ne = coord_room_s
let has_ne = coord_room_s.and_then( .and_then(|r| {
|r| r.exits.iter().find(|ex| ex.direction == Direction::NORTHEAST)) r.exits
.iter()
.find(|ex| ex.direction == Direction::NORTHEAST)
})
.is_some(); .is_some();
if has_se && has_ne { if has_se && has_ne {
buf.push('X'); buf.push('X');
@ -178,7 +226,7 @@ pub fn render_lmap_dynroom<'l, 'm>(
width: usize, width: usize,
height: usize, height: usize,
captions_needed: &'m mut Vec<(usize, &'l str, &'l str)>, captions_needed: &'m mut Vec<(usize, &'l str, &'l str)>,
connectwhere: Option<&'l str> connectwhere: Option<&'l str>,
) -> String { ) -> String {
let mut buf = String::new(); let mut buf = String::new();
let my_loc = &room.grid_coords; let my_loc = &room.grid_coords;
@ -187,57 +235,60 @@ pub fn render_lmap_dynroom<'l, 'm>(
let min_y = my_loc.y - (height as i64) / 2; let min_y = my_loc.y - (height as i64) / 2;
let max_y = min_y + (height as i64); let max_y = min_y + (height as i64);
let main_exit_dat: Option<(GridCoords, Direction)> = zone.dyn_rooms let main_exit_dat: Option<(GridCoords, Direction)> = zone
.dyn_rooms
.iter()
.flat_map(|(_, dr)| {
dr.exits
.iter() .iter()
.flat_map(|(_, dr)|
dr.exits.iter()
.filter(|ex| match ex.target { .filter(|ex| match ex.target {
dynzone::ExitTarget::ExitZone => true, dynzone::ExitTarget::ExitZone => true,
_ => false _ => false,
}) })
.map(|ex| (dr.grid_coords.apply(&ex.direction), ex.direction.clone())) .map(|ex| (dr.grid_coords.apply(&ex.direction), ex.direction.clone()))
).next(); })
.next();
let main_exit = main_exit_dat.as_ref(); let main_exit = main_exit_dat.as_ref();
for y in min_y..max_y { for y in min_y..max_y {
for x in min_x..max_x { for x in min_x..max_x {
let coord = room::GridCoords { x, y, z: my_loc.z }; let coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room: Option<&dynzone::Dynroom> = let coord_room: Option<&dynzone::Dynroom> = zone
zone.dyn_rooms.iter() .dyn_rooms
.find( .iter()
|(_, dr)| dr.grid_coords.x == x && .find(|(_, dr)| {
dr.grid_coords.y == y && dr.grid_coords.x == x && dr.grid_coords.y == y && dr.grid_coords.z == my_loc.z
dr.grid_coords.z == my_loc.z) })
.map(|(_, r)| r); .map(|(_, r)| r);
if my_loc.x == x && my_loc.y == y { if my_loc.x == x && my_loc.y == y {
buf.push_str(ansi!("<bgblue><red> () <reset>")); buf.push_str(ansi!("<bgblue><red> () <reset>"));
if let Some(room) = coord_room { if let Some(room) = coord_room {
if room.should_caption { if room.should_caption {
captions_needed.push( captions_needed.push((
( (((my_loc.x as i64 - room.grid_coords.x).abs()
(((my_loc.x as i64 - room.grid_coords.x).abs() + + (my_loc.y as i64 - room.grid_coords.y).abs())
(my_loc.y as i64 - room.grid_coords.y).abs()) as usize), as usize),
room.short, room.name room.short,
) room.name,
); ));
} }
} }
} else if let Some(room) = coord_room { } else if let Some(room) = coord_room {
if room.should_caption { if room.should_caption {
captions_needed.push( captions_needed.push((
( (((my_loc.x as i64 - room.grid_coords.x).abs()
(((my_loc.x as i64 - room.grid_coords.x).abs() + + (my_loc.y as i64 - room.grid_coords.y).abs())
(my_loc.y as i64 - room.grid_coords.y).abs()) as usize), as usize),
room.short, room.name room.short,
) room.name,
); ));
} }
buf.push('['); buf.push('[');
buf.push_str(room.short); buf.push_str(room.short);
buf.push(']'); buf.push(']');
match room.exits.iter().find(|ex| ex.direction == Direction::EAST) { match room.exits.iter().find(|ex| ex.direction == Direction::EAST) {
None => buf.push(' '), None => buf.push(' '),
Some(_) => buf.push('-') Some(_) => buf.push('-'),
} }
} else if main_exit.map(|ex| &ex.0) == Some(&coord) { } else if main_exit.map(|ex| &ex.0) == Some(&coord) {
buf.push_str("[<<]"); buf.push_str("[<<]");
@ -246,13 +297,15 @@ pub fn render_lmap_dynroom<'l, 'm>(
buf.push('-'); buf.push('-');
if let Some(connect) = connectwhere { if let Some(connect) = connectwhere {
captions_needed.push(( captions_needed.push((
((my_loc.x as i64 - ex_coord.x).abs() + (my_loc.y as i64 - ex_coord.y).abs()) ((my_loc.x as i64 - ex_coord.x).abs()
+ (my_loc.y as i64 - ex_coord.y).abs())
as usize, as usize,
"<<", connect "<<",
connect,
)) ))
} }
}, }
_ => buf.push(' ') _ => buf.push(' '),
} }
} else { } else {
buf.push_str(" "); buf.push_str(" ");
@ -260,34 +313,46 @@ pub fn render_lmap_dynroom<'l, 'm>(
} }
for x in min_x..max_x { for x in min_x..max_x {
let mut coord = room::GridCoords { x, y, z: my_loc.z }; let mut coord = room::GridCoords { x, y, z: my_loc.z };
let coord_room: Option<&'l dynzone::Dynroom> = let coord_room: Option<&'l dynzone::Dynroom> = zone
zone.dyn_rooms.iter() .dyn_rooms
.find( .iter()
|(_, dr)| dr.grid_coords.x == x && .find(|(_, dr)| {
dr.grid_coords.y == y && dr.grid_coords.x == x && dr.grid_coords.y == y && dr.grid_coords.z == my_loc.z
dr.grid_coords.z == my_loc.z) })
.map(|(_, r)| r); .map(|(_, r)| r);
match coord_room.and_then( match coord_room
|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTH)) { .and_then(|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTH))
{
Some(_) => buf.push_str(" | "), Some(_) => buf.push_str(" | "),
None if main_exit == Some(&(coord.clone(), Direction::NORTH)) => None if main_exit == Some(&(coord.clone(), Direction::NORTH)) => {
buf.push_str(" | "), buf.push_str(" | ")
}
None => buf.push_str(" "), None => buf.push_str(" "),
} }
let has_se = coord_room.and_then( let has_se = coord_room
|r| r.exits.iter().find(|ex| ex.direction == Direction::SOUTHEAST)) .and_then(|r| {
.is_some() || (main_exit == Some(&(coord.clone(), Direction::NORTHWEST))); r.exits
.iter()
.find(|ex| ex.direction == Direction::SOUTHEAST)
})
.is_some()
|| (main_exit == Some(&(coord.clone(), Direction::NORTHWEST)));
coord.y += 1; coord.y += 1;
let coord_room_s = let coord_room_s = zone
zone.dyn_rooms.iter() .dyn_rooms
.find( .iter()
|(_, dr)| dr.grid_coords.x == x && .find(|(_, dr)| {
dr.grid_coords.y == y && dr.grid_coords.x == x && dr.grid_coords.y == y && dr.grid_coords.z == my_loc.z
dr.grid_coords.z == my_loc.z) })
.map(|(_, r)| r); .map(|(_, r)| r);
let has_ne = coord_room_s.and_then( let has_ne = coord_room_s
|r| r.exits.iter().find(|ex| ex.direction == Direction::NORTHEAST)) .and_then(|r| {
.is_some() || (main_exit == Some(&(coord, Direction::SOUTHWEST))); r.exits
.iter()
.find(|ex| ex.direction == Direction::NORTHEAST)
})
.is_some()
|| (main_exit == Some(&(coord, Direction::SOUTHWEST)));
if has_se && has_ne { if has_se && has_ne {
buf.push('X'); buf.push('X');
} else if has_se { } else if has_se {
@ -304,24 +369,30 @@ pub fn render_lmap_dynroom<'l, 'm>(
buf buf
} }
pub fn caption_lmap<'l>(captions: &Vec<(usize, &'l str, &'l str)>, width: usize, height: usize) -> String { pub fn caption_lmap<'l>(
captions: &Vec<(usize, &'l str, &'l str)>,
width: usize,
height: usize,
) -> String {
let mut buf = String::new(); let mut buf = String::new();
for room in captions.iter().take(height) { for room in captions.iter().take(height) {
buf.push_str(&format!(ansi!("{}<bold>: {:.*}<reset>\n"), room.1, width, room.2)); buf.push_str(&format!(
ansi!("{}<bold>: {:.*}<reset>\n"),
room.1, width, room.2
));
} }
buf buf
} }
#[async_trait] #[async_trait]
trait MapType { trait MapType {
async fn map_room(&self, ctx: &VerbContext<'_>, async fn map_room(&self, ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()>;
room: &room::Room) -> UResult<()>;
async fn map_room_dyn<'a>( async fn map_room_dyn<'a>(
&self, &self,
ctx: &VerbContext<'_>, ctx: &VerbContext<'_>,
zone: &'a dynzone::Dynzone, zone: &'a dynzone::Dynzone,
room: &'a dynzone::Dynroom, room: &'a dynzone::Dynroom,
zoneref: &str zoneref: &str,
) -> UResult<()>; ) -> UResult<()>;
} }
@ -329,15 +400,20 @@ pub struct LmapType;
#[async_trait] #[async_trait]
impl MapType for LmapType { impl MapType for LmapType {
async fn map_room(&self, ctx: &VerbContext<'_>, async fn map_room(&self, ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()> {
room: &room::Room) -> UResult<()> {
let mut captions: Vec<(usize, &'static str, &'static str)> = Vec::new(); let mut captions: Vec<(usize, &'static str, &'static str)> = Vec::new();
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&flow_around(&render_lmap(room, 9, 7, &mut captions), 45, ansi!("<reset> "), Some(&flow_around(
&caption_lmap(&captions, 14, 27), 31 &render_lmap(room, 9, 7, &mut captions),
)) 45,
).await?; ansi!("<reset> "),
&caption_lmap(&captions, 14, 27),
31,
)),
)
.await?;
Ok(()) Ok(())
} }
@ -346,41 +422,59 @@ impl MapType for LmapType {
ctx: &VerbContext<'_>, ctx: &VerbContext<'_>,
zone: &'a dynzone::Dynzone, zone: &'a dynzone::Dynzone,
room: &'a dynzone::Dynroom, room: &'a dynzone::Dynroom,
zoneref: &str zoneref: &str,
) -> UResult<()> { ) -> UResult<()> {
let mut captions: Vec<(usize, &str, &str)> = Vec::new(); let mut captions: Vec<(usize, &str, &str)> = Vec::new();
let connectwhere_name_opt: Option<String> = match zoneref.split_once("/") { let connectwhere_name_opt: Option<String> = match zoneref.split_once("/") {
None => None, None => None,
Some((zone_t, zone_c)) => { Some((zone_t, zone_c)) => {
let zone_item: Option<Arc<Item>> = ctx.trans.find_item_by_type_code(zone_t, zone_c).await?; let zone_item: Option<Arc<Item>> =
ctx.trans.find_item_by_type_code(zone_t, zone_c).await?;
match zone_item.as_ref().map(|v| v.as_ref()) { match zone_item.as_ref().map(|v| v.as_ref()) {
Some(Item { special_data: Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), ..}), Some(Item {
..}) => special_data:
match zone_exit.split_once("/") { Some(ItemSpecialData::DynzoneData {
zone_exit: Some(zone_exit),
..
}),
..
}) => match zone_exit.split_once("/") {
None => None, None => None,
Some((ex_t, ex_c)) => Some((ex_t, ex_c)) => {
match ctx.trans.find_item_by_type_code(ex_t, ex_c).await?.as_ref() { match ctx.trans.find_item_by_type_code(ex_t, ex_c).await?.as_ref() {
Some(dest_item) => Some( Some(dest_item) => Some(dest_item.display_for_sentence(
dest_item.display_for_sentence( !ctx.session_dat.less_explicit_mode,
!ctx.session_dat.less_explicit_mode, 1, true 1,
true,
)), )),
None => None None => None,
}
} }
}, },
_ => None, _ => None,
} }
} }
}; };
let lmap_str = let lmap_str = render_lmap_dynroom(
render_lmap_dynroom(zone, room, 9, 7, &mut captions, zone,
connectwhere_name_opt.as_ref().map(|v| v.as_str())); room,
ctx.trans.queue_for_session( 9,
7,
&mut captions,
connectwhere_name_opt.as_ref().map(|v| v.as_str()),
);
ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&flow_around(&lmap_str, Some(&flow_around(
45, ansi!("<reset> "), &lmap_str,
&caption_lmap(&captions, 14, 27), 31 45,
)) ansi!("<reset> "),
).await?; &caption_lmap(&captions, 14, 27),
31,
)),
)
.await?;
Ok(()) Ok(())
} }
} }
@ -389,12 +483,10 @@ pub struct GmapType;
#[async_trait] #[async_trait]
impl MapType for GmapType { impl MapType for GmapType {
async fn map_room(&self, ctx: &VerbContext<'_>, async fn map_room(&self, ctx: &VerbContext<'_>, room: &room::Room) -> UResult<()> {
room: &room::Room) -> UResult<()> { ctx.trans
ctx.trans.queue_for_session( .queue_for_session(ctx.session, Some(&render_map(room, 32, 18)))
ctx.session, .await?;
Some(&render_map(room, 32, 18))
).await?;
Ok(()) Ok(())
} }
@ -403,12 +495,11 @@ impl MapType for GmapType {
ctx: &VerbContext<'_>, ctx: &VerbContext<'_>,
zone: &'a dynzone::Dynzone, zone: &'a dynzone::Dynzone,
room: &'a dynzone::Dynroom, room: &'a dynzone::Dynroom,
_zoneref: &str _zoneref: &str,
) -> UResult<()> { ) -> UResult<()> {
ctx.trans.queue_for_session( ctx.trans
ctx.session, .queue_for_session(ctx.session, Some(&render_map_dyn(zone, room, 16, 9)))
Some(&render_map_dyn(zone, room, 16, 9)) .await?;
).await?;
Ok(()) Ok(())
} }
} }
@ -416,7 +507,12 @@ impl MapType for GmapType {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
verb: &str,
remaining: &str,
) -> UResult<()> {
if remaining.trim() != "" { if remaining.trim() != "" {
user_error("map commands don't take anything after them".to_owned())?; user_error("map commands don't take anything after them".to_owned())?;
} }
@ -424,30 +520,38 @@ impl UserVerb for Verb {
let map_type: Box<dyn MapType + Sync + Send> = match verb { let map_type: Box<dyn MapType + Sync + Send> = match verb {
"lmap" | "lm" => Box::new(LmapType), "lmap" | "lm" => Box::new(LmapType),
"gmap" | "gm" => Box::new(GmapType), "gmap" | "gm" => Box::new(GmapType),
_ => user_error("I don't know how to show that map type.".to_owned())? _ => user_error("I don't know how to show that map type.".to_owned())?,
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let (heretype, herecode) = player_item.location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let (heretype, herecode) = player_item
let room_item: Arc<Item> = ctx.trans.find_item_by_type_code(heretype, herecode).await? .location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
let room_item: Arc<Item> = ctx
.trans
.find_item_by_type_code(heretype, herecode)
.await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?; .ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?;
if room_item.item_type == "room" { if room_item.item_type == "room" {
let room = let room = room::room_map_by_code()
room::room_map_by_code().get(room_item.item_code.as_str()) .get(room_item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?; .ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
map_type.map_room(ctx, &room).await?; map_type.map_room(ctx, &room).await?;
} else if room_item.item_type == "dynroom" { } else if room_item.item_type == "dynroom" {
let (dynzone, dynroom) = match &room_item.special_data { let (dynzone, dynroom) = match &room_item.special_data {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => { Some(ItemSpecialData::DynroomData {
dynzone::DynzoneType::from_str(dynzone_code.as_str()) dynzone_code,
.and_then(|dz_t| dynroom_code,
dynzone::dynzone_by_type().get(&dz_t)) }) => dynzone::DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dz_t| dynzone::dynzone_by_type().get(&dz_t))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr))) .and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()).map(|dr| (dz, dr)))
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))? .ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?,
}, _ => user_error("Expected dynroom to have DynroomData".to_owned())?,
_ => user_error("Expected dynroom to have DynroomData".to_owned())?
}; };
map_type.map_room_dyn(ctx, &dynzone, &dynroom, &room_item.location).await?; map_type
.map_room_dyn(ctx, &dynzone, &dynroom, &room_item.location)
.await?;
} else { } else {
user_error("Can't map here".to_owned())?; user_error("Can't map here".to_owned())?;
} }

View File

@ -1,72 +1,85 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, get_player_item_or_fail, look,
get_player_item_or_fail, open::{attempt_open_immediate, is_door_in_direction, DoorSituation},
look, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
open::{DoorSituation, is_door_in_direction, attempt_open_immediate},
}; };
use async_trait::async_trait; #[double]
use crate::db::DBTrans;
use crate::{ use crate::{
DResult,
language, language,
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
static_content::{
room::{self, Direction, ExitType, ExitClimb, MaterialType},
dynzone::{dynzone_by_type, ExitTarget as DynExitTarget, DynzoneType},
},
models::{ models::{
item::{
Item,
ItemSpecialData,
SkillType,
LocationActionType,
DoorState,
ActiveClimb,
},
consent::ConsentType, consent::ConsentType,
item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType},
}, },
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{ services::{
check_consent,
combat::{change_health, handle_resurrect, stop_attacking_mut},
comms::broadcast_to_room, comms::broadcast_to_room,
skills::skill_check_and_grind, skills::skill_check_and_grind,
combat::{
stop_attacking_mut,
handle_resurrect,
change_health
}, },
check_consent, static_content::{
} dynzone::{dynzone_by_type, DynzoneType, ExitTarget as DynExitTarget},
room::{self, Direction, ExitClimb, ExitType, MaterialType},
},
DResult,
}; };
use std::sync::Arc;
use mockall_double::double;
#[double] use crate::db::DBTrans;
use std::time;
use ansi::ansi; use ansi::ansi;
use rand_distr::{Normal, Distribution}; use async_trait::async_trait;
use mockall_double::double;
use rand_distr::{Distribution, Normal};
use std::sync::Arc;
use std::time;
pub async fn announce_move(trans: &DBTrans, character: &Item, leaving: &Item, arriving: &Item) -> DResult<()> { pub async fn announce_move(
let msg_leaving_exp = format!("{} departs towards {}\n", trans: &DBTrans,
character: &Item,
leaving: &Item,
arriving: &Item,
) -> DResult<()> {
let msg_leaving_exp = format!(
"{} departs towards {}\n",
&character.display_for_sentence(true, 1, true), &character.display_for_sentence(true, 1, true),
&arriving.display); &arriving.display
let msg_leaving_nonexp = format!("{} departs towards {}\n", );
let msg_leaving_nonexp = format!(
"{} departs towards {}\n",
character.display_for_sentence(true, 1, false), character.display_for_sentence(true, 1, false),
arriving.display_less_explicit arriving
.display_less_explicit
.as_ref() .as_ref()
.unwrap_or(&arriving.display)); .unwrap_or(&arriving.display)
broadcast_to_room(trans, &format!("{}/{}", &leaving.item_type, &leaving.item_code), );
None, &msg_leaving_exp, Some(&msg_leaving_nonexp)).await?; 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), let msg_arriving_exp = format!(
&leaving.display); "{} arrives from {}\n",
let msg_arriving_nonexp = 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), character.display_for_sentence(true, 1, false),
leaving.display_less_explicit leaving
.display_less_explicit
.as_ref() .as_ref()
.unwrap_or(&leaving.display)); .unwrap_or(&leaving.display)
broadcast_to_room(trans, &format!("{}/{}", &arriving.item_type, &arriving.item_code), );
None, &msg_arriving_exp, Some(&msg_arriving_nonexp)).await?; broadcast_to_room(
trans,
&format!("{}/{}", &arriving.item_type, &arriving.item_code),
None,
&msg_arriving_exp,
Some(&msg_arriving_nonexp),
)
.await?;
Ok(()) Ok(())
} }
@ -75,60 +88,101 @@ async fn move_to_where(
use_location: &str, use_location: &str,
direction: &Direction, direction: &Direction,
mover_for_exit_check: Option<&mut Item>, mover_for_exit_check: Option<&mut Item>,
player_ctx: &mut Option<&mut VerbContext<'_>> player_ctx: &mut Option<&mut VerbContext<'_>>,
) -> UResult<(String, Option<Item>, Option<&'static ExitClimb>)> { ) -> UResult<(String, Option<Item>, Option<&'static ExitClimb>)> {
// Firstly check dynamic exits, since they apply to rooms and dynrooms... // 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) = trans.find_exact_dyn_exit(use_location, direction).await? {
return Ok((format!("{}/{}", return Ok((
&dynroom_result.item_type, format!(
&dynroom_result.item_code), Some(dynroom_result), None)); "{}/{}",
&dynroom_result.item_type, &dynroom_result.item_code
),
Some(dynroom_result),
None,
));
} }
let (heretype, herecode) = use_location.split_once("/").unwrap_or(("room", "repro_xv_chargen")); let (heretype, herecode) = use_location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype == "dynroom" { if heretype == "dynroom" {
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? { let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
None => user_error("Your current room has vanished!".to_owned())?, None => user_error("Your current room has vanished!".to_owned())?,
Some(v) => v Some(v) => v,
}; };
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() { let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code), Some(ItemSpecialData::DynroomData {
_ => user_error("Your current room is invalid!".to_owned())? 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) let dynzone = dynzone_by_type()
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?) .get(&DynzoneType::from_str(dynzone_code).ok_or_else(|| {
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?; 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("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()))?; .ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction) let exit = dynroom
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
return match exit.target { return match exit.target {
DynExitTarget::ExitZone => { DynExitTarget::ExitZone => {
let (zonetype, zonecode) = old_dynroom_item.location.split_once("/") let (zonetype, zonecode) = old_dynroom_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?; .ok_or_else(|| UserError("Invalid zone for your room".to_owned()))?;
let zoneitem = trans.find_item_by_type_code(zonetype, zonecode).await? let zoneitem = trans
.find_item_by_type_code(zonetype, zonecode)
.await?
.ok_or_else(|| UserError("Can't find your zone".to_owned()))?; .ok_or_else(|| UserError("Can't find your zone".to_owned()))?;
let zone_exit = match zoneitem.special_data.as_ref() { let zone_exit = match zoneitem.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) => Some(ItemSpecialData::DynzoneData {
user_error("That exit doesn't seem to go anywhere".to_owned())?, zone_exit: None, ..
Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit, }) => user_error("That exit doesn't seem to go anywhere".to_owned())?,
_ => user_error("The zone you are in has invalid data associated with it".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, None)) Ok((zone_exit.to_string(), None, None))
}, }
DynExitTarget::Intrazone { subcode } => { DynExitTarget::Intrazone { subcode } => {
let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await? let to_item = trans
.ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?; .find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode)
Ok((format!("{}/{}", &to_item.item_type, &to_item.item_code), Some(to_item), None)) .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),
None,
))
} }
};
} }
if heretype != "room" { if heretype != "room" {
user_error("Navigating outside rooms not yet supported.".to_owned())? user_error("Navigating outside rooms not yet supported.".to_owned())?
} }
let room = room::room_map_by_code().get(herecode) let room = room::room_map_by_code()
.get(herecode)
.ok_or_else(|| UserError("Can't find your current location".to_owned()))?; .ok_or_else(|| UserError("Can't find your current location".to_owned()))?;
let exit = room.exits.iter().find(|ex| ex.direction == *direction) let exit = room
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?; .ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
match exit.exit_type { match exit.exit_type {
@ -144,22 +198,26 @@ async fn move_to_where(
} }
} }
let new_room = let new_room = room::resolve_exit(room, exit)
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?; .ok_or_else(|| UserError("Can't find that room".to_owned()))?;
Ok((format!("room/{}", new_room.code), None, exit.exit_climb.as_ref())) Ok((
format!("room/{}", new_room.code),
None,
exit.exit_climb.as_ref(),
))
} }
pub async fn check_room_access(trans: &DBTrans, player: &Item, room: &Item) -> UResult<()> { 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("/")) { let (owner_t, owner_c) = match room.owner.as_ref().and_then(|o| o.split_once("/")) {
None => return Ok(()), None => return Ok(()),
Some(v) => v Some(v) => v,
}; };
if owner_t == &player.item_type && owner_c == &player.item_code { if owner_t == &player.item_type && owner_c == &player.item_code {
return Ok(()); return Ok(());
} }
let owner = match trans.find_item_by_type_code(owner_t, owner_c).await? { let owner = match trans.find_item_by_type_code(owner_t, owner_c).await? {
None => return Ok(()), None => return Ok(()),
Some(v) => v Some(v) => v,
}; };
if check_consent(trans, "enter", &ConsentType::Visit, player, &owner).await? { if check_consent(trans, "enter", &ConsentType::Visit, player, &owner).await? {
@ -170,7 +228,15 @@ pub async fn check_room_access(trans: &DBTrans, player: &Item, room: &Item) -> U
// We are asking hypothetically if they entered the room, could they fight // We are asking hypothetically if they entered the room, could they fight
// the owner? We won't save this yet. // the owner? We won't save this yet.
player_hypothet.location = room.refstr(); player_hypothet.location = room.refstr();
if check_consent(trans, "enter", &ConsentType::Fight, &player_hypothet, &owner).await? { if check_consent(
trans,
"enter",
&ConsentType::Fight,
&player_hypothet,
&owner,
)
.await?
{
return Ok(()); return Ok(());
} }
@ -180,29 +246,23 @@ pub async fn check_room_access(trans: &DBTrans, player: &Item, room: &Item) -> U
the owner here.").to_owned())? the owner here.").to_owned())?
} }
pub async fn handle_fall( pub async fn handle_fall(trans: &DBTrans, faller: &mut Item, fall_dist: u64) -> UResult<String> {
trans: &DBTrans,
faller: &mut Item,
fall_dist: u64
) -> UResult<String> {
// TODO depend on distance, armour, etc... // TODO depend on distance, armour, etc...
// This is deliberately less damage than real life for the distance, // This is deliberately less damage than real life for the distance,
// since we'll assume the wristpad provides reflexes to buffer some damage. // since we'll assume the wristpad provides reflexes to buffer some damage.
let damage_modifier = match faller.location.split_once("/") { let damage_modifier = match faller.location.split_once("/") {
Some((ltype, lcode)) if ltype == "room" => { Some((ltype, lcode)) if ltype == "room" => match room::room_map_by_code().get(lcode) {
match room::room_map_by_code().get(lcode) {
None => 1.0, None => 1.0,
Some(room) => match room.material_type { Some(room) => match room.material_type {
MaterialType::WaterSurface | MaterialType::Underwater => { MaterialType::WaterSurface | MaterialType::Underwater => {
return Ok("lands with a splash".to_owned()); return Ok("lands with a splash".to_owned());
}, }
MaterialType::Soft { damage_modifier } => damage_modifier, MaterialType::Soft { damage_modifier } => damage_modifier,
MaterialType::Normal => 1.0 MaterialType::Normal => 1.0,
}
}
}, },
_ => 1.0 },
_ => 1.0,
}; };
let modified_safe_distance = 5.0 / damage_modifier; let modified_safe_distance = 5.0 / damage_modifier;
@ -210,9 +270,10 @@ pub async fn handle_fall(
return Ok("lands softly".to_owned()); return Ok("lands softly".to_owned());
} }
// The force is proportional to the square root of the fall distance. // The force is proportional to the square root of the fall distance.
let damage = ((fall_dist as f64 - modified_safe_distance).sqrt() * 3.0 * damage_modifier * let damage = ((fall_dist as f64 - modified_safe_distance).sqrt()
Normal::new(1.0, 0.3)? * 3.0
.sample(&mut rand::thread_rng())) as i64; * damage_modifier
* Normal::new(1.0, 0.3)?.sample(&mut rand::thread_rng())) as i64;
if damage > 0 { if damage > 0 {
change_health(trans, -damage, faller, "You fell", "You fell").await?; change_health(trans, -damage, faller, "You fell", "You fell").await?;
@ -240,7 +301,7 @@ pub async fn attempt_move_immediate(
direction: &Direction, direction: &Direction,
// player_ctx should only be Some if called from queue_handler finish_command // 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. // for the orig_mover's queue, because might re-queue a move command.
mut player_ctx: &mut Option<&mut VerbContext<'_>> mut player_ctx: &mut Option<&mut VerbContext<'_>>,
) -> UResult<()> { ) -> UResult<()> {
let use_location = if orig_mover.death_data.is_some() { let use_location = if orig_mover.death_data.is_some() {
if orig_mover.item_type != "player" { if orig_mover.item_type != "player" {
@ -252,9 +313,16 @@ pub async fn attempt_move_immediate(
}; };
match is_door_in_direction(trans, direction, use_location).await? { match is_door_in_direction(trans, direction, use_location).await? {
DoorSituation::NoDoor | DoorSituation::NoDoor
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => {}, | DoorSituation::DoorOutOfRoom {
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, room_with_door, .. } => { state: DoorState { open: true, .. },
..
} => {}
DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. },
room_with_door,
..
} => {
check_room_access(trans, orig_mover, &room_with_door).await?; check_room_access(trans, orig_mover, &room_with_door).await?;
} }
_ => { _ => {
@ -262,12 +330,15 @@ pub async fn attempt_move_immediate(
match player_ctx.as_mut() { match player_ctx.as_mut() {
None => { None => {
// NPCs etc... open and move in one step, but can't unlock. // NPCs etc... open and move in one step, but can't unlock.
}, }
Some(actual_player_ctx) => { Some(actual_player_ctx) => {
// Players take an extra step. So tell them to come back. // Players take an extra step. So tell them to come back.
actual_player_ctx.session_dat.queue.push_front( actual_player_ctx
QueueCommand::Movement { direction: direction.clone() } .session_dat
); .queue
.push_front(QueueCommand::Movement {
direction: direction.clone(),
});
return Ok(()); return Ok(());
} }
} }
@ -275,8 +346,14 @@ pub async fn attempt_move_immediate(
} }
let mut mover = (*orig_mover).clone(); let mut mover = (*orig_mover).clone();
let (new_loc, new_loc_item, climb_opt) = let (new_loc, new_loc_item, climb_opt) = move_to_where(
move_to_where(trans, use_location, direction, Some(&mut mover), &mut player_ctx).await?; trans,
use_location,
direction,
Some(&mut mover),
&mut player_ctx,
)
.await?;
let mut skip_escape_check: bool = false; let mut skip_escape_check: bool = false;
let mut escape_check_only: bool = false; let mut escape_check_only: bool = false;
@ -286,17 +363,30 @@ pub async fn attempt_move_immediate(
if let Some(ctx) = player_ctx { if let Some(ctx) = player_ctx {
if let Some(active_climb) = mover.active_climb.clone() { if let Some(active_climb) = mover.active_climb.clone() {
skip_escape_check = true; // Already done if we get here. skip_escape_check = true; // Already done if we get here.
let skills = skill_check_and_grind(trans, &mut mover, &SkillType::Climb, let skills = skill_check_and_grind(
climb.difficulty as f64).await?; trans,
&mut mover,
&SkillType::Climb,
climb.difficulty as f64,
)
.await?;
let mut narrative = String::new(); let mut narrative = String::new();
if skills <= -0.25 { if skills <= -0.25 {
// Crit fail - they have fallen. // Crit fail - they have fallen.
let (fall_dist, from_room, to_room) = if climb.height < 0 { let (fall_dist, from_room, to_room) = if climb.height < 0 {
// At least they get to where they want to go! // At least they get to where they want to go!
mover.location = new_loc.clone(); mover.location = new_loc.clone();
(climb.height.abs() as u64 - active_climb.height, new_loc.to_owned(), use_location.to_owned()) (
climb.height.abs() as u64 - active_climb.height,
new_loc.to_owned(),
use_location.to_owned(),
)
} else { } else {
(active_climb.height, use_location.to_owned(), new_loc.to_owned()) (
active_climb.height,
use_location.to_owned(),
new_loc.to_owned(),
)
}; };
mover.active_climb = None; mover.active_climb = None;
let descriptor = handle_fall(&trans, &mut mover, fall_dist).await?; let descriptor = handle_fall(&trans, &mut mover, fall_dist).await?;
@ -315,17 +405,18 @@ pub async fn attempt_move_immediate(
&descriptor &descriptor
); );
trans.save_item_model(&mover).await?; trans.save_item_model(&mover).await?;
broadcast_to_room(&trans, &from_room, broadcast_to_room(&trans, &from_room, None, &msg_exp, Some(&msg_nonexp))
None, &msg_exp, Some(&msg_nonexp)).await?; .await?;
broadcast_to_room(&trans, &to_room, broadcast_to_room(&trans, &to_room, None, &msg_exp, Some(&msg_nonexp)).await?;
None, &msg_exp, Some(&msg_nonexp)).await?;
ctx.session_dat.queue.truncate(0); ctx.session_dat.queue.truncate(0);
return Ok(()); return Ok(());
} else if skills <= 0.0 { } else if skills <= 0.0 {
if climb.height >= 0 { if climb.height >= 0 {
narrative.push_str("You lose your grip and slide a metre back down"); narrative.push_str("You lose your grip and slide a metre back down");
} else { } else {
narrative.push_str("You struggle to find a foothold and reluctantly climb a metre back up"); 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 let Some(ac) = mover.active_climb.as_mut() {
if ac.height > 0 { if ac.height > 0 {
@ -344,45 +435,63 @@ pub async fn attempt_move_immediate(
} }
if let Some(ac) = mover.active_climb.as_ref() { if let Some(ac) = mover.active_climb.as_ref() {
if climb.height >= 0 && ac.height >= climb.height as u64 { if climb.height >= 0 && ac.height >= climb.height as u64 {
trans.queue_for_session(&ctx.session, trans
Some("You brush yourself off and finish climbing - you \ .queue_for_session(
made it to the top!\n")).await?; &ctx.session,
Some(
"You brush yourself off and finish climbing - you \
made it to the top!\n",
),
)
.await?;
mover.active_climb = None; mover.active_climb = None;
} else if climb.height < 0 && ac.height >= (-climb.height) as u64 { } else if climb.height < 0 && ac.height >= (-climb.height) as u64 {
trans.queue_for_session(&ctx.session, trans
Some("You brush yourself off and finish climbing - you \ .queue_for_session(
made it down!\n")).await?; &ctx.session,
Some(
"You brush yourself off and finish climbing - you \
made it down!\n",
),
)
.await?;
mover.active_climb = None; mover.active_climb = None;
} else { } else {
let progress_quant = (((ac.height as f64) / (climb.height.abs() as f64)) * 10.0) as u64; let progress_quant =
(((ac.height as f64) / (climb.height.abs() as f64)) * 10.0) as u64;
trans.queue_for_session( trans.queue_for_session(
&ctx.session, &ctx.session,
Some(&format!(ansi!("<bold>[<reset><cyan>{}{}<reset><bold>] [<reset>{}/{} m<bold>]<reset> {}\n"), Some(&format!(ansi!("<bold>[<reset><cyan>{}{}<reset><bold>] [<reset>{}/{} m<bold>]<reset> {}\n"),
"=".repeat(progress_quant as usize), " ".repeat((10 - progress_quant) as usize), "=".repeat(progress_quant as usize), " ".repeat((10 - progress_quant) as usize),
ac.height, climb.height.abs(), &narrative ac.height, climb.height.abs(), &narrative
))).await?; ))).await?;
ctx.session_dat.queue.push_front( ctx.session_dat.queue.push_front(QueueCommand::Movement {
QueueCommand::Movement { direction: direction.clone() } direction: direction.clone(),
); });
trans.save_item_model(&mover).await?; trans.save_item_model(&mover).await?;
return Ok(()); return Ok(());
} }
} }
} else { } else {
let msg_exp = format!("{} starts climbing {}\n", let msg_exp = format!(
"{} starts climbing {}\n",
&orig_mover.display_for_sentence(true, 1, true), &orig_mover.display_for_sentence(true, 1, true),
&direction.describe_climb(if climb.height > 0 { "up" } else { "down" })); &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() }
); );
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; escape_check_only = true;
} }
} else { } else {
@ -391,7 +500,11 @@ pub async fn attempt_move_immediate(
} }
if !skip_escape_check { if !skip_escape_check {
match mover.active_combat.as_ref().and_then(|ac| ac.attacking.clone()) { match mover
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.clone())
{
None => {} None => {}
Some(old_victim) => { Some(old_victim) => {
if let Some((vcode, vtype)) = old_victim.split_once("/") { if let Some((vcode, vtype)) = old_victim.split_once("/") {
@ -403,7 +516,12 @@ pub async fn attempt_move_immediate(
} }
} }
} }
match mover.active_combat.clone().as_ref().map(|ac| &ac.attacked_by[..]) { match mover
.active_combat
.clone()
.as_ref()
.map(|ac| &ac.attacked_by[..])
{
None | Some([]) => {} None | Some([]) => {}
Some(attackers) => { Some(attackers) => {
let mut attacker_names = Vec::new(); let mut attacker_names = Vec::new();
@ -418,13 +536,30 @@ pub async fn attempt_move_immediate(
} }
} }
} }
let attacker_names_ref = attacker_names.iter().map(|n| n.as_str()).collect::<Vec<&str>>(); 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[..]); 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 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() { if let Some(ctx) = player_ctx.as_ref() {
trans.queue_for_session(ctx.session, trans
Some(&format!("You successfully get away from {}\n", .queue_for_session(
&attacker_names_str))).await?; ctx.session,
Some(&format!(
"You successfully get away from {}\n",
&attacker_names_str
)),
)
.await?;
} }
for item in &attacker_items[..] { for item in &attacker_items[..] {
let mut item_mut = (**item).clone(); let mut item_mut = (**item).clone();
@ -433,9 +568,15 @@ pub async fn attempt_move_immediate(
} }
} else { } else {
if let Some(ctx) = player_ctx.as_ref() { if let Some(ctx) = player_ctx.as_ref() {
trans.queue_for_session(ctx.session, trans
Some(&format!("You try and fail to run past {}\n", .queue_for_session(
&attacker_names_str))).await?; ctx.session,
Some(&format!(
"You try and fail to run past {}\n",
&attacker_names_str
)),
)
.await?;
} }
trans.save_item_model(&mover).await?; trans.save_item_model(&mover).await?;
return Ok(()); return Ok(());
@ -465,11 +606,18 @@ pub async fn attempt_move_immediate(
} }
if let Some((old_loc_type, old_loc_code)) = use_location.split_once("/") { 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(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_loc_type, new_loc_code)) = new_loc.split_once("/") {
if let Some(new_room_item) = match new_loc_item { if let Some(new_room_item) = match new_loc_item {
None => trans.find_item_by_type_code(new_loc_type, new_loc_code).await?, None => {
v => v.map(Arc::new) 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(&trans, &mover, &old_room_item, &new_room_item).await?;
} }
@ -483,17 +631,23 @@ pub async fn attempt_move_immediate(
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, _ctx: &mut VerbContext<'_>, _command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
_ctx: &mut VerbContext<'_>,
_command: &QueueCommand,
) -> UResult<time::Duration> {
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let direction = match command { let direction = match command {
QueueCommand::Movement { direction } => direction, QueueCommand::Movement { direction } => direction,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let player_item = get_player_item_or_fail(ctx).await?; 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(ctx.trans, &player_item, direction, &mut Some(ctx)).await?;
@ -505,11 +659,21 @@ pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> { async fn handle(
let dir = Direction::parse( self: &Self,
&(verb.to_owned() + " " + remaining.trim()).trim()) 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()))?; .ok_or_else(|| UserError("Unknown direction".to_owned()))?;
queue_command(ctx, &QueueCommand::Movement { direction: dir.clone() }).await queue_command(
ctx,
&QueueCommand::Movement {
direction: dir.clone(),
},
)
.await
} }
} }
static VERB_INT: Verb = Verb; static VERB_INT: Verb = Verb;

View File

@ -1,35 +1,27 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error, get_player_item_or_fail, look::direction_to_item, user_error, UResult, UserError, UserVerb,
get_player_item_or_fail, UserVerbRef, VerbContext,
look::direction_to_item,
}; };
use async_trait::async_trait; #[double]
use crate::db::DBTrans;
use crate::{ use crate::{
DResult,
regular_tasks::{
TaskRunContext,
TaskHandler,
queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
},
static_content::{
room::Direction,
possession_type::possession_data,
},
models::{ models::{
item::{Item, LocationActionType, DoorState}, item::{DoorState, Item, LocationActionType},
task::{Task, TaskMeta, TaskDetails} task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{
queued_command::{queue_command, QueueCommand, QueueCommandHandler},
TaskHandler, TaskRunContext,
}, },
services::comms::broadcast_to_room, services::comms::broadcast_to_room,
static_content::{possession_type::possession_data, room::Direction},
DResult,
}; };
use std::sync::Arc; use async_trait::async_trait;
use std::time;
use chrono::{self, Utc}; use chrono::{self, Utc};
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; use std::sync::Arc;
use std::time;
#[derive(Clone)] #[derive(Clone)]
pub struct SwingShutHandler; pub struct SwingShutHandler;
@ -39,33 +31,59 @@ pub static SWING_SHUT_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &Swing
impl TaskHandler for SwingShutHandler { impl TaskHandler for SwingShutHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (room_str, direction) = match &ctx.task.details { let (room_str, direction) = match &ctx.task.details {
TaskDetails::SwingShut { room_item, direction } => (room_item, direction), TaskDetails::SwingShut {
_ => { return Ok(None); } room_item,
direction,
} => (room_item, direction),
_ => {
return Ok(None);
}
}; };
let (room_item_type, room_item_code) = match room_str.split_once("/") { let (room_item_type, room_item_code) = match room_str.split_once("/") {
None => { return Ok(None); }, None => {
Some(v) => v return Ok(None);
}
Some(v) => v,
}; };
let room_item = match ctx.trans.find_item_by_type_code(room_item_type, room_item_code).await? { let room_item = match ctx
None => { return Ok(None); }, .trans
Some(v) => v .find_item_by_type_code(room_item_type, room_item_code)
.await?
{
None => {
return Ok(None);
}
Some(v) => v,
}; };
let mut room_item_mut = (*room_item).clone(); let mut room_item_mut = (*room_item).clone();
let mut door_state = match room_item_mut.door_states.as_mut().and_then(|ds| ds.get_mut(&direction)) { let mut door_state = match room_item_mut
None => { return Ok(None); }, .door_states
Some(v) => v .as_mut()
.and_then(|ds| ds.get_mut(&direction))
{
None => {
return Ok(None);
}
Some(v) => v,
}; };
(*door_state).open = false; (*door_state).open = false;
ctx.trans.save_item_model(&room_item_mut).await?; ctx.trans.save_item_model(&room_item_mut).await?;
let msg = format!("The door to the {} swings shut with a click.\n", let msg = format!(
&direction.describe()); "The door to the {} swings shut with a click.\n",
&direction.describe()
);
broadcast_to_room(&ctx.trans, &room_str, None, &msg, Some(&msg)).await?; broadcast_to_room(&ctx.trans, &room_str, None, &msg, Some(&msg)).await?;
if let Ok(Some(other_room)) = direction_to_item(&ctx.trans, &room_str, &direction).await { if let Ok(Some(other_room)) = direction_to_item(&ctx.trans, &room_str, &direction).await {
let msg = format!("The door to the {} swings shut with a click.\n", let msg = format!(
&direction.reverse().map(|d| d.describe()).unwrap_or_else(|| "outside".to_owned())); "The door to the {} swings shut with a click.\n",
&direction
.reverse()
.map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned())
);
broadcast_to_room(&ctx.trans, &other_room.refstr(), None, &msg, Some(&msg)).await?; broadcast_to_room(&ctx.trans, &other_room.refstr(), None, &msg, Some(&msg)).await?;
} }
@ -73,29 +91,50 @@ impl TaskHandler for SwingShutHandler {
} }
} }
pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut VerbContext<'_>>, pub async fn attempt_open_immediate(
who: &Item, direction: &Direction) -> UResult<()> { trans: &DBTrans,
ctx_opt: &mut Option<&mut VerbContext<'_>>,
who: &Item,
direction: &Direction,
) -> UResult<()> {
let use_location = if who.death_data.is_some() { let use_location = if who.death_data.is_some() {
user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())? user_error("Your ethereal hands don't seem to be able to move the door.".to_owned())?
} else { } else {
&who.location &who.location
}; };
let (room_1, dir_in_room, room_2) = match is_door_in_direction(trans, &direction, use_location).await? { let (room_1, dir_in_room, room_2) =
match is_door_in_direction(trans, &direction, use_location).await? {
DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?, DoorSituation::NoDoor => user_error("There is no door to open.".to_owned())?,
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } | DoorSituation::DoorIntoRoom {
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => state: DoorState { open: true, .. },
user_error("The door is already open.".to_owned())?, ..
DoorSituation::DoorIntoRoom { room_with_door, current_room, .. } => { }
| 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(); let entering_room_loc = room_with_door.refstr();
if let Some(revdir) = direction.reverse() { if let Some(revdir) = direction.reverse() {
if let Some(lock) = trans.find_by_action_and_location( if let Some(lock) = trans
.find_by_action_and_location(
&entering_room_loc, &entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await?.first() &LocationActionType::InstalledOnDoorAsLock(revdir.clone()),
)
.await?
.first()
{ {
if let Some(ctx) = ctx_opt { if let Some(ctx) = ctx_opt {
if let Some(lockcheck) = lock.possession_type.as_ref() if let Some(lockcheck) = lock
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.lockcheck_handler) { .and_then(|pd| pd.lockcheck_handler)
{
lockcheck.cmd(ctx, &who, &lock).await? lockcheck.cmd(ctx, &who, &lock).await?
} }
} else { } else {
@ -111,12 +150,15 @@ pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut V
} }
trans.save_item_model(&entering_room_mut).await?; trans.save_item_model(&entering_room_mut).await?;
(room_with_door, revdir, current_room) (room_with_door, revdir, current_room)
} else { } else {
user_error("There's no door possible there.".to_owned())? user_error("There's no door possible there.".to_owned())?
} }
}, }
DoorSituation::DoorOutOfRoom { room_with_door, new_room, .. } => { DoorSituation::DoorOutOfRoom {
room_with_door,
new_room,
..
} => {
let mut entering_room_mut = (*room_with_door).clone(); 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_map) = entering_room_mut.door_states.as_mut() {
if let Some(door) = door_map.get_mut(&direction) { if let Some(door) = door_map.get_mut(&direction) {
@ -128,27 +170,36 @@ pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut V
} }
}; };
for (loc, dir) in [(&room_1.refstr(), &dir_in_room.describe()), for (loc, dir) in [
(&room_2.refstr(), &dir_in_room.reverse().map(|d| d.describe()) (&room_1.refstr(), &dir_in_room.describe()),
.unwrap_or_else(|| "outside".to_owned()))] { (
&room_2.refstr(),
&dir_in_room
.reverse()
.map(|d| d.describe())
.unwrap_or_else(|| "outside".to_owned()),
),
] {
broadcast_to_room( broadcast_to_room(
&trans, &trans,
loc, loc,
None, None,
&format!("{} opens the door to the {}.\n", &format!(
"{} opens the door to the {}.\n",
&who.display_for_sentence(true, 1, true), &who.display_for_sentence(true, 1, true),
dir dir
), ),
Some( Some(&format!(
&format!("{} opens the door to the {}.\n", "{} opens the door to the {}.\n",
&who.display_for_sentence(false, 1, true), &who.display_for_sentence(false, 1, true),
dir dir
)),
) )
) .await?;
).await?;
} }
trans.upsert_task(&Task { trans
.upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()), task_code: format!("{}/{}", &room_1.refstr(), &direction.describe()),
next_scheduled: Utc::now() + chrono::Duration::seconds(120), next_scheduled: Utc::now() + chrono::Duration::seconds(120),
@ -156,9 +207,10 @@ pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut V
}, },
details: TaskDetails::SwingShut { details: TaskDetails::SwingShut {
room_item: room_1.refstr(), room_item: room_1.refstr(),
direction: dir_in_room.clone() direction: dir_in_room.clone(),
} },
}).await?; })
.await?;
Ok(()) Ok(())
} }
@ -166,11 +218,14 @@ pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut V
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let direction = match command { let direction = match command {
QueueCommand::OpenDoor { direction } => direction, QueueCommand::OpenDoor { direction } => direction,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let use_location = if player_item.death_data.is_some() { let use_location = if player_item.death_data.is_some() {
@ -180,19 +235,35 @@ impl QueueCommandHandler for QueueHandler {
}; };
match is_door_in_direction(&ctx.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::NoDoor => user_error("There is no door to open.".to_owned())?,
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } | DoorSituation::DoorIntoRoom {
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } => state: DoorState { open: true, .. },
user_error("The door is already open.".to_owned())?, ..
DoorSituation::DoorIntoRoom { room_with_door: entering_room, .. } => { }
| 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(); let entering_room_loc = entering_room.refstr();
if let Some(revdir) = direction.reverse() { if let Some(revdir) = direction.reverse() {
if let Some(lock) = ctx.trans.find_by_action_and_location( if let Some(lock) = ctx
.trans
.find_by_action_and_location(
&entering_room_loc, &entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir)).await?.first() &LocationActionType::InstalledOnDoorAsLock(revdir),
)
.await?
.first()
{ {
if let Some(lockcheck) = lock.possession_type.as_ref() if let Some(lockcheck) = lock
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.lockcheck_handler) { .and_then(|pd| pd.lockcheck_handler)
{
lockcheck.cmd(ctx, &player_item, &lock).await? lockcheck.cmd(ctx, &player_item, &lock).await?
} }
} }
@ -205,11 +276,14 @@ impl QueueCommandHandler for QueueHandler {
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let direction = match command { let direction = match command {
QueueCommand::OpenDoor { direction } => direction, QueueCommand::OpenDoor { direction } => direction,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let player_item = get_player_item_or_fail(ctx).await?; 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.trans, &mut Some(ctx), &player_item, &direction).await?;
@ -220,35 +294,54 @@ impl QueueCommandHandler for QueueHandler {
pub enum DoorSituation { pub enum DoorSituation {
NoDoor, NoDoor,
DoorIntoRoom { state: DoorState, room_with_door: Arc<Item>, current_room: Arc<Item> }, // Can be locked etc... DoorIntoRoom {
DoorOutOfRoom { state: DoorState, room_with_door: Arc<Item>, new_room: Arc<Item> } // No lockable. 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(trans: &DBTrans, direction: &Direction, use_location: &str) -> pub async fn is_door_in_direction(
UResult<DoorSituation> { trans: &DBTrans,
let (loc_type_t, loc_type_c) = use_location.split_once("/") direction: &Direction,
use_location: &str,
) -> UResult<DoorSituation> {
let (loc_type_t, loc_type_c) = use_location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?; .ok_or_else(|| UserError("Invalid location".to_owned()))?;
let cur_loc_item = 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()))?; .ok_or_else(|| UserError("Can't find your current location anymore.".to_owned()))?;
let new_loc_item = direction_to_item(trans, use_location, direction).await? let new_loc_item = direction_to_item(trans, use_location, direction)
.await?
.ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?; .ok_or_else(|| UserError("That exit doesn't really seem to go anywhere!".to_owned()))?;
if let Some(door_state) = if let Some(door_state) = cur_loc_item
cur_loc_item.door_states.as_ref() .door_states
.and_then(|v| v.get(direction)) { .as_ref()
.and_then(|v| v.get(direction))
{
return Ok(DoorSituation::DoorOutOfRoom { return Ok(DoorSituation::DoorOutOfRoom {
state: door_state.clone(), state: door_state.clone(),
room_with_door: cur_loc_item, room_with_door: cur_loc_item,
new_room: new_loc_item new_room: new_loc_item,
}); });
} }
if let Some(door_state) = if let Some(door_state) = new_loc_item.door_states.as_ref().and_then(|v| {
new_loc_item.door_states.as_ref() direction
.and_then(|v| direction.reverse().as_ref() .reverse()
.and_then(|rev| v.get(rev).map(|door| door.clone()))) { .as_ref()
.and_then(|rev| v.get(rev).map(|door| door.clone()))
}) {
return Ok(DoorSituation::DoorIntoRoom { return Ok(DoorSituation::DoorIntoRoom {
state: door_state.clone(), state: door_state.clone(),
room_with_door: new_loc_item, room_with_door: new_loc_item,
current_room: cur_loc_item current_room: cur_loc_item,
}); });
} }
Ok(DoorSituation::NoDoor) Ok(DoorSituation::NoDoor)
@ -258,10 +351,21 @@ pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
let dir = Direction::parse(remaining) self: &Self,
.ok_or_else(|| UserError("Unknown direction".to_owned()))?; ctx: &mut VerbContext,
queue_command(ctx, &QueueCommand::OpenDoor { direction: dir.clone() }).await?; _verb: &str,
remaining: &str,
) -> UResult<()> {
let dir =
Direction::parse(remaining).ok_or_else(|| UserError("Unknown direction".to_owned()))?;
queue_command(
ctx,
&QueueCommand::OpenDoor {
direction: dir.clone(),
},
)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -1,19 +1,27 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{
ItemSearchParams, user_error, get_player_item_or_fail, is_likely_explicit, parsing::parse_to_space, search_item_for_user,
get_player_item_or_fail, is_likely_explicit, user_error, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
search_item_for_user, };
parsing::parse_to_space}; use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait; use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
verb: &str,
remaining: &str,
) -> UResult<()> {
let (to_whom_name, say_what_raw) = if verb.starts_with("r") { let (to_whom_name, say_what_raw) = if verb.starts_with("r") {
let last_page_from = match ctx.user_dat.as_ref().and_then(|u| u.last_page_from.as_ref()) { let last_page_from = match ctx
.user_dat
.as_ref()
.and_then(|u| u.last_page_from.as_ref())
{
None => user_error("No one has paged you, so you can't reply.".to_owned())?, None => user_error("No one has paged you, so you can't reply.".to_owned())?,
Some(m) => (*m).clone() Some(m) => (*m).clone(),
}; };
(last_page_from, remaining) (last_page_from, remaining)
} else { } else {
@ -28,22 +36,31 @@ impl UserVerb for Verb {
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Shush, the dead can't talk!".to_string())?; user_error("Shush, the dead can't talk!".to_string())?;
} }
let to_whom = search_item_for_user(ctx, &ItemSearchParams { let to_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_active_players: true, include_active_players: true,
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, &to_whom_name) ..ItemSearchParams::base(&player_item, &to_whom_name)
}).await?; },
)
.await?;
match to_whom.item_type.as_str() { match to_whom.item_type.as_str() {
"player" => {}, "player" => {}
_ => user_error("Only players accept pages".to_string())? _ => user_error("Only players accept pages".to_string())?,
} }
ctx.trans.queue_for_session(ctx.session, Some(&format!( ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<blue>You page {} on your wristpad: \"{}\"<reset>\n"), ansi!("<blue>You page {} on your wristpad: \"{}\"<reset>\n"),
to_whom.display_for_session(&ctx.session_dat), to_whom.display_for_session(&ctx.session_dat),
say_what say_what
))).await?; )),
)
.await?;
if player_item == to_whom { if player_item == to_whom {
return Ok(()); return Ok(());
@ -51,7 +68,11 @@ impl UserVerb for Verb {
match to_whom.item_type.as_str() { match to_whom.item_type.as_str() {
"player" => { "player" => {
match ctx.trans.find_session_for_player(&to_whom.item_code).await? { match ctx
.trans
.find_session_for_player(&to_whom.item_code)
.await?
{
None => user_error("That character is asleep.".to_string())?, None => user_error("That character is asleep.".to_string())?,
Some((other_session, other_session_dets)) => { Some((other_session, other_session_dets)) => {
if other_session_dets.less_explicit_mode && is_likely_explicit(&say_what) { if other_session_dets.less_explicit_mode && is_likely_explicit(&say_what) {
@ -59,7 +80,9 @@ impl UserVerb for Verb {
content, and your message looked explicit, so it wasn't sent." content, and your message looked explicit, so it wasn't sent."
.to_owned())? .to_owned())?
} else { } else {
if let Some(mut user) = ctx.trans.find_by_username(&to_whom.item_code).await? { if let Some(mut user) =
ctx.trans.find_by_username(&to_whom.item_code).await?
{
user.last_page_from = Some(player_item.item_code.clone()); user.last_page_from = Some(player_item.item_code.clone());
ctx.trans.save_user_model(&user).await?; ctx.trans.save_user_model(&user).await?;
} }
@ -71,7 +94,7 @@ impl UserVerb for Verb {
} }
} }
} }
}, }
_ => {} _ => {}
} }

View File

@ -1,22 +1,22 @@
use super::allow::{AllowCommand, ConsentDetails, ConsentTarget};
use crate::models::consent::ConsentType;
use ansi::{ansi, strip_special_characters};
use nom::{ use nom::{
bytes::complete::{take_till, take_till1, take_while},
character::{complete::{space0, space1, alpha1, one_of, char, u8, u16}},
combinator::{recognize, fail, eof},
sequence::terminated,
branch::alt, branch::alt,
bytes::complete::{take_till, take_till1, take_while},
character::complete::{alpha1, char, one_of, space0, space1, u16, u8},
combinator::{eof, fail, recognize},
error::{context, VerboseError, VerboseErrorKind}, error::{context, VerboseError, VerboseErrorKind},
sequence::terminated,
IResult, IResult,
}; };
use super::allow::{AllowCommand, ConsentTarget, ConsentDetails};
use ansi::{ansi, strip_special_characters};
use crate::models::consent::ConsentType;
pub fn parse_command_name(input: &str) -> (&str, &str) { pub fn parse_command_name(input: &str) -> (&str, &str) {
fn parse(input: &str) -> IResult<&str, &str> { fn parse(input: &str) -> IResult<&str, &str> {
let (input, _) = space0(input)?; let (input, _) = space0(input)?;
let (input, cmd) = alt(( let (input, cmd) = alt((
recognize(one_of("-\"':.")), recognize(one_of("-\"':.")),
take_till1(|c| c == ' ' || c == '\t') take_till1(|c| c == ' ' || c == '\t'),
))(input)?; ))(input)?;
let (input, _) = space0(input)?; let (input, _) = space0(input)?;
Ok((input, cmd)) Ok((input, cmd))
@ -24,7 +24,7 @@ pub fn parse_command_name(input: &str) -> (&str, &str) {
match parse(input) { match parse(input) {
/* This parser only fails on empty / whitespace only strings. */ /* This parser only fails on empty / whitespace only strings. */
Err(_) => ("", ""), Err(_) => ("", ""),
Ok((rest, command)) => (command, rest) Ok((rest, command)) => (command, rest),
} }
} }
@ -34,7 +34,7 @@ pub fn parse_to_space(input: &str) -> (&str, &str) {
} }
match parser(input) { match parser(input) {
Err(_) => ("", ""), /* Impossible? */ Err(_) => ("", ""), /* Impossible? */
Ok((rest, token)) => (token, rest) Ok((rest, token)) => (token, rest),
} }
} }
@ -44,7 +44,7 @@ pub fn parse_offset(input: &str) -> (Option<u8>, &str) {
} }
match parser(input) { match parser(input) {
Err(_) => (None, input), Err(_) => (None, input),
Ok((rest, result)) => (Some(result), rest) Ok((rest, result)) => (Some(result), rest),
} }
} }
@ -54,7 +54,7 @@ pub fn parse_count(input: &str) -> (Option<u8>, &str) {
} }
match parser(input) { match parser(input) {
Err(_) => (None, input), Err(_) => (None, input),
Ok((rest, result)) => (Some(result), rest) Ok((rest, result)) => (Some(result), rest),
} }
} }
@ -62,21 +62,29 @@ pub fn parse_username(input: &str) -> Result<(&str, &str), &'static str> {
const CATCHALL_ERROR: &'static str = "Must only contain alphanumeric characters or _"; const CATCHALL_ERROR: &'static str = "Must only contain alphanumeric characters or _";
fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> { fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> {
let (input, l1) = context("Must start with a letter", alpha1)(input)?; let (input, l1) = context("Must start with a letter", alpha1)(input)?;
let (input, l2) = context(CATCHALL_ERROR, let (input, l2) = context(
take_while(|c: char| c.is_alphanumeric() || c == '_'))(input)?; CATCHALL_ERROR,
take_while(|c: char| c.is_alphanumeric() || c == '_'),
)(input)?;
if l1.len() + l2.len() > 20 { if l1.len() + l2.len() > 20 {
context("Limit of 20 characters", fail::<&str, &str, VerboseError<&str>>)(input)?; context(
"Limit of 20 characters",
fail::<&str, &str, VerboseError<&str>>,
)(input)?;
} }
Ok((input, ())) Ok((input, ()))
} }
match terminated(recognize(parse_valid), alt((space1, eof)))(input) { match terminated(recognize(parse_valid), alt((space1, eof)))(input) {
Ok((input, username)) => Ok((username, input)), Ok((input, username)) => Ok((username, input)),
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(e
Err(e.errors.into_iter().find_map(|k| match k.1 { .errors
.into_iter()
.find_map(|k| match k.1 {
VerboseErrorKind::Context(s) => Some(s), VerboseErrorKind::Context(s) => Some(s),
_ => None _ => None,
}).unwrap_or(CATCHALL_ERROR)), })
Err(_) => Err(CATCHALL_ERROR) .unwrap_or(CATCHALL_ERROR)),
Err(_) => Err(CATCHALL_ERROR),
} }
} }
@ -91,11 +99,11 @@ pub fn parse_on_or_default<'l>(input: &'l str, default_on: &'l str) -> (&'l str,
pub fn parse_duration_mins<'l>(input: &'l str) -> Result<(u64, &'l str), String> { pub fn parse_duration_mins<'l>(input: &'l str) -> Result<(u64, &'l str), String> {
let (input, number) = match u16::<&'l str, ()>(input) { let (input, number) = match u16::<&'l str, ()>(input) {
Err(_) => Err("Invalid number - duration should start with a number, e.g. 5 minutes")?, Err(_) => Err("Invalid number - duration should start with a number, e.g. 5 minutes")?,
Ok(n) => n Ok(n) => n,
}; };
let (tok, input) = match input.trim_start().split_once(" ") { let (tok, input) = match input.trim_start().split_once(" ") {
None => (input, ""), None => (input, ""),
Some(v) => v Some(v) => v,
}; };
Ok((match tok.to_lowercase().as_str() { Ok((match tok.to_lowercase().as_str() {
"min" | "mins" | "minute" | "minutes" => number as u64, "min" | "mins" | "minute" | "minutes" => number as u64,
@ -111,52 +119,57 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
ansi!("Usage: allow <lt>action> from <lt>user> <lt>options> | allow <lt>action> against <lt>corp> by <lt>corp> <lt>options>. Try <bold>help allow<reset> for more."); ansi!("Usage: allow <lt>action> from <lt>user> <lt>options> | allow <lt>action> against <lt>corp> by <lt>corp> <lt>options>. Try <bold>help allow<reset> for more.");
let (consent_type_s, input) = match input.trim_start().split_once(" ") { let (consent_type_s, input) = match input.trim_start().split_once(" ") {
None => Err(usage), None => Err(usage),
Some(v) => Ok(v) Some(v) => Ok(v),
}?; }?;
let consent_type = match ConsentType::from_str(&consent_type_s.trim().to_lowercase()) { let consent_type = match ConsentType::from_str(&consent_type_s.trim().to_lowercase()) {
None => Err( None => Err(if is_explicit {
if is_explicit { "Invalid consent type - options are fight, medicine, gifts, visit and sex" } else { "Invalid consent type - options are fight, medicine, gifts, visit and sex"
} else {
"Invalid consent type - options are fight, medicine, gifts and visit" "Invalid consent type - options are fight, medicine, gifts and visit"
}), }),
Some(ct) => Ok(ct) Some(ct) => Ok(ct),
}?; }?;
let (tok, mut input) = match input.trim_start().split_once(" ") { let (tok, mut input) = match input.trim_start().split_once(" ") {
None => Err(usage), None => Err(usage),
Some(v) => Ok(v) Some(v) => Ok(v),
}?; }?;
let tok_trim = tok.trim_start().to_lowercase(); let tok_trim = tok.trim_start().to_lowercase();
let consent_target = let consent_target = if tok_trim == "against" {
if tok_trim == "against" {
if consent_type != ConsentType::Fight { if consent_type != ConsentType::Fight {
Err("corps can only currently consent to fight, no other actions")? Err("corps can only currently consent to fight, no other actions")?
} else { } else {
let (my_corp_raw, new_input) = match input.trim_start().split_once(" ") { let (my_corp_raw, new_input) = match input.trim_start().split_once(" ") {
None => Err(usage), None => Err(usage),
Some(v) => Ok(v) Some(v) => Ok(v),
}?; }?;
let my_corp = my_corp_raw.trim_start(); let my_corp = my_corp_raw.trim_start();
let (tok, new_input) = match new_input.trim_start().split_once(" ") { let (tok, new_input) = match new_input.trim_start().split_once(" ") {
None => Err(usage), None => Err(usage),
Some(v) => Ok(v) Some(v) => Ok(v),
}?; }?;
if tok.trim_start().to_lowercase() != "by" { if tok.trim_start().to_lowercase() != "by" {
Err(usage)?; Err(usage)?;
} }
let (target_corp_raw, new_input) = match new_input.trim_start().split_once(" ") { let (target_corp_raw, new_input) = match new_input.trim_start().split_once(" ") {
None => (new_input.trim_start(), ""), None => (new_input.trim_start(), ""),
Some(v) => v Some(v) => v,
}; };
input = new_input; input = new_input;
ConsentTarget::CorpTarget { from_corp: my_corp, to_corp: target_corp_raw.trim_start() } ConsentTarget::CorpTarget {
from_corp: my_corp,
to_corp: target_corp_raw.trim_start(),
}
} }
} else if tok_trim == "from" { } else if tok_trim == "from" {
let (target_user_raw, new_input) = match input.trim_start().split_once(" ") { let (target_user_raw, new_input) = match input.trim_start().split_once(" ") {
None => (input.trim_start(), ""), None => (input.trim_start(), ""),
Some(v) => v Some(v) => v,
}; };
input = new_input; input = new_input;
ConsentTarget::UserTarget { to_user: target_user_raw.trim_start() } ConsentTarget::UserTarget {
to_user: target_user_raw.trim_start(),
}
} else { } else {
Err(usage)? Err(usage)?
}; };
@ -169,7 +182,7 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
} }
let (tok, new_input) = match input.split_once(" ") { let (tok, new_input) = match input.split_once(" ") {
None => (input, ""), None => (input, ""),
Some(v) => v Some(v) => v,
}; };
match tok.to_lowercase().as_str() { match tok.to_lowercase().as_str() {
"for" => { "for" => {
@ -180,7 +193,7 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"until" => { "until" => {
let (tok, new_input) = match new_input.split_once(" ") { let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""), None => (new_input, ""),
Some(v) => v Some(v) => v,
}; };
if tok.trim_start().to_lowercase() != "death" { if tok.trim_start().to_lowercase() != "death" {
Err("Option until needs to be followed with death - until death")? Err("Option until needs to be followed with death - until death")?
@ -191,7 +204,7 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"allow" => { "allow" => {
let (tok, new_input) = match new_input.split_once(" ") { let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""), None => (new_input, ""),
Some(v) => v Some(v) => v,
}; };
match tok.trim_start().to_lowercase().as_str() { match tok.trim_start().to_lowercase().as_str() {
"private" => { "private" => {
@ -210,7 +223,7 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"disallow" => { "disallow" => {
let (tok, new_input) = match new_input.split_once(" ") { let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""), None => (new_input, ""),
Some(v) => v Some(v) => v,
}; };
match tok.trim_start().to_lowercase().as_str() { match tok.trim_start().to_lowercase().as_str() {
"private" => { "private" => {
@ -226,17 +239,23 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"in" => { "in" => {
let (tok, new_input) = match new_input.split_once(" ") { let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""), None => (new_input, ""),
Some(v) => v Some(v) => v,
}; };
consent_details.only_in.push(tok); consent_details.only_in.push(tok);
input = new_input; input = new_input;
} }
_ => Err(format!("I don't understand the option \"{}\"", strip_special_characters(tok)))? _ => Err(format!(
"I don't understand the option \"{}\"",
strip_special_characters(tok)
))?,
} }
} }
Ok(AllowCommand {
Ok(AllowCommand { consent_type: consent_type, consent_target: consent_target, consent_details: consent_details }) consent_type: consent_type,
consent_target: consent_target,
consent_details: consent_details,
})
} }
#[cfg(test)] #[cfg(test)]
@ -245,28 +264,29 @@ mod tests {
#[test] #[test]
fn it_parses_normal_command() { fn it_parses_normal_command() {
assert_eq!(parse_command_name("help"), assert_eq!(parse_command_name("help"), ("help", ""));
("help", ""));
} }
#[test] #[test]
fn it_parses_normal_command_with_arg() { fn it_parses_normal_command_with_arg() {
assert_eq!(parse_command_name("help \t testing stuff"), assert_eq!(
("help", "testing stuff")); parse_command_name("help \t testing stuff"),
("help", "testing stuff")
);
} }
#[test] #[test]
fn it_parses_commands_with_leading_whitespace() { fn it_parses_commands_with_leading_whitespace() {
assert_eq!(parse_command_name(" \t \thelp \t testing stuff"), assert_eq!(
("help", "testing stuff")); parse_command_name(" \t \thelp \t testing stuff"),
("help", "testing stuff")
);
} }
#[test] #[test]
fn it_parses_empty_command_names() { fn it_parses_empty_command_names() {
assert_eq!(parse_command_name(""), assert_eq!(parse_command_name(""), ("", ""));
("", "")); assert_eq!(parse_command_name(" \t "), ("", ""));
assert_eq!(parse_command_name(" \t "),
("", ""));
} }
#[test] #[test]
@ -276,7 +296,10 @@ mod tests {
#[test] #[test]
fn it_parses_usernames_with_further_args() { fn it_parses_usernames_with_further_args() {
assert_eq!(parse_username("Wizard_123 with cat"), Ok(("Wizard_123", "with cat"))); assert_eq!(
parse_username("Wizard_123 with cat"),
Ok(("Wizard_123", "with cat"))
);
} }
#[test] #[test]
@ -306,12 +329,18 @@ mod tests {
#[test] #[test]
fn it_fails_on_usernames_with_bad_characters() { fn it_fails_on_usernames_with_bad_characters() {
assert_eq!(parse_username("Wizard!"), Err("Must only contain alphanumeric characters or _")); assert_eq!(
parse_username("Wizard!"),
Err("Must only contain alphanumeric characters or _")
);
} }
#[test] #[test]
fn it_fails_on_long_usernames() { fn it_fails_on_long_usernames() {
assert_eq!(parse_username("A23456789012345678901"), Err("Limit of 20 characters")); assert_eq!(
parse_username("A23456789012345678901"),
Err("Limit of 20 characters")
);
} }
#[test] #[test]
@ -339,22 +368,31 @@ mod tests {
#[test] #[test]
fn parse_consent_works_default_options_user() { fn parse_consent_works_default_options_user() {
assert_eq!(super::parse_allow("medicine From Athorina", false), assert_eq!(
super::parse_allow("medicine From Athorina", false),
Ok(AllowCommand { Ok(AllowCommand {
consent_type: ConsentType::Medicine, consent_type: ConsentType::Medicine,
consent_target: ConsentTarget::UserTarget { to_user: "Athorina" }, consent_target: ConsentTarget::UserTarget {
to_user: "Athorina"
},
consent_details: ConsentDetails::default_for(&ConsentType::Medicine) consent_details: ConsentDetails::default_for(&ConsentType::Medicine)
})) })
)
} }
#[test] #[test]
fn parse_consent_works_default_options_corp() { fn parse_consent_works_default_options_corp() {
assert_eq!(super::parse_allow("Fight Against megacorp By supercorp", false), assert_eq!(
super::parse_allow("Fight Against megacorp By supercorp", false),
Ok(AllowCommand { Ok(AllowCommand {
consent_type: ConsentType::Fight, consent_type: ConsentType::Fight,
consent_target: ConsentTarget::CorpTarget { from_corp: "megacorp", to_corp: "supercorp" }, consent_target: ConsentTarget::CorpTarget {
from_corp: "megacorp",
to_corp: "supercorp"
},
consent_details: ConsentDetails::default_for(&ConsentType::Fight) consent_details: ConsentDetails::default_for(&ConsentType::Fight)
})) })
)
} }
#[test] #[test]

View File

@ -1,15 +1,19 @@
use super::{ use super::{UResult, UserVerb, UserVerbRef, VerbContext};
VerbContext, UserVerb, UserVerbRef, UResult
};
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
ctx.trans.queue_for_session(ctx.session, self: &Self,
Some(ansi!("<red>Bye!<reset>\r\n"))).await?; ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
ctx.trans
.queue_for_session(ctx.session, Some(ansi!("<red>Bye!<reset>\r\n")))
.await?;
ctx.trans.queue_for_session(ctx.session, None).await?; ctx.trans.queue_for_session(ctx.session, None).await?;
Ok(()) Ok(())
} }

View File

@ -1,27 +1,37 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult}; use super::{parsing::parse_username, user_error};
use async_trait::async_trait; use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use super::{user_error, parsing::parse_username}; use crate::models::{
use crate::models::{user::User, item::{Item, Pronouns}}; item::{Item, Pronouns},
use chrono::Utc; user::User,
};
use ansi::ansi; use ansi::ansi;
use tokio::time; use async_trait::async_trait;
use chrono::Utc;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::collections::HashSet; use std::collections::HashSet;
use tokio::time;
pub fn is_invalid_username(name: &str) -> bool { pub fn is_invalid_username(name: &str) -> bool {
static INVALID_PREFIXES: OnceCell<Vec<&'static str>> = OnceCell::new(); static INVALID_PREFIXES: OnceCell<Vec<&'static str>> = OnceCell::new();
static INVALID_SUFFIXES: OnceCell<Vec<&'static str>> = OnceCell::new(); static INVALID_SUFFIXES: OnceCell<Vec<&'static str>> = OnceCell::new();
static INVALID_WORDS: OnceCell<HashSet<&'static str>> = OnceCell::new(); static INVALID_WORDS: OnceCell<HashSet<&'static str>> = OnceCell::new();
let invalid_prefixes = INVALID_PREFIXES.get_or_init(|| vec!( let invalid_prefixes =
"admin", "god", "helper", "npc", "corpse", "dead" INVALID_PREFIXES.get_or_init(|| vec!["admin", "god", "helper", "npc", "corpse", "dead"]);
)); let invalid_suffixes = INVALID_SUFFIXES.get_or_init(|| vec!["bot"]);
let invalid_suffixes = INVALID_SUFFIXES.get_or_init(|| vec!( let invalid_words = INVALID_WORDS.get_or_init(|| {
"bot" HashSet::from([
)); "corp",
let invalid_words = INVALID_WORDS.get_or_init(|| HashSet::from( "to",
["corp", "to", "from", "dog", "bot", "for", "against", "on", "from",
"privileges", "as"] "dog",
)); "bot",
"for",
"against",
"on",
"privileges",
"as",
])
});
if invalid_words.contains(name) { if invalid_words.contains(name) {
return true; return true;
} }
@ -38,20 +48,27 @@ pub fn is_invalid_username(name: &str) -> bool {
false false
} }
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (username, password, email) = match parse_username(remaining) { let (username, password, email) = match parse_username(remaining) {
Err(e) => user_error("Invalid username: ".to_owned() + e)?, Err(e) => user_error("Invalid username: ".to_owned() + e)?,
Ok((username, rest)) => { Ok((username, rest)) => match rest.split_whitespace().collect::<Vec<&str>>()[..] {
match rest.split_whitespace().collect::<Vec<&str>>()[..] {
[password, email] => (username, password, email), [password, email] => (username, password, email),
[] | [_] => user_error("Too few options to register - supply username, password, and email".to_owned())?, [] | [_] => user_error(
_ => user_error("Too many options to register - supply username, password, and email".to_owned())?, "Too few options to register - supply username, password, and email".to_owned(),
} )?,
} _ => user_error(
"Too many options to register - supply username, password, and email"
.to_owned(),
)?,
},
}; };
if is_invalid_username(&username.to_lowercase()) { if is_invalid_username(&username.to_lowercase()) {
@ -67,10 +84,14 @@ impl UserVerb for Verb {
if password.len() < 6 { if password.len() < 6 {
user_error("Password must be 6 characters long or longer".to_owned())?; user_error("Password must be 6 characters long or longer".to_owned())?;
} else if !validator::validate_email(email) { } else if !validator::validate_email(email) {
user_error("Please supply a valid email in case you need to reset your password.".to_owned())?; user_error(
"Please supply a valid email in case you need to reset your password.".to_owned(),
)?;
} }
let player_item_id = ctx.trans.create_item(&Item { let player_item_id = ctx
.trans
.create_item(&Item {
item_type: "player".to_owned(), item_type: "player".to_owned(),
item_code: username.to_lowercase(), item_code: username.to_lowercase(),
display: username.to_owned(), display: username.to_owned(),
@ -78,7 +99,8 @@ impl UserVerb for Verb {
location: "room/repro_xv_chargen".to_owned(), location: "room/repro_xv_chargen".to_owned(),
pronouns: Pronouns::default_animate(), pronouns: Pronouns::default_animate(),
..Item::default() ..Item::default()
}).await?; })
.await?;
// Force a wait to protect against abuse. // Force a wait to protect against abuse.
time::sleep(time::Duration::from_secs(5)).await; time::sleep(time::Duration::from_secs(5)).await;
@ -93,13 +115,19 @@ impl UserVerb for Verb {
}; };
*ctx.user_dat = Some(user_dat); *ctx.user_dat = Some(user_dat);
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&format!(ansi!("Welcome <bold>{}<reset>, you are now officially registered.\r\n"), Some(&format!(
&username)) ansi!("Welcome <bold>{}<reset>, you are now officially registered.\r\n"),
).await?; &username
)),
)
.await?;
super::agree::check_and_notify_accepts(ctx).await?; super::agree::check_and_notify_accepts(ctx).await?;
ctx.trans.create_user(ctx.session, ctx.user_dat.as_ref().unwrap()).await?; ctx.trans
.create_user(ctx.session, ctx.user_dat.as_ref().unwrap())
.await?;
Ok(()) Ok(())
} }

View File

@ -1,69 +1,73 @@
use super::{ use super::{
VerbContext, get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
UserVerb, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
UserVerbRef,
UResult,
ItemSearchParams,
UserError,
user_error,
get_player_item_or_fail,
search_items_for_user,
parsing::parse_count
}; };
use crate::{ use crate::{
models::item::{BuffCause, Item, LocationActionType},
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{comms::broadcast_to_room, skills::calculate_total_stats_skills_for_user},
static_content::possession_type::{possession_data, WearData}, static_content::possession_type::{possession_data, WearData},
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
services::{
comms::broadcast_to_room,
skills::calculate_total_stats_skills_for_user,
},
models::item::{Item, LocationActionType, BuffCause},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::time; use std::time;
async fn check_removeable(ctx: &mut VerbContext<'_>, async fn check_removeable(
item: &Item, player_item: &Item) -> UResult<()> { ctx: &mut VerbContext<'_>,
item: &Item,
player_item: &Item,
) -> UResult<()> {
if item.location != player_item.refstr() { if item.location != player_item.refstr() {
user_error(format!("You try to remove {} but realise you no longer have it.", user_error(format!(
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? "You try to remove {} but realise you no longer have it.",
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))?
} }
if item.action_type != LocationActionType::Worn { if item.action_type != LocationActionType::Worn {
user_error("You realise you're not wearing it!".to_owned())?; user_error("You realise you're not wearing it!".to_owned())?;
} }
let poss_data = item.possession_type.as_ref() let poss_data = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| UserError( .ok_or_else(|| {
"That item no longer exists in the game so can't be handled. Ask staff for help.".to_owned()))?; UserError(
"That item no longer exists in the game so can't be handled. Ask staff for help."
.to_owned(),
)
})?;
let wear_data = poss_data.wear_data.as_ref().ok_or_else( let wear_data = poss_data.wear_data.as_ref().ok_or_else(|| {
|| UserError("You seem to be wearing something that isn't clothes! Ask staff for help.".to_owned()))?; UserError(
"You seem to be wearing something that isn't clothes! Ask staff for help.".to_owned(),
)
})?;
let other_clothes = let other_clothes = ctx
ctx.trans.find_by_action_and_location( .trans
&player_item.refstr(), &LocationActionType::Worn).await?; .find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn)
.await?;
if let Some(my_worn_since) = item.action_type_started { if let Some(my_worn_since) = item.action_type_started {
for part in &wear_data.covers_parts { for part in &wear_data.covers_parts {
if let Some(other_item) = other_clothes.iter().find( if let Some(other_item) = other_clothes.iter().find(|other_item| {
|other_item| match other_item
match other_item.possession_type.as_ref() .possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref()) { .and_then(|pd| pd.wear_data.as_ref())
{
None => false, None => false,
Some(WearData { covers_parts, .. }) => Some(WearData { covers_parts, .. }) => {
covers_parts.contains(&part) && covers_parts.contains(&part)
other_item.action_type_started && other_item
.action_type_started
.map(|other_worn_since| other_worn_since < my_worn_since) .map(|other_worn_since| other_worn_since < my_worn_since)
.unwrap_or(false) .unwrap_or(false)
} }
) { }
}) {
user_error(format!( user_error(format!(
"You can't do that without first removing your {} from your {}.", "You can't do that without first removing your {} from your {}.",
&other_item.display_for_session(&ctx.session_dat), &other_item.display_for_session(&ctx.session_dat),
@ -78,58 +82,98 @@ async fn check_removeable(ctx: &mut VerbContext<'_>,
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to remove it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Remove { possession_id } => possession_id, QueueCommand::Remove { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
check_removeable(ctx, &item, &player_item).await?; check_removeable(ctx, &item, &player_item).await?;
let msg_exp = format!("{} fumbles around trying to take off {}\n", let msg_exp = format!(
"{} fumbles around trying to take off {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} fumbles around trying to take off {}\n", );
let msg_nonexp = format!(
"{} fumbles around trying to take off {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to remove it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Remove { possession_id } => possession_id, QueueCommand::Remove { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
check_removeable(ctx, &item, &player_item).await?; check_removeable(ctx, &item, &player_item).await?;
let msg_exp = format!("{} removes {}\n", let msg_exp = format!(
"{} removes {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} removes {}\n", );
let msg_nonexp = format!(
"{} removes {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
item_mut.action_type = LocationActionType::Normal; item_mut.action_type = LocationActionType::Normal;
item_mut.action_type_started = None; item_mut.action_type_started = None;
@ -139,14 +183,24 @@ impl QueueCommandHandler for QueueHandler {
.ok_or_else(|| UserError( .ok_or_else(|| UserError(
"That item no longer exists in the game so can't be handled. Ask staff for help.".to_owned()))?; "That item no longer exists in the game so can't be handled. Ask staff for help.".to_owned()))?;
let wear_data = poss_data.wear_data.as_ref().ok_or_else( let wear_data = poss_data.wear_data.as_ref().ok_or_else(|| {
|| UserError("You seem to be wearing something that isn't clothes! Ask staff for help.".to_owned()))?; UserError(
"You seem to be wearing something that isn't clothes! Ask staff for help."
.to_owned(),
)
})?;
if wear_data.dodge_penalty != 0.0 { if wear_data.dodge_penalty != 0.0 {
let mut player_item_mut = (*player_item).clone(); let mut player_item_mut = (*player_item).clone();
player_item_mut.temporary_buffs = player_item_mut.temporary_buffs.into_iter() player_item_mut.temporary_buffs = player_item_mut
.filter(|buf| buf.cause != .temporary_buffs
BuffCause::ByItem { item_code: item_mut.item_code.clone(), .into_iter()
item_type: item_mut.item_type.clone() }) .filter(|buf| {
buf.cause
!= BuffCause::ByItem {
item_code: item_mut.item_code.clone(),
item_type: item_mut.item_type.clone(),
}
})
.collect(); .collect();
if let Some(ref usr) = ctx.user_dat { if let Some(ref usr) = ctx.user_dat {
calculate_total_stats_skills_for_user(&mut player_item_mut, usr); calculate_total_stats_skills_for_user(&mut player_item_mut, usr);
@ -162,7 +216,12 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, mut remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1); let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") { if remaining == "all" || remaining.starts_with("all ") {
@ -172,24 +231,37 @@ impl UserVerb for Verb {
get_limit = Some(n); get_limit = Some(n);
remaining = remaining2; remaining = remaining2;
} }
let targets = search_items_for_user(ctx, &ItemSearchParams { let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
item_type_only: Some("possession"), item_type_only: Some("possession"),
item_action_type_only: Some(&LocationActionType::Worn), item_action_type_only: Some(&LocationActionType::Worn),
limit: get_limit.unwrap_or(100), limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining) ..ItemSearchParams::base(&player_item, &remaining)
}).await?; },
)
.await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("The dead don't undress themselves".to_owned())?; user_error("The dead don't undress themselves".to_owned())?;
} }
let mut did_anything: bool = false; let mut did_anything: bool = false;
for target in targets.iter().filter(|t| t.action_type.is_visible_in_look()) { for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type != "possession" { if target.item_type != "possession" {
user_error("You can't remove that!".to_owned())?; user_error("You can't remove that!".to_owned())?;
} }
did_anything = true; did_anything = true;
queue_command(ctx, &QueueCommand::Remove { possession_id: target.item_code.clone() }).await?; queue_command(
ctx,
&QueueCommand::Remove {
possession_id: target.item_code.clone(),
},
)
.await?;
} }
if !did_anything { if !did_anything {
user_error("I didn't find anything matching.".to_owned())?; user_error("I didn't find anything matching.".to_owned())?;

View File

@ -1,44 +1,44 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserError, UserVerb,
user_error, get_player_item_or_fail, get_user_or_fail, UserVerbRef, VerbContext,
}; };
use crate::{ use crate::{
DResult, models::{
item::{Item, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::{ static_content::{
room::{room_map_by_code, Direction},
dynzone::dynzone_by_type, dynzone::dynzone_by_type,
npc::npc_by_code, npc::npc_by_code,
room::{room_map_by_code, Direction},
}, },
models::{ DResult,
task::{TaskDetails, TaskMeta, Task},
item::{Item, ItemSpecialData},
},
regular_tasks::{
TaskHandler,
TaskRunContext,
},
}; };
use log::info;
use async_trait::async_trait;
use chrono::{Utc, Duration};
use std::time;
use ansi::ansi; use ansi::ansi;
use async_recursion::async_recursion; use async_recursion::async_recursion;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use itertools::Itertools; use itertools::Itertools;
use log::info;
use std::time;
#[async_recursion] #[async_recursion]
async fn recursively_destroy_or_move_item(ctx: &mut TaskRunContext<'_>, item: &Item) -> DResult<()> { pub async fn recursively_destroy_or_move_item(
ctx: &mut TaskRunContext<'_>,
item: &Item,
) -> DResult<()> {
let mut item_mut = item.clone(); let mut item_mut = item.clone();
match item.item_type.as_str() { match item.item_type.as_str() {
"npc" => { "npc" => {
let npc = match npc_by_code().get(item.item_code.as_str()) { let npc = match npc_by_code().get(item.item_code.as_str()) {
None => { return Ok(()) }, None => return Ok(()),
Some(r) => r Some(r) => r,
}; };
item_mut.location = npc.spawn_location.to_owned(); item_mut.location = npc.spawn_location.to_owned();
ctx.trans.save_item_model(&item_mut).await?; ctx.trans.save_item_model(&item_mut).await?;
return Ok(()); return Ok(());
}, }
"player" => { "player" => {
let session = ctx.trans.find_session_for_player(&item.item_code).await?; let session = ctx.trans.find_session_for_player(&item.item_code).await?;
match session.as_ref() { match session.as_ref() {
@ -47,17 +47,19 @@ async fn recursively_destroy_or_move_item(ctx: &mut TaskRunContext<'_>, item: &I
&listener_sess, &listener_sess,
Some(ansi!("<red>The landlord barges in with a bunch of very big hired goons, who stuff you in a sack, while the landlord mumbles something about vacant possession.<reset> After what seems like an eternity being jostled along while stuffed in the sack, they dump you out into a room that seems to be some kind of homeless shelter, and beat a hasty retreat.\n")) Some(ansi!("<red>The landlord barges in with a bunch of very big hired goons, who stuff you in a sack, while the landlord mumbles something about vacant possession.<reset> After what seems like an eternity being jostled along while stuffed in the sack, they dump you out into a room that seems to be some kind of homeless shelter, and beat a hasty retreat.\n"))
).await?; ).await?;
}, }
None => {} None => {}
} }
item_mut.location = "room/melbs_homelessshelter".to_owned(); item_mut.location = "room/melbs_homelessshelter".to_owned();
ctx.trans.save_item_model(&item_mut).await?; ctx.trans.save_item_model(&item_mut).await?;
return Ok(()); return Ok(());
}, }
_ => {} _ => {}
} }
ctx.trans.delete_item(&item.item_type, &item.item_code).await?; ctx.trans
.delete_item(&item.item_type, &item.item_code)
.await?;
let loc = format!("{}/{}", &item.item_type, &item.item_code); let loc = format!("{}/{}", &item.item_type, &item.item_code);
// It's paginated so we loop to get everything... // It's paginated so we loop to get everything...
loop { loop {
@ -78,47 +80,59 @@ pub struct ChargeRoomTaskHandler;
impl TaskHandler for ChargeRoomTaskHandler { impl TaskHandler for ChargeRoomTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (zone_item_ref, daily_price) = match &mut ctx.task.details { let (zone_item_ref, daily_price) = match &mut ctx.task.details {
TaskDetails::ChargeRoom { zone_item, daily_price } => (zone_item, daily_price), TaskDetails::ChargeRoom {
_ => Err("Expected ChargeRoom type")? zone_item,
daily_price,
} => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?,
}; };
let zone_item_code = match zone_item_ref.split_once("/") { let zone_item_code = match zone_item_ref.split_once("/") {
Some(("dynzone", c)) => c, Some(("dynzone", c)) => c,
_ => Err("Invalid zone item ref when charging room")? _ => Err("Invalid zone item ref when charging room")?,
}; };
let zone_item = match ctx.trans.find_item_by_type_code("dynzone", zone_item_code).await? { let zone_item = match ctx
.trans
.find_item_by_type_code("dynzone", zone_item_code)
.await?
{
None => { None => {
info!("Can't charge rent for dynzone {}, it's gone", zone_item_code); info!(
"Can't charge rent for dynzone {}, it's gone",
zone_item_code
);
return Ok(None); return Ok(None);
} }
Some(it) => it Some(it) => it,
}; };
let vacate_after = match zone_item.special_data { let vacate_after = match zone_item.special_data {
Some(ItemSpecialData::DynzoneData { vacate_after, .. }) => vacate_after, Some(ItemSpecialData::DynzoneData { vacate_after, .. }) => vacate_after,
_ => Err("Expected ChargeRoom dynzone to have DynzoneData")? _ => Err("Expected ChargeRoom dynzone to have DynzoneData")?,
}; };
match vacate_after { match vacate_after {
Some(t) if t < Utc::now() => { Some(t) if t < Utc::now() => {
recursively_destroy_or_move_item(ctx, &zone_item).await?; recursively_destroy_or_move_item(ctx, &zone_item).await?;
return Ok(None); return Ok(None);
}, }
_ => () _ => (),
} }
let bill_player_code = match zone_item.owner.as_ref().and_then(|s| s.split_once("/")) { let bill_player_code = match zone_item.owner.as_ref().and_then(|s| s.split_once("/")) {
Some((player_item_type, player_item_code)) if player_item_type == "player" => Some((player_item_type, player_item_code)) if player_item_type == "player" => {
player_item_code, player_item_code
}
_ => { _ => {
info!("Can't charge rent for dynzone {}, owner {:?} isn't chargeable", zone_item_code, info!(
&zone_item.owner "Can't charge rent for dynzone {}, owner {:?} isn't chargeable",
zone_item_code, &zone_item.owner
); );
return Ok(None) return Ok(None);
} }
}; };
let mut bill_user = match ctx.trans.find_by_username(bill_player_code).await? { let mut bill_user = match ctx.trans.find_by_username(bill_player_code).await? {
None => return Ok(None), None => return Ok(None),
Some(user) => user Some(user) => user,
}; };
let session = ctx.trans.find_session_for_player(bill_player_code).await?; let session = ctx.trans.find_session_for_player(bill_player_code).await?;
@ -133,18 +147,21 @@ impl TaskHandler for ChargeRoomTaskHandler {
zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData { zone_item_mut.special_data = Some(ItemSpecialData::DynzoneData {
vacate_after: Some(Utc::now() + Duration::days(1)), vacate_after: Some(Utc::now() + Duration::days(1)),
zone_exit: match zone_item.special_data.as_ref() { zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => zone_exit.clone(),
zone_exit.clone(), _ => None,
_ => None },
}}); });
ctx.trans.save_item_model(&zone_item_mut).await?; ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx.trans.find_item_by_type_code("dynroom", match ctx
&(zone_item.item_code.clone() + "/doorstep")).await? { .trans
None => {}, .find_item_by_type_code("dynroom", &(zone_item.item_code.clone() + "/doorstep"))
.await?
{
None => {}
Some(doorstep_room) => { Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone(); let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details = Some( doorstep_mut.details = Some(
doorstep_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE doorstep_mut.details.clone().unwrap_or("".to_owned()) + EVICTION_NOTICE,
); );
ctx.trans.save_item_model(&doorstep_mut).await?; ctx.trans.save_item_model(&doorstep_mut).await?;
} }
@ -184,78 +201,116 @@ pub static CHARGE_ROOM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &Char
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let item_name = remaining.trim(); let item_name = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item.location.split_once("/") let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?; .ok_or_else(|| UserError("Invalid location".to_owned()))?;
if loc_type != "room" { if loc_type != "room" {
user_error("You can't rent anything from here.".to_owned())?; user_error("You can't rent anything from here.".to_owned())?;
} }
let room = room_map_by_code().get(loc_code) let room = room_map_by_code()
.get(loc_code)
.ok_or_else(|| UserError("Can't find your room".to_owned()))?; .ok_or_else(|| UserError("Can't find your room".to_owned()))?;
if room.rentable_dynzone.is_empty() { if room.rentable_dynzone.is_empty() {
user_error("You can't rent anything from here.".to_owned())?; user_error("You can't rent anything from here.".to_owned())?;
} }
let rentinfo = match room.rentable_dynzone.iter().find(|ri| ri.rent_what == item_name) { let rentinfo = match room
None => user_error(format!("Rent must be followed by the specific thing you want to rent: {}", .rentable_dynzone
room.rentable_dynzone.iter() .iter()
.map(|ri| ri.rent_what).join(", ")))?, .find(|ri| ri.rent_what == item_name)
Some(v) => v {
None => user_error(format!(
"Rent must be followed by the specific thing you want to rent: {}",
room.rentable_dynzone
.iter()
.map(|ri| ri.rent_what)
.join(", ")
))?,
Some(v) => v,
}; };
let user = get_user_or_fail(ctx)?; let user = get_user_or_fail(ctx)?;
if user.credits < rentinfo.setup_fee { if user.credits < rentinfo.setup_fee {
user_error("The robot rolls its eyes at you derisively. \"I don't think so - you couldn't even afford the setup fee!\"".to_owned())? user_error("The robot rolls its eyes at you derisively. \"I don't think so - you couldn't even afford the setup fee!\"".to_owned())?
} }
let zone = dynzone_by_type().get(&rentinfo.dynzone) let zone = dynzone_by_type().get(&rentinfo.dynzone).ok_or_else(|| {
.ok_or_else(|| UserError("That seems to no longer exist, so you can't rent it.".to_owned()))?; UserError("That seems to no longer exist, so you can't rent it.".to_owned())
})?;
match ctx.trans.find_exact_dyn_exit( match ctx
.trans
.find_exact_dyn_exit(
&player_item.location, &player_item.location,
&Direction::IN { item: player_item.display.clone() }) &Direction::IN {
item: player_item.display.clone(),
},
)
.await? .await?
.as_ref() .as_ref()
.and_then(|it| it.location.split_once("/")) .and_then(|it| it.location.split_once("/"))
{ {
None => {}, None => {}
Some((ref ex_zone_t, ref ex_zone_c)) => { Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) = if let Some(ex_zone) = ctx
ctx.trans.find_item_by_type_code(ex_zone_t, ex_zone_c) .trans
.await? { .find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data { match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData { Some(ItemSpecialData::DynzoneData {
vacate_after: None, .. }) => vacate_after: None, ..
user_error( }) => user_error(
"You can only rent one apartment here, and you already have one!".to_owned())?, "You can only rent one apartment here, and you already have one!"
.to_owned(),
)?,
Some(ItemSpecialData::DynzoneData { Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_), zone_exit: ref ex }) => { vacate_after: Some(_),
zone_exit: ref ex,
}) => {
let mut user_mut = user.clone(); let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee; user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?; ctx.trans.save_user_model(&user_mut).await?;
ctx.trans.save_item_model( ctx.trans
&Item { .save_item_model(&Item {
special_data: special_data: Some(ItemSpecialData::DynzoneData {
Some(ItemSpecialData::DynzoneData {
vacate_after: None, vacate_after: None,
zone_exit: ex.clone() zone_exit: ex.clone(),
}), ..(*ex_zone).clone() } }),
).await?; ..(*ex_zone).clone()
})
.await?;
match ctx.trans.find_item_by_type_code("dynroom", match ctx
&(ex_zone.item_code.clone() + "/doorstep")).await? { .trans
None => {}, .find_item_by_type_code(
"dynroom",
&(ex_zone.item_code.clone() + "/doorstep"),
)
.await?
{
None => {}
Some(doorstep_room) => { Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone(); let mut doorstep_mut = (*doorstep_room).clone();
doorstep_mut.details = Some( doorstep_mut.details = Some(
doorstep_mut.details.clone().unwrap_or("".to_owned()).replace(EVICTION_NOTICE, "") doorstep_mut
.details
.clone()
.unwrap_or("".to_owned())
.replace(EVICTION_NOTICE, ""),
); );
ctx.trans.save_item_model(&doorstep_mut).await?; ctx.trans.save_item_model(&doorstep_mut).await?;
} }
} }
ctx.trans.queue_for_session( ctx.trans.queue_for_session(
ctx.session, ctx.session,
Some(&format!(ansi!( Some(&format!(ansi!(
@ -263,20 +318,27 @@ impl UserVerb for Verb {
<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"), rentinfo.setup_fee)) <yellow>Your wristpad beeps for a deduction of ${}<reset>\n"), rentinfo.setup_fee))
).await?; ).await?;
return Ok(()); return Ok(());
}, }
_ => {} _ => {}
} }
} }
} }
} }
let zonecode = zone.create_instance( let zonecode = zone
ctx.trans, &player_item.location, .create_instance(
ctx.trans,
&player_item.location,
"You can only rent one apartment here, and you already have one!", "You can only rent one apartment here, and you already have one!",
&player_item, &Direction::IN { item: player_item.display.clone() } &player_item,
).await?; &Direction::IN {
item: player_item.display.clone(),
},
)
.await?;
ctx.trans.upsert_task(&Task { ctx.trans
.upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: format!("charge_rent/{}", &zonecode), task_code: format!("charge_rent/{}", &zonecode),
is_static: false, is_static: false,
@ -286,18 +348,23 @@ impl UserVerb for Verb {
}, },
details: TaskDetails::ChargeRoom { details: TaskDetails::ChargeRoom {
zone_item: zonecode.clone(), zone_item: zonecode.clone(),
daily_price: rentinfo.daily_price daily_price: rentinfo.daily_price,
} },
}).await?; })
.await?;
let mut user_mut = user.clone(); let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee; user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?; ctx.trans.save_user_model(&user_mut).await?;
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
ctx.session, ctx.session,
Some(&format!(ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"), Some(&format!(
rentinfo.setup_fee)) ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
).await?; rentinfo.setup_fee
)),
)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,29 +1,37 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError, use super::{
user_error, get_player_item_or_fail, is_likely_explicit, user_error, UResult, UserError, UserVerb,
get_player_item_or_fail, is_likely_explicit}; UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
models::item::{Item, ItemFlag}, models::item::{Item, ItemFlag},
services::comms::broadcast_to_room, services::comms::broadcast_to_room,
}; };
use mockall_double::double; use ansi::{ansi, ignore_special_characters};
#[double] use crate::db::DBTrans;
use async_trait::async_trait; use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi}; use mockall_double::double;
pub async fn say_to_room<'l>( pub async fn say_to_room<'l>(
trans: &DBTrans, trans: &DBTrans,
from_item: &Item, from_item: &Item,
location: &str, location: &str,
say_what: &str, say_what: &str,
is_explicit: bool is_explicit: bool,
) -> UResult<()> { ) -> UResult<()> {
let (loc_type, loc_code) = location.split_once("/") let (loc_type, loc_code) = location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?; .ok_or_else(|| UserError("Invalid location".to_owned()))?;
let room_item = trans.find_item_by_type_code(loc_type, loc_code).await? let room_item = trans
.find_item_by_type_code(loc_type, loc_code)
.await?
.ok_or_else(|| UserError("Room missing".to_owned()))?; .ok_or_else(|| UserError("Room missing".to_owned()))?;
if room_item.flags.contains(&ItemFlag::NoSay) { if room_item.flags.contains(&ItemFlag::NoSay) {
user_error("Your wristpad vibrates and flashes up an error - apparently it has \ user_error(
been programmed to block your voice from working here.".to_owned())? "Your wristpad vibrates and flashes up an error - apparently it has \
been programmed to block your voice from working here."
.to_owned(),
)?
} }
let msg_exp = format!( let msg_exp = format!(
ansi!("<yellow>{} says: <reset><bold>\"{}\"<reset>\n"), ansi!("<yellow>{} says: <reset><bold>\"{}\"<reset>\n"),
@ -41,15 +49,25 @@ pub async fn say_to_room<'l>(
location, location,
Some(from_item), Some(from_item),
&msg_exp, &msg_exp,
if is_explicit { None } else { Some(&msg_lessexp) } if is_explicit {
).await?; None
} else {
Some(&msg_lessexp)
},
)
.await?;
Ok(()) Ok(())
} }
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let say_what = ignore_special_characters(remaining); let say_what = ignore_special_characters(remaining);
if say_what == "" { if say_what == "" {
user_error("You need to provide a message to send.".to_owned())?; user_error("You need to provide a message to send.".to_owned())?;
@ -58,8 +76,14 @@ impl UserVerb for Verb {
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Shush, the dead can't talk!".to_string())?; user_error("Shush, the dead can't talk!".to_string())?;
} }
say_to_room(ctx.trans, &player_item, &player_item.location, say_to_room(
&say_what, is_likely_explicit(&say_what)).await ctx.trans,
&player_item,
&player_item.location,
&say_what,
is_likely_explicit(&say_what),
)
.await
} }
} }
static VERB_INT: Verb = Verb; static VERB_INT: Verb = Verb;

View File

@ -1,28 +1,32 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
user_error, get_player_item_or_fail}; use crate::models::item::{SkillType, StatType};
use crate::{
models::item::{StatType, SkillType}
};
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let user = match ctx.user_dat { let user = match ctx.user_dat {
None => user_error("Log in first".to_owned())?, None => user_error("Log in first".to_owned())?,
Some(user) => user Some(user) => user,
}; };
let mut msg = String::new(); let mut msg = String::new();
msg.push_str(&format!(ansi!("<bgblue><white><bold>| {:11} | {:5} | {:5} |<reset>\n"), msg.push_str(&format!(
ansi!("<bgblue><white><bold>| {:11} | {:5} | {:5} |<reset>\n"),
"Stat", "Raw", "Total" "Stat", "Raw", "Total"
)); ));
for st in StatType::values().iter() { for st in StatType::values().iter() {
msg.push_str(&format!(ansi!("| <bold>{:11}<reset> | {:5.2} | {:5.2} |\n"), msg.push_str(&format!(
ansi!("| <bold>{:11}<reset> | {:5.2} | {:5.2} |\n"),
st.display(), st.display(),
user.raw_stats.get(st).unwrap_or(&0.0), user.raw_stats.get(st).unwrap_or(&0.0),
player_item.total_stats.get(st).unwrap_or(&0.0) player_item.total_stats.get(st).unwrap_or(&0.0)
@ -30,20 +34,25 @@ impl UserVerb for Verb {
} }
msg.push_str("\n"); msg.push_str("\n");
msg.push_str(&format!(ansi!("<bgblue><white><bold>| {:11} | {:5} | {:5} |<reset>\n"), msg.push_str(&format!(
ansi!("<bgblue><white><bold>| {:11} | {:5} | {:5} |<reset>\n"),
"Skill", "Raw", "Total" "Skill", "Raw", "Total"
)); ));
for st in SkillType::values().iter() { for st in SkillType::values().iter() {
msg.push_str(&format!(ansi!("| <bold>{:11}<reset> | {:5.2} | {:5.2} |\n"), msg.push_str(&format!(
ansi!("| <bold>{:11}<reset> | {:5.2} | {:5.2} |\n"),
st.display(), st.display(),
user.raw_skills.get(st).unwrap_or(&0.0), user.raw_skills.get(st).unwrap_or(&0.0),
player_item.total_skills.get(st).unwrap_or(&0.0) player_item.total_skills.get(st).unwrap_or(&0.0)
)); ));
} }
msg.push_str("\n"); msg.push_str("\n");
msg.push_str(&format!(ansi!("Experience: <bold>{}<reset> total, \ msg.push_str(&format!(
ansi!(
"Experience: <bold>{}<reset> total, \
change of {} this re-roll, \ change of {} this re-roll, \
{} spent since re-roll.\n"), {} spent since re-roll.\n"
),
player_item.total_xp, player_item.total_xp,
user.experience.xp_change_for_this_reroll, user.experience.xp_change_for_this_reroll,
user.experience.spent_xp user.experience.spent_xp

View File

@ -1,31 +1,43 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, search_item_for_user}; use super::{
use crate::{ get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
db::ItemSearchParams, VerbContext,
static_content::possession_type::possession_data,
}; };
use crate::{db::ItemSearchParams, static_content::possession_type::possession_data};
use async_trait::async_trait; use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let item_name = remaining.trim(); let item_name = remaining.trim();
if item_name == "" { if item_name == "" {
user_error("Sign what? Try: sign something".to_owned())?; user_error("Sign what? Try: sign something".to_owned())?;
} }
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let item = search_item_for_user(ctx, &ItemSearchParams { let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
..ItemSearchParams::base(&player_item, item_name) ..ItemSearchParams::base(&player_item, item_name)
}).await?; },
)
.await?;
if item.item_type != "possession" { if item.item_type != "possession" {
user_error("You can't sign that!".to_owned())?; user_error("You can't sign that!".to_owned())?;
} }
let handler = match item.possession_type.as_ref() let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.sign_handler) { .and_then(|pd| pd.sign_handler)
{
None => user_error("You can't sign that!".to_owned())?, None => user_error("You can't sign that!".to_owned())?,
Some(h) => h Some(h) => h,
}; };
handler.cmd(ctx, &player_item, &item).await?; handler.cmd(ctx, &player_item, &item).await?;

View File

@ -1,10 +1,7 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
user_error, get_player_item_or_fail}; use crate::services::combat::max_health;
use crate::{
services::combat::max_health,
};
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
fn bar_n_of_m(mut actual: u64, max: u64) -> String { fn bar_n_of_m(mut actual: u64, max: u64) -> String {
if actual > max { if actual > max {
@ -23,21 +20,31 @@ fn bar_n_of_m(mut actual: u64, max: u64) -> String {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let user = match ctx.user_dat { let user = match ctx.user_dat {
None => user_error("Log in first".to_owned())?, None => user_error("Log in first".to_owned())?,
Some(user) => user Some(user) => user,
}; };
let mut msg = String::new(); let mut msg = String::new();
let maxh = max_health(&player_item); let maxh = max_health(&player_item);
msg.push_str(&format!(ansi!("<bold>Health [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"), msg.push_str(&format!(
ansi!("<bold>Health [<green>{}<reset><bold>] [ {}/{} ]<reset>\n"),
bar_n_of_m(player_item.health, maxh), bar_n_of_m(player_item.health, maxh),
player_item.health, maxh player_item.health,
maxh
));
msg.push_str(&format!(
ansi!("<bold>Credits <green>${}<reset>\n"),
user.credits
)); ));
msg.push_str(&format!(ansi!("<bold>Credits <green>${}<reset>\n"), user.credits));
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?; ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(()) Ok(())

View File

@ -1,64 +1,92 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{
UserError, get_player_item_or_fail, search_items_for_user, user_error, UResult, UserError, UserVerb,
user_error, get_player_item_or_fail, search_items_for_user}; UserVerbRef, VerbContext,
};
use crate::{ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
static_content::{
possession_type::possession_data,
room::Direction,
},
models::item::ItemFlag, models::item::ItemFlag,
static_content::{possession_type::possession_data, room::Direction},
}; };
use async_trait::async_trait;
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { 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 ") { let (uninstall_what_raw, what_dir_raw) = match remaining.rsplit_once(" from door to ") {
None => user_error(ansi!("Uninstall from where? Try <bold>uninstall<reset> <lt>lock> <bold>from door to<reset> <lt>direction>").to_owned())?, None => user_error(ansi!("Uninstall from where? Try <bold>uninstall<reset> <lt>lock> <bold>from door to<reset> <lt>direction>").to_owned())?,
Some(v) => v Some(v) => v
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Apparently, you have to be alive to work as an uninstaller. \ user_error(
So discriminatory!".to_owned())?; "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("/") let (loc_t, loc_c) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid current location".to_owned()))?; .ok_or_else(|| UserError("Invalid current location".to_owned()))?;
let loc_item = ctx.trans.find_item_by_type_code(loc_t, loc_c).await? 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()))?; .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) { if loc_item.owner.as_ref() != Some(&player_item.refstr())
user_error("You can only uninstall things while standing in a private room you own. \ || !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." If you are outside, try uninstalling from the inside."
.to_owned())?; .to_owned(),
)?;
} }
let dir = Direction::parse(what_dir_raw.trim()).ok_or_else( let dir = Direction::parse(what_dir_raw.trim())
|| UserError("Invalid direction.".to_owned()))?; .ok_or_else(|| UserError("Invalid direction.".to_owned()))?;
let cand_items = search_items_for_user(ctx, &ItemSearchParams { let cand_items = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
..ItemSearchParams::base(&player_item, uninstall_what_raw.trim()) ..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( .await?;
|| UserError( let item = cand_items
"Sorry, I couldn't find anything matching installed on that door.".to_owned()))?; .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" { if item.item_type != "possession" {
user_error("You can't uninstall that!".to_owned())?; user_error("You can't uninstall that!".to_owned())?;
} }
let handler = match item.possession_type.as_ref() let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.install_handler) { .and_then(|pd| pd.install_handler)
{
None => user_error("You can't uninstall that!".to_owned())?, None => user_error("You can't uninstall that!".to_owned())?,
Some(h) => h Some(h) => h,
}; };
handler.uninstall_cmd(ctx, &player_item, &item, &loc_item, &dir).await?; handler
.uninstall_cmd(ctx, &player_item, &item, &loc_item, &dir)
.await?;
Ok(()) Ok(())
} }

View File

@ -1,33 +1,15 @@
use super::{ use super::{
VerbContext, get_player_item_or_fail, parsing, search_item_for_user, user_error, ItemSearchParams, UResult,
UserVerb, UserVerb, UserVerbRef, VerbContext,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_item_for_user,
parsing,
}; };
use crate::{ use crate::{
static_content::possession_type::{
possession_data,
},
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
models::item::{
SkillType,
},
services::{
comms::broadcast_to_room,
skills::skill_check_and_grind,
effect::run_effects,
check_consent,
},
language, language,
models::item::SkillType,
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
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 async_trait::async_trait;
use std::time; use std::time;
@ -35,74 +17,115 @@ use std::time;
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to use it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Use { possession_id, target_id } => (possession_id, target_id), QueueCommand::Use {
_ => user_error("Unexpected command".to_owned())? possession_id,
target_id,
} => (possession_id, target_id),
_ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != format!("player/{}", player_item.item_code) { if item.location != format!("player/{}", player_item.item_code) {
user_error(format!("You try to use {} but realise you no longer have it", user_error(format!(
item.display_for_sentence( "You try to use {} but realise you no longer have it",
!ctx.session_dat.less_explicit_mode, item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
1, false
)
))? ))?
} }
let (target_type, target_code) = match target_type_code.split_once("/") { let (target_type, target_code) = match target_type_code.split_once("/") {
None => user_error("Couldn't handle use command (invalid target)".to_owned())?, None => user_error("Couldn't handle use command (invalid target)".to_owned())?,
Some(spl) => spl 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 == player_item.item_code;
let target = let target = if is_self_use {
if is_self_use {
player_item.clone() player_item.clone()
} else { } else {
match ctx.trans.find_item_by_type_code(&target_type, &target_code).await? { match ctx
None => user_error(format!("Couldn't handle use command (target {} missing)", .trans
target_type_code))?, .find_item_by_type_code(&target_type, &target_code)
Some(it) => it .await?
{
None => user_error(format!(
"Couldn't handle use command (target {} missing)",
target_type_code
))?,
Some(it) => it,
} }
}; };
if !is_self_use && target.location != player_item.location && if !is_self_use
target.location != format!("player/{}", player_item.item_code) { && target.location != player_item.location
let target_name = target.display_for_sentence(!ctx.session_dat.less_explicit_mode, && target.location != format!("player/{}", player_item.item_code)
1, false); {
user_error(format!("You try to use {} on {}, but realise {} is no longer here", let target_name =
item.display_for_sentence( target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false);
!ctx.session_dat.less_explicit_mode, user_error(format!(
1, false), "You try to use {} on {}, but realise {} is no longer here",
target_name, target_name item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false),
target_name,
target_name
))? ))?
} }
let msg_exp = format!("{} prepares to use {} {} on {}\n", let msg_exp = format!(
"{} prepares to use {} {} on {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&player_item.pronouns.possessive, &player_item.pronouns.possessive,
&item.display_for_sentence(true, 1, false), &item.display_for_sentence(true, 1, false),
&if is_self_use { player_item.pronouns.intensive.clone() } else { &if is_self_use {
player_item.pronouns.intensive.clone()
} else {
player_item.display_for_sentence(true, 1, false) player_item.display_for_sentence(true, 1, false)
}); }
let msg_nonexp = format!("{} prepares to use {} {} on {}\n", );
let msg_nonexp = format!(
"{} prepares to use {} {} on {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&player_item.pronouns.possessive, &player_item.pronouns.possessive,
&item.display_for_sentence(false, 1, false), &item.display_for_sentence(false, 1, false),
&if is_self_use { player_item.pronouns.intensive.clone() } else { &if is_self_use {
player_item.pronouns.intensive.clone()
} else {
player_item.display_for_sentence(true, 1, false) player_item.display_for_sentence(true, 1, false)
}); }
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
let mut draw_level: f64 = *player_item.total_skills.get(&SkillType::Quickdraw).to_owned().unwrap_or(&8.0); broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut draw_level: f64 = *player_item
.total_skills
.get(&SkillType::Quickdraw)
.to_owned()
.unwrap_or(&8.0);
let mut player_item_mut = (*player_item).clone(); let mut player_item_mut = (*player_item).clone();
let skill_result = let skill_result = skill_check_and_grind(
skill_check_and_grind(ctx.trans, &mut player_item_mut, &SkillType::Quickdraw, draw_level).await?; ctx.trans,
&mut player_item_mut,
&SkillType::Quickdraw,
draw_level,
)
.await?;
if skill_result < -0.5 { if skill_result < -0.5 {
draw_level -= 2.0; draw_level -= 2.0;
} else if skill_result < -0.25 { } else if skill_result < -0.25 {
@ -115,72 +138,97 @@ impl QueueCommandHandler for QueueHandler {
ctx.trans.save_item_model(&player_item_mut).await?; ctx.trans.save_item_model(&player_item_mut).await?;
let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0); let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0);
Ok(time::Duration::from_millis((wait_ticks * 500.0).round() as u64)) Ok(time::Duration::from_millis(
(wait_ticks * 500.0).round() as u64
))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to use it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Use { possession_id, target_id } => (possession_id, target_id), QueueCommand::Use {
_ => user_error("Unexpected command".to_owned())? possession_id,
target_id,
} => (possession_id, target_id),
_ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != format!("player/{}", player_item.item_code) { if item.location != format!("player/{}", player_item.item_code) {
user_error(format!("You try to use {} but realise you no longer have it", user_error(format!(
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, "You try to use {} but realise you no longer have it",
1, false) item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))? ))?
} }
let (ref target_type, ref target_code) = match target_type_code.split_once("/") { let (ref target_type, ref target_code) = match target_type_code.split_once("/") {
None => user_error("Couldn't handle use command (invalid target)".to_owned())?, None => user_error("Couldn't handle use command (invalid target)".to_owned())?,
Some(ref sp) => sp.clone() Some(ref sp) => sp.clone(),
}; };
let target = match ctx.trans.find_item_by_type_code(&target_type, &target_code).await? { let target = match ctx
.trans
.find_item_by_type_code(&target_type, &target_code)
.await?
{
None => user_error("Couldn't handle use command (target missing)".to_owned())?, None => user_error("Couldn't handle use command (target missing)".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if target.location != player_item.location && if target.location != player_item.location
target.location != format!("player/{}", player_item.item_code) { && target.location != format!("player/{}", player_item.item_code)
let target_name = target.display_for_sentence(!ctx.session_dat.less_explicit_mode, {
1, false); let target_name =
user_error(format!("You try to use {} on {}, but realise {} is no longer here", target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false);
item.display_for_sentence( user_error(format!(
!ctx.session_dat.less_explicit_mode, "You try to use {} on {}, but realise {} is no longer here",
1, false), item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false),
target_name, target_name target_name,
target_name
))? ))?
} }
let use_data = match item.possession_type.as_ref() let use_data = match item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type)) .and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.use_data.as_ref()) { .and_then(|poss_data| poss_data.use_data.as_ref())
{
None => user_error("You can't use that!".to_owned())?, None => user_error("You can't use that!".to_owned())?,
Some(d) => d Some(d) => d,
}; };
if let Some(consent_type) = use_data.needs_consent_check.as_ref() { 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, &player_item, &target).await? {
user_error(format!("{} doesn't allow {} from you", user_error(format!(
&target.display_for_sentence(!ctx.session_dat.less_explicit_mode, "{} doesn't allow {} from you",
1, true), &target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true),
consent_type.to_str()))? consent_type.to_str()
))?
} }
} }
if let Some(charge_data) = item.possession_type.as_ref() if let Some(charge_data) = item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type)) .and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.charge_data.as_ref()) { .and_then(|poss_data| poss_data.charge_data.as_ref())
{
if item.charges < 1 { if item.charges < 1 {
user_error( user_error(format!(
format!("{} has no {} {} left", "{} has no {} {} left",
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true),
1, true),
&language::pluralise(charge_data.charge_name_prefix), &language::pluralise(charge_data.charge_name_prefix),
charge_data.charge_name_suffix charge_data.charge_name_suffix
))?; ))?;
@ -189,22 +237,32 @@ impl QueueCommandHandler for QueueHandler {
if let Some(err) = (use_data.errorf)(&item, &target) { if let Some(err) = (use_data.errorf)(&item, &target) {
user_error(err)?; user_error(err)?;
} }
if ctx.trans.check_task_by_type_code( if ctx
.trans
.check_task_by_type_code(
"DelayedHealth", "DelayedHealth",
&format!("{}/{}/{}", &target.item_type, &target.item_code, &format!(
use_data.task_ref) "{}/{}/{}",
).await? { &target.item_type, &target.item_code, use_data.task_ref
user_error(format!("You see no reason to use {} on {}", ),
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, )
1, false), .await?
target.display_for_sentence(!ctx.session_dat.less_explicit_mode, {
1, false) 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)
))?; ))?;
} }
let is_self_use = target_type == &"player" && target_code == &player_item.item_code; let is_self_use = target_type == &"player" && target_code == &player_item.item_code;
let mut player_mut = (*player_item).clone(); let mut player_mut = (*player_item).clone();
let skillcheck = skill_check_and_grind(&ctx.trans, &mut player_mut, let skillcheck = skill_check_and_grind(
&use_data.uses_skill, use_data.diff_level).await?; &ctx.trans,
&mut player_mut,
&use_data.uses_skill,
use_data.diff_level,
)
.await?;
let (effects, skilllvl) = if skillcheck <= -0.5 { let (effects, skilllvl) = if skillcheck <= -0.5 {
// 0-1 how bad was the crit fail? // 0-1 how bad was the crit fail?
(&use_data.crit_fail_effects, (-0.5 - skillcheck) * 2.0) (&use_data.crit_fail_effects, (-0.5 - skillcheck) * 2.0)
@ -214,9 +272,21 @@ impl QueueCommandHandler for QueueHandler {
(&use_data.success_effects, skillcheck) (&use_data.success_effects, skillcheck)
}; };
let mut target_mut = if is_self_use { None } else { Some((*target).clone()) }; let mut target_mut = if is_self_use {
run_effects(ctx.trans, &effects, &mut player_mut, &item, &mut target_mut, skilllvl, None
use_data.task_ref).await?; } else {
Some((*target).clone())
};
run_effects(
ctx.trans,
&effects,
&mut player_mut,
&item,
&mut target_mut,
skilllvl,
use_data.task_ref,
)
.await?;
if let Some(target_mut_save) = target_mut { if let Some(target_mut_save) = target_mut {
ctx.trans.save_item_model(&target_mut_save).await?; ctx.trans.save_item_model(&target_mut_save).await?;
} }
@ -224,25 +294,40 @@ impl QueueCommandHandler for QueueHandler {
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
let mut save_item = false; let mut save_item = false;
if item.possession_type.as_ref() if item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type)) .and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.charge_data.as_ref()).is_some() { .and_then(|poss_data| poss_data.charge_data.as_ref())
.is_some()
{
item_mut.charges -= 1; item_mut.charges -= 1;
save_item = true; save_item = true;
} }
if item_mut.charges == 0 { if item_mut.charges == 0 {
if let Some((new_poss, new_poss_dat)) = item.possession_type.as_ref() if let Some((new_poss, new_poss_dat)) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|poss_data| poss_data.becomes_on_spent.as_ref()) .and_then(|poss_data| poss_data.becomes_on_spent.as_ref())
.and_then(|poss_type| possession_data().get(&poss_type) .and_then(|poss_type| {
.map(|poss_dat| (poss_type, poss_dat))) possession_data()
.get(&poss_type)
.map(|poss_dat| (poss_type, poss_dat))
})
{ {
item_mut.possession_type = Some(new_poss.clone()); item_mut.possession_type = Some(new_poss.clone());
item_mut.display = new_poss_dat.display.to_owned(); item_mut.display = new_poss_dat.display.to_owned();
item_mut.display_less_explicit = new_poss_dat.display_less_explicit.map(|d| d.to_owned()); item_mut.display_less_explicit =
new_poss_dat.display_less_explicit.map(|d| d.to_owned());
item_mut.details = Some(new_poss_dat.details.to_owned()); item_mut.details = Some(new_poss_dat.details.to_owned());
item_mut.details_less_explicit = new_poss_dat.details_less_explicit.map(|d| d.to_owned()); item_mut.details_less_explicit =
item_mut.aliases = new_poss_dat.aliases.iter().map(|al| (*al).to_owned()).collect(); new_poss_dat.details_less_explicit.map(|d| d.to_owned());
item_mut.aliases = new_poss_dat
.aliases
.iter()
.map(|al| (*al).to_owned())
.collect();
item_mut.health = new_poss_dat.max_health; item_mut.health = new_poss_dat.max_health;
item_mut.weight = new_poss_dat.weight; item_mut.weight = new_poss_dat.weight;
save_item = true; save_item = true;
@ -258,42 +343,67 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to use it, but your ghostly hands slip through it uselessly".to_owned())?; user_error(
"You try to use it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
} }
let (what_name, whom_name) = parsing::parse_on_or_default(remaining, "me"); let (what_name, whom_name) = parsing::parse_on_or_default(remaining, "me");
let item = search_item_for_user(ctx, &ItemSearchParams { let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
item_type_only: Some("possession"), item_type_only: Some("possession"),
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, &what_name) ..ItemSearchParams::base(&player_item, &what_name)
}).await?; },
)
.await?;
let target = if whom_name == "me" || whom_name == "self" { player_item.clone() } else { let target = if whom_name == "me" || whom_name == "self" {
search_item_for_user(ctx, &ItemSearchParams { player_item.clone()
} else {
search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
include_loc_contents: true, include_loc_contents: true,
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, &whom_name) ..ItemSearchParams::base(&player_item, &whom_name)
}).await? },
)
.await?
}; };
let use_data = match item.possession_type.as_ref() let use_data = match item
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type)) .and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.use_data.as_ref()) { .and_then(|poss_data| poss_data.use_data.as_ref())
{
None => user_error("You can't use that!".to_owned())?, None => user_error("You can't use that!".to_owned())?,
Some(d) => d Some(d) => d,
}; };
if let Some(err) = (use_data.errorf)(&item, &target) { if let Some(err) = (use_data.errorf)(&item, &target) {
user_error(err)?; user_error(err)?;
} }
queue_command(ctx, &QueueCommand::Use { queue_command(
ctx,
&QueueCommand::Use {
possession_id: item.item_code.clone(), possession_id: item.item_code.clone(),
target_id: format!("{}/{}", target.item_type, target.item_code)}).await?; target_id: format!("{}/{}", target.item_type, target.item_code),
},
)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -1,75 +1,95 @@
use super::{ use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
user_error, get_player_item_or_fail,
}; };
use crate::{ use crate::{
static_content::{ models::item::{Item, ItemSpecialData},
room::{room_map_by_code, Direction}, static_content::room::{room_map_by_code, Direction},
},
models::{
item::{Item, ItemSpecialData},
},
}; };
use chrono::{Utc, Duration};
use async_trait::async_trait; use async_trait::async_trait;
use chrono::{Duration, Utc};
use itertools::Itertools; use itertools::Itertools;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let item_name = remaining.trim(); let item_name = remaining.trim();
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let (loc_type, loc_code) = player_item.location.split_once("/") let (loc_type, loc_code) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid location".to_owned()))?; .ok_or_else(|| UserError("Invalid location".to_owned()))?;
if loc_type != "room" { if loc_type != "room" {
user_error("You must go to where you rented the place (e.g. reception) to vacate.".to_owned())?; user_error(
"You must go to where you rented the place (e.g. reception) to vacate.".to_owned(),
)?;
} }
let room = room_map_by_code().get(loc_code) let room = room_map_by_code()
.get(loc_code)
.ok_or_else(|| UserError("Can't find your room".to_owned()))?; .ok_or_else(|| UserError("Can't find your room".to_owned()))?;
if room.rentable_dynzone.is_empty() { if room.rentable_dynzone.is_empty() {
user_error("Go to where you rented the place (e.g. reception) to vacate.".to_owned())?; user_error("Go to where you rented the place (e.g. reception) to vacate.".to_owned())?;
} }
match room.rentable_dynzone.iter().find(|ri| ri.rent_what == item_name) { match room
None => user_error(format!("Vacate must be followed by the specific thing you want to vacate: {}", .rentable_dynzone
room.rentable_dynzone.iter() .iter()
.map(|ri| ri.rent_what).join(", ")))?, .find(|ri| ri.rent_what == item_name)
Some(_) => () {
None => user_error(format!(
"Vacate must be followed by the specific thing you want to vacate: {}",
room.rentable_dynzone
.iter()
.map(|ri| ri.rent_what)
.join(", ")
))?,
Some(_) => (),
}; };
match ctx.trans.find_exact_dyn_exit( match ctx
.trans
.find_exact_dyn_exit(
&player_item.location, &player_item.location,
&Direction::IN { item: player_item.display.clone() }) &Direction::IN {
item: player_item.display.clone(),
},
)
.await? .await?
.as_ref() .as_ref()
.and_then(|it| it.location.split_once("/")) .and_then(|it| it.location.split_once("/"))
{ {
None => { None => user_error("You aren't renting anything from here!".to_owned())?,
user_error("You aren't renting anything from here!".to_owned())?
},
Some((ref ex_zone_t, ref ex_zone_c)) => { Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) = if let Some(ex_zone) = ctx
ctx.trans.find_item_by_type_code(ex_zone_t, ex_zone_c) .trans
.await? { .find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data { match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData { Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_), .. }) => { vacate_after: Some(_),
user_error("Your lease is already up for termination.".to_owned())? ..
}, }) => user_error("Your lease is already up for termination.".to_owned())?,
Some(ItemSpecialData::DynzoneData { Some(ItemSpecialData::DynzoneData {
vacate_after: None, zone_exit: ref ex }) => { vacate_after: None,
ctx.trans.save_item_model( zone_exit: ref ex,
&Item { }) => {
ctx.trans
.save_item_model(&Item {
special_data: Some(ItemSpecialData::DynzoneData { special_data: Some(ItemSpecialData::DynzoneData {
zone_exit: ex.clone(), zone_exit: ex.clone(),
vacate_after: Some(Utc::now() + Duration::days(1)) vacate_after: Some(Utc::now() + Duration::days(1)),
}), }),
..(*ex_zone).clone() ..(*ex_zone).clone()
} })
).await?; .await?;
ctx.trans.queue_for_session(ctx.session, Some("The robot files away your notice of intention to vacate. \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"\n")).await? ctx.trans.queue_for_session(ctx.session, Some("The robot files away your notice of intention to vacate. \"You have 24 hours to get all your stuff out, then the landlord will send someone up to boot out anyone still in there, and we will sell anything left behind to cover our costs. If you change your mind before then, just rent again and we'll cancel out your notice and let you keep the same apartment - then you'll have to pay the setup fee again though.\"\n")).await?
}, }
_ => user_error("The premises seem to be broken anyway".to_owned())? _ => user_error("The premises seem to be broken anyway".to_owned())?,
} }
} }
} }

View File

@ -1,33 +1,12 @@
use super::{ use super::{
VerbContext, get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
UserVerb, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
UserVerbRef,
UResult,
ItemSearchParams,
UserError,
user_error,
get_player_item_or_fail,
search_items_for_user,
parsing::parse_count
}; };
use crate::{ use crate::{
models::item::{Buff, BuffCause, BuffImpact, LocationActionType, SkillType},
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{comms::broadcast_to_room, skills::calculate_total_stats_skills_for_user},
static_content::possession_type::possession_data, static_content::possession_type::possession_data,
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
services::{
comms::broadcast_to_room,
skills::calculate_total_stats_skills_for_user,
},
models::item::{
LocationActionType,
Buff,
BuffCause,
BuffImpact,
SkillType,
},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
@ -36,99 +15,141 @@ use std::time;
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to wear it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Wear { possession_id } => possession_id, QueueCommand::Wear { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != player_item.refstr() { if item.location != player_item.refstr() {
user_error( user_error(format!(
format!("You try to wear {} but realise you no longer have it", "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(!ctx.session_dat.less_explicit_mode, 1, false)
) ))?
)?
} }
if item.action_type == LocationActionType::Worn { if item.action_type == LocationActionType::Worn {
user_error("You realise you're already wearing it!".to_owned())?; user_error("You realise you're already wearing it!".to_owned())?;
} }
let poss_data = item.possession_type.as_ref() let poss_data = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| UserError( .ok_or_else(|| {
"That item no longer exists in the game so can't be handled".to_owned()))?; UserError("That item no longer exists in the game so can't be handled".to_owned())
})?;
poss_data.wear_data.as_ref().ok_or_else( poss_data
|| UserError("You can't wear that!".to_owned()))?; .wear_data
.as_ref()
.ok_or_else(|| UserError("You can't wear that!".to_owned()))?;
let msg_exp = format!("{} fumbles around trying to put on {}\n", let msg_exp = format!(
"{} fumbles around trying to put on {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} fumbles around trying to put on {}\n", );
let msg_nonexp = format!(
"{} fumbles around trying to put on {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1)) Ok(time::Duration::from_secs(1))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to wear it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Wear { possession_id } => possession_id, QueueCommand::Wear { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != player_item.refstr() { if item.location != player_item.refstr() {
user_error(format!("You try to wear {} but realise it is no longer there.", user_error(format!(
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? "You try to wear {} but realise it is no longer there.",
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))?
} }
if item.action_type == LocationActionType::Worn { if item.action_type == LocationActionType::Worn {
user_error("You realise you're already wearing it!".to_owned())?; user_error("You realise you're already wearing it!".to_owned())?;
} }
let poss_data = item.possession_type.as_ref() let poss_data = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.ok_or_else(|| UserError( .ok_or_else(|| {
"That item no longer exists in the game so can't be handled".to_owned()))?; UserError("That item no longer exists in the game so can't be handled".to_owned())
})?;
let wear_data = poss_data.wear_data.as_ref().ok_or_else( let wear_data = poss_data
|| UserError("You can't wear that!".to_owned()))?; .wear_data
.as_ref()
.ok_or_else(|| UserError("You can't wear that!".to_owned()))?;
let other_clothes = let other_clothes = ctx
ctx.trans.find_by_action_and_location( .trans
&player_item.refstr(), &LocationActionType::Worn).await?; .find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn)
.await?;
for part in &wear_data.covers_parts { for part in &wear_data.covers_parts {
let thickness: f64 = let thickness: f64 =
other_clothes.iter().fold( other_clothes
wear_data.thickness, .iter()
|tot, other_item| .fold(wear_data.thickness, |tot, other_item| {
match other_item.possession_type.as_ref() match other_item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref()) .and_then(|pd| pd.wear_data.as_ref())
{ {
Some(wd) if wd.covers_parts.contains(&part) => Some(wd) if wd.covers_parts.contains(&part) => tot + wd.thickness,
tot + wd.thickness,
_ => tot, _ => tot,
} }
); });
if thickness > 12.0 { if thickness > 12.0 {
user_error(format!( user_error(format!(
"You're wearing too much on your {} already.", "You're wearing too much on your {} already.",
@ -137,13 +158,24 @@ impl QueueCommandHandler for QueueHandler {
} }
} }
let msg_exp = format!("{} wears {}\n", let msg_exp = format!(
"{} wears {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} wears {}\n", );
let msg_nonexp = format!(
"{} wears {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
item_mut.action_type = LocationActionType::Worn; item_mut.action_type = LocationActionType::Worn;
item_mut.action_type_started = Some(Utc::now()); item_mut.action_type_started = Some(Utc::now());
@ -156,10 +188,10 @@ impl QueueCommandHandler for QueueHandler {
item_type: item_mut.item_type.clone(), item_type: item_mut.item_type.clone(),
item_code: item_mut.item_code.clone(), item_code: item_mut.item_code.clone(),
}, },
impacts: vec!(BuffImpact::ChangeSkill { impacts: vec![BuffImpact::ChangeSkill {
skill: SkillType::Dodge, skill: SkillType::Dodge,
magnitude: -wear_data.dodge_penalty magnitude: -wear_data.dodge_penalty,
}) }],
}); });
if let Some(ref usr) = ctx.user_dat { if let Some(ref usr) = ctx.user_dat {
calculate_total_stats_skills_for_user(&mut player_item_mut, usr); calculate_total_stats_skills_for_user(&mut player_item_mut, usr);
@ -175,7 +207,12 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, mut remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
mut remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let mut get_limit = Some(1); let mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") { if remaining == "all" || remaining.starts_with("all ") {
@ -185,24 +222,37 @@ impl UserVerb for Verb {
get_limit = Some(n); get_limit = Some(n);
remaining = remaining2; remaining = remaining2;
} }
let targets = search_items_for_user(ctx, &ItemSearchParams { let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
item_type_only: Some("possession"), item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100), limit: get_limit.unwrap_or(100),
item_action_type_only: Some(&LocationActionType::Normal), item_action_type_only: Some(&LocationActionType::Normal),
..ItemSearchParams::base(&player_item, &remaining) ..ItemSearchParams::base(&player_item, &remaining)
}).await?; },
)
.await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("The dead don't dress themselves".to_owned())?; user_error("The dead don't dress themselves".to_owned())?;
} }
let mut did_anything: bool = false; let mut did_anything: bool = false;
for target in targets.iter().filter(|t| t.action_type.is_visible_in_look()) { for target in targets
.iter()
.filter(|t| t.action_type.is_visible_in_look())
{
if target.item_type != "possession" { if target.item_type != "possession" {
user_error("You can't wear that!".to_owned())?; user_error("You can't wear that!".to_owned())?;
} }
did_anything = true; did_anything = true;
queue_command(ctx, &QueueCommand::Wear { possession_id: target.item_code.clone() }).await?; queue_command(
ctx,
&QueueCommand::Wear {
possession_id: target.item_code.clone(),
},
)
.await?;
} }
if !did_anything { if !did_anything {
user_error("I didn't find anything matching.".to_owned())?; user_error("I didn't find anything matching.".to_owned())?;

View File

@ -1,16 +1,20 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, use super::{
ItemSearchParams, user_error, get_player_item_or_fail, is_likely_explicit, parsing::parse_to_space, search_item_for_user,
get_player_item_or_fail, is_likely_explicit, user_error, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
search_item_for_user, };
parsing::parse_to_space};
use crate::static_content::npc::npc_by_code; use crate::static_content::npc::npc_by_code;
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait; use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (to_whom_name, say_what_raw) = parse_to_space(remaining); let (to_whom_name, say_what_raw) = parse_to_space(remaining);
let say_what = ignore_special_characters(say_what_raw); let say_what = ignore_special_characters(say_what_raw);
if say_what == "" { if say_what == "" {
@ -20,24 +24,33 @@ impl UserVerb for Verb {
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("Shush, the dead can't talk!".to_string())?; user_error("Shush, the dead can't talk!".to_string())?;
} }
let to_whom = search_item_for_user(ctx, &ItemSearchParams { let to_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true, include_loc_contents: true,
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, &to_whom_name) ..ItemSearchParams::base(&player_item, &to_whom_name)
}).await?; },
)
.await?;
match to_whom.item_type.as_str() { match to_whom.item_type.as_str() {
"npc" => {} "npc" => {}
"player" => {}, "player" => {}
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())? _ => user_error("Only characters (players / NPCs) accept whispers".to_string())?,
} }
ctx.trans.queue_for_session(ctx.session, Some(&format!( ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(
ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"), ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"),
player_item.display_for_session(&ctx.session_dat), player_item.display_for_session(&ctx.session_dat),
to_whom.display_for_session(&ctx.session_dat), to_whom.display_for_session(&ctx.session_dat),
say_what say_what
))).await?; )),
)
.await?;
if player_item == to_whom { if player_item == to_whom {
return Ok(()); return Ok(());
@ -45,15 +58,22 @@ impl UserVerb for Verb {
match to_whom.item_type.as_str() { match to_whom.item_type.as_str() {
"npc" => { "npc" => {
let npc = npc_by_code().get(to_whom.item_code.as_str()) let npc = npc_by_code()
.get(to_whom.item_code.as_str())
.map(Ok) .map(Ok)
.unwrap_or_else(|| user_error("That NPC is no longer available".to_owned()))?; .unwrap_or_else(|| user_error("That NPC is no longer available".to_owned()))?;
if let Some(handler) = npc.message_handler { if let Some(handler) = npc.message_handler {
handler.handle(ctx, &player_item, &to_whom, &say_what).await?; handler
.handle(ctx, &player_item, &to_whom, &say_what)
.await?;
} }
} }
"player" => { "player" => {
match ctx.trans.find_session_for_player(&to_whom.item_code).await? { match ctx
.trans
.find_session_for_player(&to_whom.item_code)
.await?
{
None => user_error("That character is asleep.".to_string())?, None => user_error("That character is asleep.".to_string())?,
Some((other_session, other_session_dets)) => { Some((other_session, other_session_dets)) => {
if other_session_dets.less_explicit_mode && is_likely_explicit(&say_what) { if other_session_dets.less_explicit_mode && is_likely_explicit(&say_what) {
@ -61,16 +81,21 @@ impl UserVerb for Verb {
content, and your message looked explicit, so it wasn't sent." content, and your message looked explicit, so it wasn't sent."
.to_owned())? .to_owned())?
} else { } else {
ctx.trans.queue_for_session(&other_session, Some(&format!( ctx.trans
.queue_for_session(
&other_session,
Some(&format!(
ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"), ansi!("<blue>{} whispers to {}: \"{}\"<reset>\n"),
player_item.display_for_session(&ctx.session_dat), player_item.display_for_session(&ctx.session_dat),
to_whom.display_for_session(&ctx.session_dat), to_whom.display_for_session(&ctx.session_dat),
say_what say_what
))).await?; )),
)
.await?;
}
} }
} }
} }
},
_ => {} _ => {}
} }

View File

@ -1,27 +1,35 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult}; use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait; use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
use chrono::Utc; use chrono::Utc;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, _remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
_remaining: &str,
) -> UResult<()> {
let mut msg = String::new(); let mut msg = String::new();
msg.push_str(&format!(ansi!("<bold><bgblue><white>| {:20} | {:20} | {:15} |<reset>\n"), msg.push_str(&format!(
ansi!("<bold><bgblue><white>| {:20} | {:20} | {:15} |<reset>\n"),
ansi!("Username"), ansi!("Username"),
ansi!("Corp"), ansi!("Corp"),
ansi!("Idle"))); ansi!("Idle")
));
for online in ctx.trans.get_online_info().await? { for online in ctx.trans.get_online_info().await? {
if let Some(online_time) = online.time { if let Some(online_time) = online.time {
let diff = let diff = humantime::format_duration(std::time::Duration::from_secs(
humantime::format_duration( (Utc::now() - online_time).num_seconds() as u64,
std::time::Duration::from_secs( ));
(Utc::now() - online_time).num_seconds() as u64));
msg.push_str(&format!( msg.push_str(&format!(
"| {:20} | {:20} | {:15} |\n", &ignore_special_characters(&online.username), "| {:20} | {:20} | {:15} |\n",
&ignore_special_characters(&online.username),
&ignore_special_characters(&online.corp.unwrap_or("".to_string())), &ignore_special_characters(&online.corp.unwrap_or("".to_string())),
&format!("{}", &diff))); &format!("{}", &diff)
));
} }
} }
msg.push_str("\n"); msg.push_str("\n");

View File

@ -1,28 +1,12 @@
use super::{ use super::{
VerbContext, get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult, UserVerb,
UserVerb, UserVerbRef, VerbContext,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_item_for_user,
}; };
use crate::{ use crate::{
models::item::{LocationActionType, SkillType},
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{comms::broadcast_to_room, skills::skill_check_and_grind},
static_content::possession_type::possession_data, static_content::possession_type::possession_data,
regular_tasks::queued_command::{
QueueCommandHandler,
QueueCommand,
queue_command
},
models::item::{
LocationActionType,
SkillType,
},
services::{
comms::broadcast_to_room,
skills::skill_check_and_grind,
},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::time; use std::time;
@ -30,37 +14,66 @@ use std::time;
pub struct QueueHandler; pub struct QueueHandler;
#[async_trait] #[async_trait]
impl QueueCommandHandler for QueueHandler { impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn start_command(
-> UResult<time::Duration> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to wield it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Wield { possession_id } => possession_id, QueueCommand::Wield { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != format!("player/{}", player_item.item_code) { if item.location != format!("player/{}", player_item.item_code) {
user_error("You try to wield it but realise you no longer have it".to_owned())? user_error("You try to wield it but realise you no longer have it".to_owned())?
} }
let msg_exp = format!("{} fumbles around with {} {}\n", let msg_exp = format!(
"{} fumbles around with {} {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&player_item.pronouns.possessive, &player_item.pronouns.possessive,
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} fumbles around with {} {}\n", );
let msg_nonexp = format!(
"{} fumbles around with {} {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&player_item.pronouns.possessive, &player_item.pronouns.possessive,
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
let mut draw_level: f64 = *player_item.total_skills.get(&SkillType::Quickdraw).to_owned().unwrap_or(&8.0); broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut draw_level: f64 = *player_item
.total_skills
.get(&SkillType::Quickdraw)
.to_owned()
.unwrap_or(&8.0);
let mut player_item_mut = (*player_item).clone(); let mut player_item_mut = (*player_item).clone();
let skill_result = let skill_result = skill_check_and_grind(
skill_check_and_grind(ctx.trans, &mut player_item_mut, &SkillType::Quickdraw, draw_level).await?; ctx.trans,
&mut player_item_mut,
&SkillType::Quickdraw,
draw_level,
)
.await?;
if skill_result < -0.5 { if skill_result < -0.5 {
draw_level -= 2.0; draw_level -= 2.0;
} else if skill_result < -0.25 { } else if skill_result < -0.25 {
@ -73,37 +86,63 @@ impl QueueCommandHandler for QueueHandler {
ctx.trans.save_item_model(&player_item_mut).await?; ctx.trans.save_item_model(&player_item_mut).await?;
let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0); let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0);
Ok(time::Duration::from_millis((wait_ticks * 500.0).round() as u64)) Ok(time::Duration::from_millis(
(wait_ticks * 500.0).round() as u64
))
} }
#[allow(unreachable_patterns)] #[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) async fn finish_command(
-> UResult<()> { &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to wield it, but your ghostly hands slip through it uselessly".to_owned())?; 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 command {
QueueCommand::Wield { possession_id } => possession_id, QueueCommand::Wield { possession_id } => possession_id,
_ => user_error("Unexpected command".to_owned())? _ => user_error("Unexpected command".to_owned())?,
}; };
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { let item = match ctx
.trans
.find_item_by_type_code("possession", &item_id)
.await?
{
None => user_error("Item not found".to_owned())?, None => user_error("Item not found".to_owned())?,
Some(it) => it Some(it) => it,
}; };
if item.location != format!("player/{}", player_item.item_code) { if item.location != format!("player/{}", player_item.item_code) {
user_error("You try to wield it but realise you no longer have it".to_owned())? user_error("You try to wield it but realise you no longer have it".to_owned())?
} }
let msg_exp = format!("{} wields {}\n", let msg_exp = format!(
"{} wields {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} wields {}\n", );
let msg_nonexp = format!(
"{} wields {}\n",
&player_item.display_for_sentence(false, 1, true), &player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false)); &item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
ctx.trans.set_exclusive_action_type_to(&item, broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
ctx.trans
.set_exclusive_action_type_to(
&item,
&LocationActionType::Wielded, &LocationActionType::Wielded,
&LocationActionType::Normal).await?; &LocationActionType::Normal,
)
.await?;
Ok(()) Ok(())
} }
} }
@ -111,27 +150,47 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let weapon = search_item_for_user(ctx, &ItemSearchParams { let weapon = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
limit: 1, limit: 1,
..ItemSearchParams::base(&player_item, &remaining) ..ItemSearchParams::base(&player_item, &remaining)
}).await?; },
)
.await?;
if player_item.death_data.is_some() { if player_item.death_data.is_some() {
user_error("You try to wield it, but your ghostly hands slip through it uselessly".to_owned())?; user_error(
"You try to wield it, but your ghostly hands slip through it uselessly".to_owned(),
)?;
} }
if weapon.action_type == LocationActionType::Wielded { if weapon.action_type == LocationActionType::Wielded {
user_error("You're actually already wielding it.".to_owned())?; user_error("You're actually already wielding it.".to_owned())?;
} }
if weapon.item_type != "possession" || if weapon.item_type != "possession"
weapon.possession_type.as_ref() || weapon
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type)) .and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.weapon_data.as_ref()) .and_then(|poss_data| poss_data.weapon_data.as_ref())
.is_none() { .is_none()
{
user_error("You can't wield that!".to_owned())?; user_error("You can't wield that!".to_owned())?;
} }
queue_command(ctx, &QueueCommand::Wield { possession_id: weapon.item_code.clone() }).await?; queue_command(
ctx,
&QueueCommand::Wield {
possession_id: weapon.item_code.clone(),
},
)
.await?;
Ok(()) Ok(())
} }
} }

View File

@ -1,34 +1,48 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, search_item_for_user}; use super::{
use crate::{ get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
db::ItemSearchParams, VerbContext,
static_content::possession_type::possession_data,
}; };
use crate::{db::ItemSearchParams, static_content::possession_type::possession_data};
use async_trait::async_trait; use async_trait::async_trait;
pub struct Verb; pub struct Verb;
#[async_trait] #[async_trait]
impl UserVerb for Verb { impl UserVerb for Verb {
async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, remaining: &str) -> UResult<()> { async fn handle(
self: &Self,
ctx: &mut VerbContext,
_verb: &str,
remaining: &str,
) -> UResult<()> {
let (write_what_raw, on_what_raw) = match remaining.rsplit_once(" on ") { let (write_what_raw, on_what_raw) = match remaining.rsplit_once(" on ") {
None => user_error("Write on what? Try write something on something".to_owned())?, None => user_error("Write on what? Try write something on something".to_owned())?,
Some(v) => v Some(v) => v,
}; };
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let item = search_item_for_user(ctx, &ItemSearchParams { let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true, include_contents: true,
..ItemSearchParams::base(&player_item, on_what_raw.trim()) ..ItemSearchParams::base(&player_item, on_what_raw.trim())
}).await?; },
)
.await?;
if item.item_type != "possession" { if item.item_type != "possession" {
user_error("You can't write on that!".to_owned())?; user_error("You can't write on that!".to_owned())?;
} }
let handler = match item.possession_type.as_ref() let handler = match item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.write_handler) { .and_then(|pd| pd.write_handler)
{
None => user_error("You can't write on that!".to_owned())?, None => user_error("You can't write on that!".to_owned())?,
Some(h) => h Some(h) => h,
}; };
handler.write_cmd(ctx, &player_item, &item, write_what_raw.trim()).await?; handler
.write_cmd(ctx, &player_item, &item, write_what_raw.trim())
.await?;
Ok(()) Ok(())
} }

View File

@ -1,13 +1,13 @@
use serde::{Serialize, Deserialize};
use serde_json::Value;
use chrono::{DateTime, Utc};
use crate::services::effect::DelayedHealthEffect; use crate::services::effect::DelayedHealthEffect;
use std::collections::VecDeque;
use crate::static_content::room::Direction; use crate::static_content::room::Direction;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::VecDeque;
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub enum TaskRecurrence { pub enum TaskRecurrence {
FixedDuration { seconds: u32 } FixedDuration { seconds: u32 },
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -16,7 +16,7 @@ pub enum TaskDetails {
RunQueuedCommand, RunQueuedCommand,
NPCSay { NPCSay {
npc_code: String, npc_code: String,
say_code: String say_code: String,
}, },
NPCWander { NPCWander {
npc_code: String, npc_code: String,
@ -26,26 +26,29 @@ pub enum TaskDetails {
}, },
AttackTick, AttackTick,
RecloneNPC { RecloneNPC {
npc_code: String npc_code: String,
}, },
RotCorpse { RotCorpse {
corpse_code: String corpse_code: String,
}, },
DelayedHealth { DelayedHealth {
item: String, item: String,
effect_series: VecDeque<DelayedHealthEffect> effect_series: VecDeque<DelayedHealthEffect>,
}, },
ExpireItem { ExpireItem {
item_code: String item_code: String,
}, },
ChargeRoom { ChargeRoom {
zone_item: String, zone_item: String,
daily_price: u64 daily_price: u64,
}, },
SwingShut { SwingShut {
room_item: String, room_item: String,
direction: Direction direction: Direction,
} },
DestroyUser {
username: String,
},
} }
impl TaskDetails { impl TaskDetails {
pub fn name(self: &Self) -> &'static str { pub fn name(self: &Self) -> &'static str {
@ -62,6 +65,7 @@ impl TaskDetails {
ExpireItem { .. } => "ExpireItem", ExpireItem { .. } => "ExpireItem",
ChargeRoom { .. } => "ChargeRoom", ChargeRoom { .. } => "ChargeRoom",
SwingShut { .. } => "SwingShut", SwingShut { .. } => "SwingShut",
DestroyUser { .. } => "DestroyUser",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too. // Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
} }
} }
@ -83,7 +87,7 @@ impl Default for TaskMeta {
is_static: false, is_static: false,
recurrence: None, recurrence: None,
consecutive_failure_count: 0, consecutive_failure_count: 0,
next_scheduled: Utc::now() + chrono::Duration::seconds(3600) next_scheduled: Utc::now() + chrono::Duration::seconds(3600),
} }
} }
} }
@ -103,12 +107,12 @@ pub struct TaskOther {
#[serde(flatten)] #[serde(flatten)]
pub meta: TaskMeta, pub meta: TaskMeta,
pub task_type: String, pub task_type: String,
pub task_details: Value pub task_details: Value,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(untagged)] #[serde(untagged)]
pub enum TaskParse { pub enum TaskParse {
Known(Task), Known(Task),
Unknown(TaskOther) Unknown(TaskOther),
} }

View File

@ -1,9 +1,9 @@
use serde::{Serialize, Deserialize};
use chrono::{DateTime, Utc};
use super::{ use super::{
item::{SkillType, StatType}, item::{SkillType, StatType},
journal::JournalState journal::JournalState,
}; };
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap; use std::collections::BTreeMap;
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -19,7 +19,7 @@ pub struct UserExperienceData {
pub spent_xp: u64, // Since last chargen complete. pub spent_xp: u64, // Since last chargen complete.
pub journals: JournalState, pub journals: JournalState,
pub xp_change_for_this_reroll: i64, pub xp_change_for_this_reroll: i64,
pub crafted_items: BTreeMap<String, u64> pub crafted_items: BTreeMap<String, u64>,
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
@ -41,6 +41,7 @@ pub struct User {
pub last_skill_improve: BTreeMap<SkillType, DateTime<Utc>>, pub last_skill_improve: BTreeMap<SkillType, DateTime<Utc>>,
pub last_page_from: Option<String>, pub last_page_from: Option<String>,
pub credits: u64, pub credits: u64,
pub danger_code: Option<String>,
// Reminder: Consider backwards compatibility when updating this. // Reminder: Consider backwards compatibility when updating this.
} }
@ -49,7 +50,7 @@ impl Default for UserTermData {
UserTermData { UserTermData {
accepted_terms: BTreeMap::new(), accepted_terms: BTreeMap::new(),
terms_complete: false, terms_complete: false,
last_presented_term: None last_presented_term: None,
} }
} }
} }
@ -83,7 +84,8 @@ impl Default for User {
raw_stats: BTreeMap::new(), raw_stats: BTreeMap::new(),
last_skill_improve: BTreeMap::new(), last_skill_improve: BTreeMap::new(),
last_page_from: None, last_page_from: None,
credits: 500 credits: 500,
danger_code: None,
} }
} }
} }

View File

@ -1,29 +1,33 @@
use tokio::{task, time, sync::oneshot}; #[double]
use async_trait::async_trait; use crate::db::DBTrans;
#[cfg(not(test))]
use crate::models::task::{TaskParse, TaskRecurrence};
use crate::{ use crate::{
DResult,
db, db,
models::task::Task,
listener::{ListenerMap, ListenerSend}, listener::{ListenerMap, ListenerSend},
static_content::npc, message_handler::user_commands::{delete, drop, open, rent},
models::task::Task,
services::{combat, effect}, services::{combat, effect},
message_handler::user_commands::{drop, rent, open}, static_content::npc,
DResult,
}; };
#[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence}; use async_trait::async_trait;
use mockall_double::double;
#[double] use crate::db::DBTrans;
use blastmud_interfaces::MessageToListener; use blastmud_interfaces::MessageToListener;
#[cfg(not(test))]
use chrono::Utc;
use log::warn; use log::warn;
use mockall_double::double;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
#[cfg(not(test))] use std::ops::AddAssign;
use std::collections::BTreeMap; use std::collections::BTreeMap;
#[cfg(not(test))] use chrono::Utc; #[cfg(not(test))]
use std::ops::AddAssign;
use tokio::{sync::oneshot, task, time};
pub mod queued_command; pub mod queued_command;
pub struct TaskRunContext<'l> { pub struct TaskRunContext<'l> {
pub trans: &'l DBTrans, pub trans: &'l DBTrans,
pub task: &'l mut Task pub task: &'l mut Task,
} }
#[async_trait] #[async_trait]
@ -31,11 +35,13 @@ pub trait TaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>>; async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>>;
} }
fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn TaskHandler + Sync + Send)> { fn task_handler_registry(
static TASK_HANDLER_REGISTRY: OnceCell<BTreeMap<&'static str, &'static (dyn TaskHandler + Sync + Send)>> = ) -> &'static BTreeMap<&'static str, &'static (dyn TaskHandler + Sync + Send)> {
OnceCell::new(); static TASK_HANDLER_REGISTRY: OnceCell<
TASK_HANDLER_REGISTRY.get_or_init( BTreeMap<&'static str, &'static (dyn TaskHandler + Sync + Send)>,
|| vec!( > = OnceCell::new();
TASK_HANDLER_REGISTRY.get_or_init(|| {
vec![
("RunQueuedCommand", queued_command::HANDLER.clone()), ("RunQueuedCommand", queued_command::HANDLER.clone()),
("NPCSay", npc::SAY_HANDLER.clone()), ("NPCSay", npc::SAY_HANDLER.clone()),
("NPCWander", npc::WANDER_HANDLER.clone()), ("NPCWander", npc::WANDER_HANDLER.clone()),
@ -47,8 +53,11 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
("ExpireItem", drop::EXPIRE_ITEM_HANDLER.clone()), ("ExpireItem", drop::EXPIRE_ITEM_HANDLER.clone()),
("ChargeRoom", rent::CHARGE_ROOM_HANDLER.clone()), ("ChargeRoom", rent::CHARGE_ROOM_HANDLER.clone()),
("SwingShut", open::SWING_SHUT_HANDLER.clone()), ("SwingShut", open::SWING_SHUT_HANDLER.clone()),
).into_iter().collect() ("DestroyUser", delete::DESTROY_USER_HANDLER.clone()),
) ]
.into_iter()
.collect()
})
} }
async fn cleanup_session_once(pool: db::DBPool) -> DResult<()> { async fn cleanup_session_once(pool: db::DBPool) -> DResult<()> {
@ -76,24 +85,30 @@ async fn process_sendqueue_once(pool: db::DBPool, listener_map: ListenerMap) ->
loop { loop {
let q = pool.get_from_sendqueue().await?; let q = pool.get_from_sendqueue().await?;
for item in &q { for item in &q {
match listener_map.lock().await.get(&item.session.listener).map(|l| l.clone()) { match listener_map
.lock()
.await
.get(&item.session.listener)
.map(|l| l.clone())
{
None => {} None => {}
Some(listener_sender) => { Some(listener_sender) => {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
listener_sender.send( listener_sender
ListenerSend { .send(ListenerSend {
message: match item.message.clone() { message: match item.message.clone() {
None => MessageToListener::DisconnectSession { None => MessageToListener::DisconnectSession {
session: item.session.session.clone() session: item.session.session.clone(),
}, },
Some(msg) => MessageToListener::SendToSession { Some(msg) => MessageToListener::SendToSession {
session: item.session.session.clone(), session: item.session.session.clone(),
msg: msg msg: msg,
}
}, },
ack_notify: tx },
} ack_notify: tx,
).await.unwrap_or(()); })
.await
.unwrap_or(());
rx.await.unwrap_or(()); rx.await.unwrap_or(());
pool.delete_from_sendqueue(&item).await?; pool.delete_from_sendqueue(&item).await?;
} }
@ -125,49 +140,78 @@ async fn process_tasks_once(pool: db::DBPool) -> DResult<()> {
loop { loop {
let tx = pool.start_transaction().await?; let tx = pool.start_transaction().await?;
match tx.get_next_scheduled_task().await? { match tx.get_next_scheduled_task().await? {
None => { break; } None => {
break;
}
Some(task_parse) => { Some(task_parse) => {
match task_parse { match task_parse {
TaskParse::Known(mut task) => { TaskParse::Known(mut task) => {
match task_handler_registry().get(task.details.name()) { match task_handler_registry().get(task.details.name()) {
None => { None => {
warn!("Found a known but unregistered task type: {}", warn!(
task.details.name()); "Found a known but unregistered task type: {}",
task.details.name()
);
// This is always a logic error, so just delete the task // This is always a logic error, so just delete the task
// to help with recovery. // to help with recovery.
tx.delete_task(&task.details.name(), &task.meta.task_code).await?; tx.delete_task(&task.details.name(), &task.meta.task_code)
.await?;
tx.commit().await?; tx.commit().await?;
} }
Some(handler) => { Some(handler) => {
let mut ctx = TaskRunContext { trans: &tx, task: &mut task }; let mut ctx = TaskRunContext {
trans: &tx,
task: &mut task,
};
match handler.do_task(&mut ctx).await { match handler.do_task(&mut ctx).await {
Err(e) => { Err(e) => {
task.meta.consecutive_failure_count += 1; task.meta.consecutive_failure_count += 1;
warn!("Error handling event of type {} code {} (consecutive count: {}): {:?}", warn!("Error handling event of type {} code {} (consecutive count: {}): {:?}",
&task.details.name(), &task.meta.task_code, &task.details.name(), &task.meta.task_code,
task.meta.consecutive_failure_count, e); task.meta.consecutive_failure_count, e);
if task.meta.consecutive_failure_count > 3 && !task.meta.is_static { if task.meta.consecutive_failure_count > 3
tx.delete_task(&task.details.name(), &task.meta.task_code).await?; && !task.meta.is_static
{
tx.delete_task(
&task.details.name(),
&task.meta.task_code,
)
.await?;
} else { } else {
task.meta.next_scheduled = Utc::now() + chrono::Duration::seconds(60); task.meta.next_scheduled =
tx.update_task(&task.details.name(), &task.meta.task_code, Utc::now() + chrono::Duration::seconds(60);
&TaskParse::Known(task.clone())).await?; tx.update_task(
&task.details.name(),
&task.meta.task_code,
&TaskParse::Known(task.clone()),
)
.await?;
} }
tx.commit().await?; tx.commit().await?;
}, }
Ok(resched) => { Ok(resched) => {
task.meta.consecutive_failure_count = 0; task.meta.consecutive_failure_count = 0;
match task.meta.recurrence.clone().or( match task.meta.recurrence.clone().or(resched.map(|r| {
resched.map(|r| TaskRecurrence::FixedDuration { seconds: r.as_secs() as u32 })) { TaskRecurrence::FixedDuration {
seconds: r.as_secs() as u32,
}
})) {
None => { None => {
tx.delete_task(&task.details.name(), tx.delete_task(
&task.meta.task_code).await?; &task.details.name(),
&task.meta.task_code,
)
.await?;
} }
Some(TaskRecurrence::FixedDuration { seconds }) => { Some(TaskRecurrence::FixedDuration { seconds }) => {
task.meta.next_scheduled = Utc::now() + task.meta.next_scheduled = Utc::now()
chrono::Duration::seconds(seconds as i64); + chrono::Duration::seconds(seconds as i64);
tx.update_task(&task.details.name(), &task.meta.task_code, tx.update_task(
&TaskParse::Known(task.clone())).await?; &task.details.name(),
&task.meta.task_code,
&TaskParse::Known(task.clone()),
)
.await?;
} }
} }
tx.commit().await?; tx.commit().await?;
@ -177,26 +221,34 @@ async fn process_tasks_once(pool: db::DBPool) -> DResult<()> {
} }
} }
TaskParse::Unknown(mut task) => { TaskParse::Unknown(mut task) => {
warn!("Found unknown task type: {}, code: {}", warn!(
&task.task_type, &task.meta.task_code); "Found unknown task type: {}, code: {}",
&task.task_type, &task.meta.task_code
);
if task.meta.is_static { if task.meta.is_static {
// Probably a new (or newly removed) static type. // Probably a new (or newly removed) static type.
// We just skip this tick of it. // We just skip this tick of it.
match task.meta.recurrence { match task.meta.recurrence {
None => { None => {
tx.delete_task(&task.task_type, &task.meta.task_code).await?; tx.delete_task(&task.task_type, &task.meta.task_code)
.await?;
tx.commit().await?; tx.commit().await?;
} }
Some(TaskRecurrence::FixedDuration { seconds }) => { Some(TaskRecurrence::FixedDuration { seconds }) => {
task.meta.next_scheduled.add_assign( task.meta
chrono::Duration::seconds(seconds as i64) .next_scheduled
); .add_assign(chrono::Duration::seconds(seconds as i64));
tx.update_task(&task.task_type, &task.meta.task_code, tx.update_task(
&TaskParse::Unknown(task.clone())).await?; &task.task_type,
&task.meta.task_code,
&TaskParse::Unknown(task.clone()),
)
.await?;
} }
} }
} else { } else {
tx.delete_task(&task.task_type, &task.meta.task_code).await?; tx.delete_task(&task.task_type, &task.meta.task_code)
.await?;
tx.commit().await?; tx.commit().await?;
} }
} }
@ -230,14 +282,15 @@ fn start_task_runner(pool: db::DBPool) {
async fn send_version_once(listener_map: ListenerMap) -> DResult<()> { async fn send_version_once(listener_map: ListenerMap) -> DResult<()> {
for listener_sender in listener_map.lock().await.values().cloned() { for listener_sender in listener_map.lock().await.values().cloned() {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
listener_sender.send( listener_sender
ListenerSend { .send(ListenerSend {
message: MessageToListener::GameserverVersion { message: MessageToListener::GameserverVersion {
version: env!("GIT_VERSION").to_owned() version: env!("GIT_VERSION").to_owned(),
}, },
ack_notify: tx ack_notify: tx,
} })
).await.unwrap_or(()); .await
.unwrap_or(());
rx.await.unwrap_or(()); rx.await.unwrap_or(());
} }
Ok(()) Ok(())

View File

@ -1,47 +1,52 @@
use super::{TaskHandler, TaskRunContext}; use super::{TaskHandler, TaskRunContext};
use async_trait::async_trait;
use std::time;
use chrono::Utc;
use crate::DResult;
use serde::{Serialize, Deserialize};
use std::collections::BTreeMap;
use crate::models::task::{
Task,
TaskMeta,
TaskDetails,
};
use crate::message_handler::user_commands::{ use crate::message_handler::user_commands::{
VerbContext, close, cut, drop, get, get_user_or_fail, movement, open, remove, use_cmd, user_error, wear,
CommandHandlingError, wield, CommandHandlingError, UResult, VerbContext,
UResult,
close,
cut,
drop,
get,
get_user_or_fail,
movement,
open,
remove,
use_cmd,
user_error,
wear,
wield,
}; };
use crate::models::task::{Task, TaskDetails, TaskMeta};
use crate::static_content::room::Direction; use crate::static_content::room::Direction;
use crate::DResult;
use async_trait::async_trait;
use chrono::Utc;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use std::time;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum QueueCommand { pub enum QueueCommand {
CloseDoor { direction: Direction }, CloseDoor {
Cut { from_corpse: String, what_part: String }, direction: Direction,
Drop { possession_id: String }, },
Get { possession_id: String }, Cut {
Movement { direction: Direction }, from_corpse: String,
OpenDoor { direction: Direction }, what_part: String,
Remove { possession_id: String }, },
Use { possession_id: String, target_id: String }, Drop {
Wear { possession_id: String }, possession_id: String,
Wield { possession_id: String }, },
Get {
possession_id: String,
},
Movement {
direction: Direction,
},
OpenDoor {
direction: Direction,
},
Remove {
possession_id: String,
},
Use {
possession_id: String,
target_id: String,
},
Wear {
possession_id: String,
},
Wield {
possession_id: String,
},
} }
impl QueueCommand { impl QueueCommand {
pub fn name(&self) -> &'static str { pub fn name(&self) -> &'static str {
@ -63,25 +68,69 @@ impl QueueCommand {
#[async_trait] #[async_trait]
pub trait QueueCommandHandler { pub trait QueueCommandHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult<time::Duration>; async fn start_command(
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult<()>; &self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration>;
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()>;
} }
fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn QueueCommandHandler + Sync + Send)> { fn queue_command_registry(
static REGISTRY: OnceCell<BTreeMap<&'static str, &'static (dyn QueueCommandHandler + Sync + Send)>> = ) -> &'static BTreeMap<&'static str, &'static (dyn QueueCommandHandler + Sync + Send)> {
OnceCell::new(); static REGISTRY: OnceCell<
REGISTRY.get_or_init(|| vec!( BTreeMap<&'static str, &'static (dyn QueueCommandHandler + Sync + Send)>,
("Cut", &cut::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), > = OnceCell::new();
("CloseDoor", &close::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), REGISTRY.get_or_init(|| {
("Drop", &drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), vec![
("Get", &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), (
("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), "Cut",
("OpenDoor", &open::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), &cut::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
("Remove", &remove::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), ),
("Use", &use_cmd::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), (
("Wear", &wear::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), "CloseDoor",
("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), &close::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
).into_iter().collect()) ),
(
"Drop",
&drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Get",
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Movement",
&movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"OpenDoor",
&open::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Remove",
&remove::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Use",
&use_cmd::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Wear",
&wear::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
(
"Wield",
&wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
]
.into_iter()
.collect()
})
} }
pub async fn queue_command(ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult<()> { pub async fn queue_command(ctx: &mut VerbContext<'_>, command: &QueueCommand) -> UResult<()> {
@ -95,29 +144,42 @@ pub async fn queue_command(ctx: &mut VerbContext<'_>, command: &QueueCommand) ->
match queue_command_registry() match queue_command_registry()
.get(&command.name()) .get(&command.name())
.expect("QueueCommand to have been registered") .expect("QueueCommand to have been registered")
.start_command(ctx, &command).await { .start_command(ctx, &command)
.await
{
Err(CommandHandlingError::UserError(err_msg)) => { Err(CommandHandlingError::UserError(err_msg)) => {
ctx.session_dat.queue.truncate(0); ctx.session_dat.queue.truncate(0);
ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?; ctx.trans
ctx.trans.queue_for_session(&ctx.session, Some(&(err_msg + "\r\n"))).await?; .save_session_model(ctx.session, ctx.session_dat)
.await?;
ctx.trans
.queue_for_session(&ctx.session, Some(&(err_msg + "\r\n")))
.await?;
} }
Err(CommandHandlingError::SystemError(e)) => Err(e)?, Err(CommandHandlingError::SystemError(e)) => Err(e)?,
Ok(dur) => { Ok(dur) => {
ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?; ctx.trans
ctx.trans.upsert_task(&Task { .save_session_model(ctx.session, ctx.session_dat)
.await?;
ctx.trans
.upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: username, task_code: username,
next_scheduled: Utc::now() + chrono::Duration::from_std(dur)?, next_scheduled: Utc::now() + chrono::Duration::from_std(dur)?,
..Default::default() ..Default::default()
}, },
details: TaskDetails::RunQueuedCommand details: TaskDetails::RunQueuedCommand,
}).await?; })
.await?;
} }
} }
} else { } else {
ctx.trans.queue_for_session(ctx.session, Some("[queued]\n")).await?; ctx.trans
ctx.trans.save_session_model(ctx.session, ctx.session_dat).await?; .queue_for_session(ctx.session, Some("[queued]\n"))
.await?;
ctx.trans
.save_session_model(ctx.session, ctx.session_dat)
.await?;
} }
Ok(()) Ok(())
} }
@ -133,34 +195,40 @@ impl TaskHandler for RunQueuedCommandTaskHandler {
// Queue is gone if session is gone, and don't schedule another // Queue is gone if session is gone, and don't schedule another
// job, but otherwise this is a successful run. // job, but otherwise this is a successful run.
return Ok(None); return Ok(None);
}, }
Some(x) => x Some(x) => x,
}; };
let queue_command = match sess_dets.queue.pop_front() { let queue_command = match sess_dets.queue.pop_front() {
None => { return Ok(None); } None => {
Some(x) => x return Ok(None);
}
Some(x) => x,
}; };
let mut user = ctx.trans.find_by_username(username).await?; let mut user = ctx.trans.find_by_username(username).await?;
let mut verbcontext = VerbContext { let mut verbcontext = VerbContext {
session: &listener_sess, session: &listener_sess,
session_dat: &mut sess_dets, session_dat: &mut sess_dets,
user_dat: &mut user, user_dat: &mut user,
trans: ctx.trans trans: ctx.trans,
}; };
let uresult_finish = let uresult_finish = queue_command_registry()
queue_command_registry()
.get(&queue_command.name()) .get(&queue_command.name())
.expect("QueueCommand to have been registered") .expect("QueueCommand to have been registered")
.finish_command(&mut verbcontext, &queue_command).await; .finish_command(&mut verbcontext, &queue_command)
.await;
match uresult_finish { match uresult_finish {
Ok(()) => {} Ok(()) => {}
Err(CommandHandlingError::UserError(err_msg)) => { Err(CommandHandlingError::UserError(err_msg)) => {
ctx.trans.queue_for_session(&listener_sess, Some(&(err_msg + "\r\n"))).await?; ctx.trans
.queue_for_session(&listener_sess, Some(&(err_msg + "\r\n")))
.await?;
sess_dets.queue.truncate(0); sess_dets.queue.truncate(0);
ctx.trans.save_session_model(&listener_sess, &sess_dets).await?; ctx.trans
.save_session_model(&listener_sess, &sess_dets)
.await?;
return Ok(None); return Ok(None);
} }
Err(CommandHandlingError::SystemError(e)) => Err(e)? Err(CommandHandlingError::SystemError(e)) => Err(e)?,
}; };
let next_command_opt = verbcontext.session_dat.queue.front().cloned(); let next_command_opt = verbcontext.session_dat.queue.front().cloned();
@ -170,19 +238,27 @@ impl TaskHandler for RunQueuedCommandTaskHandler {
match queue_command_registry() match queue_command_registry()
.get(&next_command.name()) .get(&next_command.name())
.expect("QueueCommand to have been registered") .expect("QueueCommand to have been registered")
.start_command(&mut verbcontext, &next_command).await { .start_command(&mut verbcontext, &next_command)
.await
{
Err(CommandHandlingError::UserError(err_msg)) => { Err(CommandHandlingError::UserError(err_msg)) => {
ctx.trans.queue_for_session(&listener_sess, Some(&(err_msg + "\r\n"))).await?; ctx.trans
.queue_for_session(&listener_sess, Some(&(err_msg + "\r\n")))
.await?;
sess_dets.queue.truncate(0); sess_dets.queue.truncate(0);
ctx.trans.save_session_model(&listener_sess, &sess_dets).await?; ctx.trans
.save_session_model(&listener_sess, &sess_dets)
.await?;
None None
} }
Err(CommandHandlingError::SystemError(e)) => Err(e)?, Err(CommandHandlingError::SystemError(e)) => Err(e)?,
Ok(dur) => Some(dur) Ok(dur) => Some(dur),
} }
} }
}; };
ctx.trans.save_session_model(&listener_sess, &sess_dets).await?; ctx.trans
.save_session_model(&listener_sess, &sess_dets)
.await?;
Ok(result) Ok(result)
} }

View File

@ -1,81 +1,99 @@
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{user_error, CommandHandlingError, UResult},
models::{
item::{DeathData, Item, LocationActionType, SkillType, Subattack},
journal::JournalType,
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
services::{ services::{
comms::broadcast_to_room, comms::broadcast_to_room,
skills::{
skill_check_and_grind,
skill_check_only,
calculate_total_stats_skills_for_user,
},
destroy_container, destroy_container,
}, skills::{calculate_total_stats_skills_for_user, skill_check_and_grind, skill_check_only},
models::{
item::{Item, LocationActionType, Subattack, SkillType, DeathData},
task::{Task, TaskMeta, TaskDetails},
journal::JournalType,
}, },
static_content::{ static_content::{
possession_type::{WeaponData, WeaponAttackData, DamageType, possession_data, fist}, journals::{award_journal_if_needed, check_journal_for_kill},
npc::npc_by_code, npc::npc_by_code,
species::species_info_map, possession_type::{fist, possession_data, DamageType, WeaponAttackData, WeaponData},
journals::{check_journal_for_kill, award_journal_if_needed} species::{species_info_map, BodyPart},
}, },
message_handler::user_commands::{user_error, UResult, CommandHandlingError},
regular_tasks::{TaskRunContext, TaskHandler},
DResult, DResult,
}; };
use mockall_double::double; use ansi::ansi;
#[double] use crate::db::DBTrans; use async_recursion::async_recursion;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use async_recursion::async_recursion; use mockall_double::double;
use std::time;
use ansi::ansi;
use rand::{prelude::IteratorRandom, Rng}; use rand::{prelude::IteratorRandom, Rng};
use rand_distr::{Normal, Distribution}; use rand_distr::{Distribution, Normal};
use std::time;
async fn soak_damage(ctx: &mut TaskRunContext<'_>, attack: &WeaponAttackData, victim: &Item, presoak_amount: f64) -> DResult<f64> { async fn soak_damage(
let mut damage_by_type: Vec<(&DamageType, f64)> = ctx: &mut TaskRunContext<'_>,
attack.other_damage_types.iter().map( attack: &WeaponAttackData,
|(frac, dtype)| (dtype, frac * presoak_amount)).collect(); victim: &Item,
damage_by_type.push((&attack.base_damage_type, presoak_amount - presoak_amount: f64,
damage_by_type.iter().map(|v|v.1).sum::<f64>())); part: &BodyPart,
) -> DResult<f64> {
let mut damage_by_type: Vec<(&DamageType, f64)> = attack
.other_damage_types
.iter()
.map(|(frac, dtype)| (dtype, frac * presoak_amount))
.collect();
damage_by_type.push((
&attack.base_damage_type,
presoak_amount - damage_by_type.iter().map(|v| v.1).sum::<f64>(),
));
let mut clothes: Vec<Item> = let mut clothes: Vec<Item> = ctx
ctx.trans.find_by_action_and_location(&victim.refstr(), .trans
&LocationActionType::Worn).await? .find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn)
.iter().map(|cl| (*cl.as_ref()).clone()).collect(); .await?
.iter()
.map(|cl| (*cl.as_ref()).clone())
.collect();
clothes.sort_unstable_by(|c1, c2| c2.action_type_started.cmp(&c1.action_type_started)); clothes.sort_unstable_by(|c1, c2| c2.action_type_started.cmp(&c1.action_type_started));
let mut total_damage = 0.0; let mut total_damage = 0.0;
for (damage_type, mut damage_amount) in &damage_by_type { for (damage_type, mut damage_amount) in &damage_by_type {
for mut clothing in &mut clothes { for mut clothing in &mut clothes {
if let Some(soak) = clothing.possession_type if let Some(soak) = clothing
.possession_type
.as_ref() .as_ref()
.and_then(|pt| possession_data().get(pt)) .and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.wear_data.as_ref()) .and_then(|pd| pd.wear_data.as_ref())
.and_then(|wd| wd.soaks.get(&damage_type)) { .filter(|wd| wd.covers_parts.contains(part))
.and_then(|wd| wd.soaks.get(&damage_type))
{
if damage_amount <= 0.0 { if damage_amount <= 0.0 {
break; break;
} }
let soak_amount: f64 = ((soak.max_soak - soak.min_soak) * let soak_amount: f64 = ((soak.max_soak - soak.min_soak)
rand::thread_rng().gen::<f64>()).min(damage_amount); * rand::thread_rng().gen::<f64>())
.min(damage_amount);
damage_amount -= soak_amount; damage_amount -= soak_amount;
let clothes_damage = ((0..(soak_amount as i64)) let clothes_damage = ((0..(soak_amount as i64))
.filter(|_| rand::thread_rng().gen::<f64>() < .filter(|_| rand::thread_rng().gen::<f64>() < soak.damage_probability_per_soak)
soak.damage_probability_per_soak).count() .count() as u64)
as u64).min(clothing.health); .min(clothing.health);
if clothes_damage > 0 { if clothes_damage > 0 {
clothing.health -= clothes_damage; clothing.health -= clothes_damage;
if victim.item_type == "player" { if victim.item_type == "player" {
if let Some((vic_sess, sess_dat)) = if let Some((vic_sess, sess_dat)) =
ctx.trans.find_session_for_player(&victim.item_code).await? ctx.trans.find_session_for_player(&victim.item_code).await?
{ {
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
&vic_sess, &vic_sess,
Some(&format!("A few bits and pieces fly off your {}.\n", Some(&format!(
clothing.display_for_session(&sess_dat))) "A few bits and pieces fly off your {}.\n",
).await?; clothing.display_for_session(&sess_dat)
)),
)
.await?;
} }
} }
} }
@ -86,16 +104,22 @@ async fn soak_damage(ctx: &mut TaskRunContext<'_>, attack: &WeaponAttackData, vi
for clothing in &clothes { for clothing in &clothes {
if clothing.health <= 0 { if clothing.health <= 0 {
ctx.trans.delete_item(&clothing.item_type, &clothing.item_code).await?; ctx.trans
.delete_item(&clothing.item_type, &clothing.item_code)
.await?;
if victim.item_type == "player" { if victim.item_type == "player" {
if let Some((vic_sess, sess_dat)) = if let Some((vic_sess, sess_dat)) =
ctx.trans.find_session_for_player(&victim.item_code).await? ctx.trans.find_session_for_player(&victim.item_code).await?
{ {
ctx.trans.queue_for_session( ctx.trans
.queue_for_session(
&vic_sess, &vic_sess,
Some(&format!("Your {} is completely destroyed; it crumbles away to nothing.\n", Some(&format!(
clothing.display_for_session(&sess_dat))) "Your {} is completely destroyed; it crumbles away to nothing.\n",
).await?; clothing.display_for_session(&sess_dat)
)),
)
.await?;
} }
} }
} }
@ -104,21 +128,39 @@ async fn soak_damage(ctx: &mut TaskRunContext<'_>, attack: &WeaponAttackData, vi
Ok(total_damage) Ok(total_damage)
} }
async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item, victim_item: &mut Item, async fn process_attack(
attack: &WeaponAttackData, weapon: &WeaponData) -> DResult<bool> { ctx: &mut TaskRunContext<'_>,
let attack_skill = *attacker_item.total_skills.get(&weapon.uses_skill).unwrap_or(&0.0); attacker_item: &mut Item,
let victim_dodge_skill = *victim_item.total_skills.get(&SkillType::Dodge).unwrap_or(&0.0); victim_item: &mut Item,
attack: &WeaponAttackData,
weapon: &WeaponData,
) -> DResult<bool> {
let attack_skill = *attacker_item
.total_skills
.get(&weapon.uses_skill)
.unwrap_or(&0.0);
let victim_dodge_skill = *victim_item
.total_skills
.get(&SkillType::Dodge)
.unwrap_or(&0.0);
let dodge_result = skill_check_and_grind(ctx.trans, victim_item, &SkillType::Dodge, let dodge_result =
attack_skill).await?; skill_check_and_grind(ctx.trans, victim_item, &SkillType::Dodge, attack_skill).await?;
let user_opt = if attacker_item.item_type == "player" { let user_opt = if attacker_item.item_type == "player" {
ctx.trans.find_by_username(&attacker_item.item_code).await? ctx.trans.find_by_username(&attacker_item.item_code).await?
} else { None }; } else {
None
};
let attack_result = if let Some(user) = user_opt { let attack_result = if let Some(user) = user_opt {
let raw_skill = *user.raw_skills.get(&weapon.uses_skill).unwrap_or(&0.0); let raw_skill = *user.raw_skills.get(&weapon.uses_skill).unwrap_or(&0.0);
if raw_skill >= weapon.raw_min_to_learn && raw_skill <= weapon.raw_max_to_learn { if raw_skill >= weapon.raw_min_to_learn && raw_skill <= weapon.raw_max_to_learn {
skill_check_and_grind(ctx.trans, attacker_item, &weapon.uses_skill, skill_check_and_grind(
victim_dodge_skill).await? ctx.trans,
attacker_item,
&weapon.uses_skill,
victim_dodge_skill,
)
.await?
} else { } else {
skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill) skill_check_only(&attacker_item, &weapon.uses_skill, victim_dodge_skill)
} }
@ -127,13 +169,24 @@ async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item,
}; };
if dodge_result > attack_result { if dodge_result > attack_result {
let msg_exp = format!("{} dodges out of the way of {}'s attack.\n", let msg_exp = format!(
"{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(true, 1, true), victim_item.display_for_sentence(true, 1, true),
attacker_item.display_for_sentence(true, 1, false)); attacker_item.display_for_sentence(true, 1, false)
let msg_nonexp = format!("{} dodges out of the way of {}'s attack.\n", );
let msg_nonexp = format!(
"{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(false, 1, true), victim_item.display_for_sentence(false, 1, true),
attacker_item.display_for_sentence(false, 1, false)); attacker_item.display_for_sentence(false, 1, false)
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; );
broadcast_to_room(
ctx.trans,
&attacker_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
ctx.trans.save_item_model(&attacker_item).await?; ctx.trans.save_item_model(&attacker_item).await?;
ctx.trans.save_item_model(&victim_item).await?; ctx.trans.save_item_model(&victim_item).await?;
} else { } else {
@ -146,21 +199,30 @@ async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item,
let mut mean_damage: f64 = attack.mean_damage; let mut mean_damage: f64 = attack.mean_damage;
for scaling in attack.skill_scaling.iter() { for scaling in attack.skill_scaling.iter() {
let skill = *attacker_item.total_skills.get(&scaling.skill).unwrap_or(&0.0); let skill = *attacker_item
.total_skills
.get(&scaling.skill)
.unwrap_or(&0.0);
if skill >= scaling.min_skill { if skill >= scaling.min_skill {
mean_damage += (skill - scaling.min_skill) * scaling.mean_damage_per_point_over_min; mean_damage += (skill - scaling.min_skill) * scaling.mean_damage_per_point_over_min;
} }
} }
let actual_damage_presoak = Normal::new(mean_damage, let actual_damage_presoak = Normal::new(mean_damage, attack.stdev_damage)?
attack.stdev_damage)? .sample(&mut rand::thread_rng())
.sample(&mut rand::thread_rng()).floor().max(1.0) as i64; .floor()
.max(1.0) as i64;
ctx.trans.save_item_model(&attacker_item).await?; ctx.trans.save_item_model(&attacker_item).await?;
let actual_damage = soak_damage(ctx, &attack, victim_item, actual_damage_presoak as f64).await? as i64; let actual_damage = soak_damage(
let msg_exp = attack.success_message(&attacker_item, victim_item, ctx,
&part, true); &attack,
let msg_nonexp = attack.success_message(&attacker_item, victim_item, victim_item,
&part, false); actual_damage_presoak as f64,
&part,
)
.await? as i64;
let msg_exp = attack.success_message(&attacker_item, victim_item, &part, true);
let msg_nonexp = attack.success_message(&attacker_item, victim_item, &part, false);
if actual_damage == 0 { if actual_damage == 0 {
let msg_exp = format!( let msg_exp = format!(
"{}'s attack bounces off {}'s {}.\n", "{}'s attack bounces off {}'s {}.\n",
@ -168,17 +230,29 @@ async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item,
&victim_item.display_for_sentence(true, 1, false), &victim_item.display_for_sentence(true, 1, false),
&part.display(victim_item.sex.clone()) &part.display(victim_item.sex.clone())
); );
let msg_nonexp = let msg_nonexp = format!(
format!(
"{}'s attack bounces off {}'s {}.\n", "{}'s attack bounces off {}'s {}.\n",
attacker_item.display_for_sentence(false, 1, true), attacker_item.display_for_sentence(false, 1, true),
victim_item.display_for_sentence(false, 1, false), victim_item.display_for_sentence(false, 1, false),
&part.display(None) &part.display(None)
); );
broadcast_to_room(&ctx.trans, &victim_item.location, broadcast_to_room(
None, &msg_exp, Some(&msg_nonexp)).await?; &ctx.trans,
} else if change_health(ctx.trans, -actual_damage, victim_item, &victim_item.location,
&msg_exp, &msg_nonexp).await? { None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
} else if change_health(
ctx.trans,
-actual_damage,
victim_item,
&msg_exp,
&msg_nonexp,
)
.await?
{
ctx.trans.save_item_model(victim_item).await?; ctx.trans.save_item_model(victim_item).await?;
return Ok(true); return Ok(true);
} }
@ -187,7 +261,14 @@ async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item,
let msg_exp = &(attack.start_message(&attacker_item, victim_item, true) + ".\n"); let msg_exp = &(attack.start_message(&attacker_item, victim_item, true) + ".\n");
let msg_nonexp = &(attack.start_message(&attacker_item, victim_item, false) + ".\n"); let msg_nonexp = &(attack.start_message(&attacker_item, victim_item, false) + ".\n");
broadcast_to_room(ctx.trans, &attacker_item.location, None, msg_exp, Some(msg_nonexp)).await?; broadcast_to_room(
ctx.trans,
&attacker_item.location,
None,
msg_exp,
Some(msg_nonexp),
)
.await?;
Ok(false) Ok(false)
} }
@ -196,30 +277,50 @@ pub struct AttackTaskHandler;
#[async_trait] #[async_trait]
impl TaskHandler for AttackTaskHandler { impl TaskHandler for AttackTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (ctype, ccode) = ctx.task.meta.task_code.split_once("/") let (ctype, ccode) = ctx
.task
.meta
.task_code
.split_once("/")
.ok_or("Invalid AttackTick task code")?; .ok_or("Invalid AttackTick task code")?;
let mut attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? { let mut attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? {
None => { return Ok(None); } // Player is gone None => {
Some(item) => (*item).clone() return Ok(None);
} // Player is gone
Some(item) => (*item).clone(),
}; };
let (vtype, vcode) = let (vtype, vcode) = match attacker_item
match attacker_item.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()).and_then(|v| v.split_once("/")) { .active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
.and_then(|v| v.split_once("/"))
{
None => return Ok(None), None => return Ok(None),
Some(x) => x Some(x) => x,
}; };
let mut victim_item = match ctx.trans.find_item_by_type_code(vtype, vcode).await? { let mut victim_item = match ctx.trans.find_item_by_type_code(vtype, vcode).await? {
None => { return Ok(None); } None => {
Some(item) => (*item).clone() return Ok(None);
}
Some(item) => (*item).clone(),
}; };
if attacker_item.death_data.is_some() || victim_item.death_data.is_some() { if attacker_item.death_data.is_some() || victim_item.death_data.is_some() {
return Ok(None) return Ok(None);
} }
let weapon = what_wielded(ctx.trans, &attacker_item).await?; let weapon = what_wielded(ctx.trans, &attacker_item).await?;
if process_attack(ctx, &mut attacker_item, &mut victim_item, &weapon.normal_attack, &weapon).await? { if process_attack(
ctx,
&mut attacker_item,
&mut victim_item,
&weapon.normal_attack,
&weapon,
)
.await?
{
Ok(None) Ok(None)
} else { } else {
Ok(Some(attack_speed(&attacker_item))) Ok(Some(attack_speed(&attacker_item)))
@ -227,29 +328,35 @@ impl TaskHandler for AttackTaskHandler {
} }
} }
pub async fn change_health(trans: &DBTrans, pub async fn change_health(
trans: &DBTrans,
change: i64, change: i64,
victim: &mut Item, victim: &mut Item,
reason_exp: &str, reason_nonexp: &str) -> DResult<bool> { reason_exp: &str,
reason_nonexp: &str,
) -> DResult<bool> {
let maxh = max_health(victim); let maxh = max_health(victim);
let new_health = ((victim.health as i64 + change).max(0) as u64).min(maxh); let new_health = ((victim.health as i64 + change).max(0) as u64).min(maxh);
if change >= 0 && new_health == victim.health { if change >= 0 && new_health == victim.health {
return Ok(false); return Ok(false);
} }
let colour = if change > 0 { ansi!("<green>") } else { ansi!("<red>") }; let colour = if change > 0 {
let msg_exp = format!(ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"), ansi!("<green>")
} else {
ansi!("<red>")
};
let msg_exp = format!(
ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
colour, colour,
change, change,
new_health, new_health,
max_health(&victim), max_health(&victim),
reason_exp); reason_exp
let msg_nonexp = );
format!(ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"), let msg_nonexp = format!(
colour, ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
change, colour, change, new_health, maxh, reason_nonexp
new_health, );
maxh,
reason_nonexp);
broadcast_to_room(trans, &victim.location, None, &msg_exp, Some(&msg_nonexp)).await?; broadcast_to_room(trans, &victim.location, None, &msg_exp, Some(&msg_nonexp)).await?;
victim.health = new_health; victim.health = new_health;
if new_health == 0 { if new_health == 0 {
@ -260,24 +367,28 @@ pub async fn change_health(trans: &DBTrans,
} }
} }
pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item: &Item) -> DResult<()> { pub async fn consider_reward_for(
trans: &DBTrans,
by_item: &mut Item,
for_item: &Item,
) -> DResult<()> {
if by_item.item_type != "player" { if by_item.item_type != "player" {
return Ok(()); return Ok(());
} }
let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? { let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? {
None => return Ok(()), None => return Ok(()),
Some(r) => r Some(r) => r,
}; };
let mut user = match trans.find_by_username(&by_item.item_code).await? { let mut user = match trans.find_by_username(&by_item.item_code).await? {
None => return Ok(()), None => return Ok(()),
Some(r) => r Some(r) => r,
}; };
let xp_gain = if by_item.total_xp >= for_item.total_xp { let xp_gain = if by_item.total_xp >= for_item.total_xp {
0 0
} else { } else {
let xp_gain = let xp_gain = (((for_item.total_xp - by_item.total_xp) as f64 * 10.0
(((for_item.total_xp - by_item.total_xp) as f64 * 10.0 / (by_item.total_xp + 1) as f64) as u64) / (by_item.total_xp + 1) as f64) as u64)
.min(100); .min(100);
by_item.total_xp += xp_gain; by_item.total_xp += xp_gain;
@ -290,16 +401,34 @@ pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item:
if let Some(npc) = npc_by_code().get(for_item.item_code.as_str()) { if let Some(npc) = npc_by_code().get(for_item.item_code.as_str()) {
if let Some(bonus) = &npc.kill_bonus { if let Some(bonus) = &npc.kill_bonus {
user.credits += bonus.payment; user.credits += bonus.payment;
trans.queue_for_session(&session, Some(&format!("{}\nYour wristpad beeps for a credit of {} for that.\n", bonus.msg, bonus.payment))).await?; trans
.queue_for_session(
&session,
Some(&format!(
"{}\nYour wristpad beeps for a credit of {} for that.\n",
bonus.msg, bonus.payment
)),
)
.await?;
} }
} }
} }
trans.save_user_model(&user).await?; trans.save_user_model(&user).await?;
if xp_gain == 0 { if xp_gain == 0 {
trans.queue_for_session(&session, Some("[You didn't gain any experience for that]\n")).await?; trans
.queue_for_session(
&session,
Some("[You didn't gain any experience for that]\n"),
)
.await?;
} else { } else {
trans.queue_for_session(&session, Some(&format!("You gained {} experience points!\n", xp_gain))).await?; trans
.queue_for_session(
&session,
Some(&format!("You gained {} experience points!\n", xp_gain)),
)
.await?;
} }
Ok(()) Ok(())
@ -317,8 +446,10 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
broadcast_to_room(trans, &whom.location, None, &msg_exp, Some(&msg_nonexp)).await?; broadcast_to_room(trans, &whom.location, None, &msg_exp, Some(&msg_nonexp)).await?;
whom.death_data = Some(DeathData { whom.death_data = Some(DeathData {
parts_remaining: species_info_map().get(&whom.species) parts_remaining: species_info_map()
.map(|sp| sp.corpse_butchers_into.clone()).unwrap_or_else(|| vec!()), .get(&whom.species)
.map(|sp| sp.corpse_butchers_into.clone())
.unwrap_or_else(|| vec![]),
..Default::default() ..Default::default()
}); });
let vic_is_npc = whom.item_type == "npc"; let vic_is_npc = whom.item_type == "npc";
@ -346,27 +477,28 @@ pub async fn handle_death(trans: &DBTrans, whom: &mut Item) -> DResult<()> {
} }
} }
if vic_is_npc { if vic_is_npc {
trans.upsert_task(&Task { trans
.upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: whom.item_code.clone(), task_code: whom.item_code.clone(),
next_scheduled: Utc::now() + chrono::Duration::seconds(120), next_scheduled: Utc::now() + chrono::Duration::seconds(120),
..Default::default() ..Default::default()
}, },
details: TaskDetails::RecloneNPC { details: TaskDetails::RecloneNPC {
npc_code: whom.item_code.clone() npc_code: whom.item_code.clone(),
} },
}).await?; })
.await?;
} else if whom.item_type == "player" { } else if whom.item_type == "player" {
trans.revoke_until_death_consent(&whom.item_code).await?; trans.revoke_until_death_consent(&whom.item_code).await?;
match trans.find_by_username(&whom.item_code).await? { match trans.find_by_username(&whom.item_code).await? {
None => {}, None => {}
Some(mut user) => { Some(mut user) => {
if award_journal_if_needed(trans, &mut user, whom, JournalType::Died).await? { if award_journal_if_needed(trans, &mut user, whom, JournalType::Died).await? {
trans.save_user_model(&user).await?; trans.save_user_model(&user).await?;
} }
} }
} }
} }
Ok(()) Ok(())
} }
@ -377,17 +509,24 @@ pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult<boo
let lost_xp = (player.total_xp / 200).max(10).min(player.total_xp); let lost_xp = (player.total_xp / 200).max(10).min(player.total_xp);
let (session, _) = match trans.find_session_for_player(&player.item_code).await? { let (session, _) = match trans.find_session_for_player(&player.item_code).await? {
None => return Ok(false), None => return Ok(false),
Some(r) => r Some(r) => r,
}; };
let mut user = match trans.find_by_username(&player.item_code).await? { let mut user = match trans.find_by_username(&player.item_code).await? {
None => return Ok(false), None => return Ok(false),
Some(r) => r Some(r) => r,
}; };
trans.queue_for_session(&session, trans
Some(&format!("You lost {} experience points by dying.\n", lost_xp))).await?; .queue_for_session(
&session,
Some(&format!(
"You lost {} experience points by dying.\n",
lost_xp
)),
)
.await?;
player.total_xp -= lost_xp; player.total_xp -= lost_xp;
user.experience.xp_change_for_this_reroll -= lost_xp as i64; user.experience.xp_change_for_this_reroll -= lost_xp as i64;
player.temporary_buffs = vec!(); player.temporary_buffs = vec![];
calculate_total_stats_skills_for_user(player, &user); calculate_total_stats_skills_for_user(player, &user);
player.health = max_health(&player); player.health = max_health(&player);
@ -399,13 +538,16 @@ pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult<boo
pub fn max_health(whom: &Item) -> u64 { pub fn max_health(whom: &Item) -> u64 {
if whom.item_type == "npc" { if whom.item_type == "npc" {
npc_by_code().get(whom.item_code.as_str()) npc_by_code()
.get(whom.item_code.as_str())
.map(|npc| npc.max_health) .map(|npc| npc.max_health)
.unwrap_or(24) .unwrap_or(24)
} else if whom.item_type == "player" { } else if whom.item_type == "player" {
(22.0 + (whom.total_xp as f64).log(1.4)).min(60.0) as u64 (22.0 + (whom.total_xp as f64).log(1.4)).min(60.0) as u64
} else if whom.item_type == "possession" { } else if whom.item_type == "possession" {
whom.possession_type.as_ref().and_then(|pt| possession_data().get(&pt)) whom.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.map(|poss| poss.max_health) .map(|poss| poss.max_health)
.unwrap_or(10) .unwrap_or(10)
} else { } else {
@ -415,14 +557,18 @@ pub fn max_health(whom: &Item) -> u64 {
pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &AttackTaskHandler; pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &AttackTaskHandler;
pub async fn stop_attacking_mut(trans: &DBTrans, new_by_whom: &mut Item, new_to_whom: &mut Item, pub async fn stop_attacking_mut(
auto_refocus: bool) -> trans: &DBTrans,
DResult<()> new_by_whom: &mut Item,
{ new_to_whom: &mut Item,
trans.delete_task("AttackTick", auto_refocus: bool,
&format!("{}/{}", ) -> DResult<()> {
new_by_whom.item_type, trans
new_by_whom.item_code)).await?; .delete_task(
"AttackTick",
&format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code),
)
.await?;
if let Some(ac) = new_to_whom.active_combat.as_mut() { if let Some(ac) = new_to_whom.active_combat.as_mut() {
let old_attacker = format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code); let old_attacker = format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code);
ac.attacked_by.retain(|v| v != &old_attacker); ac.attacked_by.retain(|v| v != &old_attacker);
@ -431,13 +577,17 @@ pub async fn stop_attacking_mut(trans: &DBTrans, new_by_whom: &mut Item, new_to_
ac.attacking = None; ac.attacking = None;
if auto_refocus { if auto_refocus {
let old_vic = format!("{}/{}", new_to_whom.item_type, new_to_whom.item_code); let old_vic = format!("{}/{}", new_to_whom.item_type, new_to_whom.item_code);
let new_vic_opt = ac.attacked_by.iter().filter(|i| **i != old_vic).choose(&mut rand::thread_rng()); let new_vic_opt = ac
.attacked_by
.iter()
.filter(|i| **i != old_vic)
.choose(&mut rand::thread_rng());
if let Some(new_vic) = new_vic_opt { if let Some(new_vic) = new_vic_opt {
if let Some((vtype, vcode)) = new_vic.split_once("/") { if let Some((vtype, vcode)) = new_vic.split_once("/") {
if let Some(vic_item) = trans.find_item_by_type_code(vtype, vcode).await? { if let Some(vic_item) = trans.find_item_by_type_code(vtype, vcode).await? {
let mut new_vic_item = (*vic_item).clone(); let mut new_vic_item = (*vic_item).clone();
match start_attack_mut(trans, new_by_whom, &mut new_vic_item).await { match start_attack_mut(trans, new_by_whom, &mut new_vic_item).await {
Err(CommandHandlingError::UserError(_)) | Ok(()) => {}, Err(CommandHandlingError::UserError(_)) | Ok(()) => {}
Err(CommandHandlingError::SystemError(e)) => return Err(e), Err(CommandHandlingError::SystemError(e)) => return Err(e),
} }
trans.save_item_model(&new_vic_item).await?; trans.save_item_model(&new_vic_item).await?;
@ -453,7 +603,6 @@ pub async fn stop_attacking_mut(trans: &DBTrans, new_by_whom: &mut Item, new_to_
Ok(()) Ok(())
} }
pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> DResult<()> { pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> DResult<()> {
let mut new_by_whom = (*by_whom).clone(); let mut new_by_whom = (*by_whom).clone();
let mut new_to_whom = (*to_whom).clone(); let mut new_to_whom = (*to_whom).clone();
@ -464,21 +613,32 @@ pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) ->
} }
async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> { async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> {
if let Some(item) = trans.find_by_action_and_location( if let Some(item) = trans
&who.refstr(), &LocationActionType::Wielded).await?.first() { .find_by_action_and_location(&who.refstr(), &LocationActionType::Wielded)
if let Some(dat) = item.possession_type.as_ref() .await?
.first()
{
if let Some(dat) = item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.weapon_data.as_ref()) { .and_then(|pd| pd.weapon_data.as_ref())
{
return Ok(dat); return Ok(dat);
} }
} }
// TODO: Search inventory for wielded item first. // TODO: Search inventory for wielded item first.
if who.item_type == "npc" { if who.item_type == "npc" {
if let Some(intrinsic) = npc_by_code().get(who.item_code.as_str()) if let Some(intrinsic) = npc_by_code()
.and_then(|npc| npc.intrinsic_weapon.as_ref()) { .get(who.item_code.as_str())
if let Some(weapon) = possession_data().get(intrinsic).and_then(|p| p.weapon_data.as_ref()) { .and_then(|npc| npc.intrinsic_weapon.as_ref())
return Ok(weapon) {
if let Some(weapon) = possession_data()
.get(intrinsic)
.and_then(|p| p.weapon_data.as_ref())
{
return Ok(weapon);
} }
} }
} }
@ -500,29 +660,50 @@ pub async fn start_attack(trans: &DBTrans, by_whom: &Item, to_whom: &Item) -> UR
} }
#[async_recursion] #[async_recursion]
pub async fn start_attack_mut(trans: &DBTrans, by_whom: &mut Item, to_whom: &mut Item) -> UResult<()> { pub async fn start_attack_mut(
trans: &DBTrans,
by_whom: &mut Item,
to_whom: &mut Item,
) -> UResult<()> {
let mut msg_exp = String::new(); let mut msg_exp = String::new();
let mut msg_nonexp = String::new(); let mut msg_nonexp = String::new();
let mut verb: String = "attacks".to_string(); let mut verb: String = "attacks".to_string();
match by_whom.action_type { match by_whom.action_type {
LocationActionType::Sitting | LocationActionType::Reclining => { LocationActionType::Sitting | LocationActionType::Reclining => {
msg_exp.push_str(&format!(ansi!("{} stands up.\n"), &by_whom.display)); msg_exp.push_str(&format!(ansi!("{} stands up.\n"), &by_whom.display));
msg_nonexp.push_str(&format!(ansi!("{} stands up.\n"), msg_nonexp.push_str(&format!(
by_whom.display_less_explicit.as_ref().unwrap_or(&by_whom.display))); ansi!("{} stands up.\n"),
}, by_whom
.display_less_explicit
.as_ref()
.unwrap_or(&by_whom.display)
));
}
LocationActionType::Attacking(_) => { LocationActionType::Attacking(_) => {
match by_whom.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref().and_then(|s| s.split_once("/"))) { match by_whom
Some((cur_type, cur_code)) if cur_type == to_whom.item_type && cur_code == to_whom.item_code => .active_combat
user_error(format!("You're already attacking {}!", to_whom.pronouns.object))?, .as_ref()
.and_then(|ac| ac.attacking.as_ref().and_then(|s| s.split_once("/")))
{
Some((cur_type, cur_code))
if cur_type == to_whom.item_type && cur_code == to_whom.item_code =>
{
user_error(format!(
"You're already attacking {}!",
to_whom.pronouns.object
))?
}
Some((cur_type, cur_code)) => { Some((cur_type, cur_code)) => {
if let Some(cur_item_arc) = trans.find_item_by_type_code(cur_type, cur_code).await? { if let Some(cur_item_arc) =
trans.find_item_by_type_code(cur_type, cur_code).await?
{
stop_attacking(trans, by_whom, &cur_item_arc).await?; stop_attacking(trans, by_whom, &cur_item_arc).await?;
} }
} }
_ => {} _ => {}
} }
verb = "refocuses ".to_string() + &by_whom.pronouns.possessive + " attacks on"; verb = "refocuses ".to_string() + &by_whom.pronouns.possessive + " attacks on";
}, }
_ => {} _ => {}
} }
@ -530,41 +711,57 @@ pub async fn start_attack_mut(trans: &DBTrans, by_whom: &mut Item, to_whom: &mut
ansi!("<red>{} {} {}.<reset>\n"), ansi!("<red>{} {} {}.<reset>\n"),
&by_whom.display_for_sentence(true, 1, true), &by_whom.display_for_sentence(true, 1, true),
verb, verb,
&to_whom.display_for_sentence(true, 1, false)) &to_whom.display_for_sentence(true, 1, false)
); ));
msg_nonexp.push_str(&format!( msg_nonexp.push_str(&format!(
ansi!("<red>{} {} {}.<reset>\n"), ansi!("<red>{} {} {}.<reset>\n"),
&by_whom.display_for_sentence(false, 1, true), &by_whom.display_for_sentence(false, 1, true),
verb, verb,
&to_whom.display_for_sentence(false, 1, false)) &to_whom.display_for_sentence(false, 1, false)
); ));
let wielded = what_wielded(trans, by_whom).await?; let wielded = what_wielded(trans, by_whom).await?;
msg_exp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, true) + ".\n")); msg_exp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, true) + ".\n"));
msg_nonexp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, false) + ".\n")); msg_nonexp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, false) + ".\n"));
broadcast_to_room(trans, &by_whom.location, None::<&Item>, &msg_exp, Some(msg_nonexp.as_str())).await?; broadcast_to_room(
trans,
&by_whom.location,
None::<&Item>,
&msg_exp,
Some(msg_nonexp.as_str()),
)
.await?;
by_whom.active_combat.get_or_insert_with(|| Default::default()).attacking = by_whom
Some(format!("{}/{}", .active_combat
&to_whom.item_type, &to_whom.item_code)); .get_or_insert_with(|| Default::default())
.attacking = Some(format!("{}/{}", &to_whom.item_type, &to_whom.item_code));
by_whom.action_type = LocationActionType::Attacking(Subattack::Normal); by_whom.action_type = LocationActionType::Attacking(Subattack::Normal);
to_whom.active_combat.get_or_insert_with(|| Default::default()).attacked_by.push( to_whom
format!("{}/{}", .active_combat
&by_whom.item_type, &by_whom.item_code) .get_or_insert_with(|| Default::default())
); .attacked_by
.push(format!("{}/{}", &by_whom.item_type, &by_whom.item_code));
trans.upsert_task(&Task { trans
.upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: format!("{}/{}", by_whom.item_type, by_whom.item_code), task_code: format!("{}/{}", by_whom.item_type, by_whom.item_code),
next_scheduled: Utc::now() + chrono::Duration::milliseconds( next_scheduled: Utc::now()
attack_speed(by_whom).as_millis() as i64), + chrono::Duration::milliseconds(attack_speed(by_whom).as_millis() as i64),
..Default::default() ..Default::default()
}, },
details: TaskDetails::AttackTick details: TaskDetails::AttackTick,
}).await?; })
.await?;
// Auto-counterattack if victim isn't busy. // Auto-counterattack if victim isn't busy.
if to_whom.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()) == None { if to_whom
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
== None
{
start_attack_mut(trans, to_whom, by_whom).await?; start_attack_mut(trans, to_whom, by_whom).await?;
} }
@ -577,14 +774,18 @@ pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult<Item> {
new_item.item_code = format!("{}", trans.alloc_item_code().await?); new_item.item_code = format!("{}", trans.alloc_item_code().await?);
new_item.is_static = false; new_item.is_static = false;
trans.save_item_model(&new_item).await?; trans.save_item_model(&new_item).await?;
trans.upsert_task(&Task { trans
.upsert_task(&Task {
meta: TaskMeta { meta: TaskMeta {
task_code: new_item.item_code.clone(), task_code: new_item.item_code.clone(),
next_scheduled: Utc::now() + chrono::Duration::minutes(5), next_scheduled: Utc::now() + chrono::Duration::minutes(5),
..Default::default() ..Default::default()
}, },
details: TaskDetails::RotCorpse { corpse_code: new_item.item_code.clone() } details: TaskDetails::RotCorpse {
}).await?; corpse_code: new_item.item_code.clone(),
},
})
.await?;
trans.transfer_all_possessions(base_item, &new_item).await?; trans.transfer_all_possessions(base_item, &new_item).await?;
@ -597,16 +798,16 @@ impl TaskHandler for NPCRecloneTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let npc_code = match &ctx.task.details { let npc_code = match &ctx.task.details {
TaskDetails::RecloneNPC { npc_code } => npc_code.clone(), 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? { let mut npc_item = match ctx.trans.find_item_by_type_code("npc", &npc_code).await? {
None => { return Ok(None) }, None => return Ok(None),
Some(r) => (*r).clone() Some(r) => (*r).clone(),
}; };
let npc = match npc_by_code().get(npc_code.as_str()) { let npc = match npc_by_code().get(npc_code.as_str()) {
None => { return Ok(None) }, None => return Ok(None),
Some(r) => r Some(r) => r,
}; };
if npc_item.death_data.is_none() { if npc_item.death_data.is_none() {
@ -628,19 +829,34 @@ impl TaskHandler for RotCorpseTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let corpse_code = match &ctx.task.details { let corpse_code = match &ctx.task.details {
TaskDetails::RotCorpse { corpse_code } => corpse_code.clone(), TaskDetails::RotCorpse { corpse_code } => corpse_code.clone(),
_ => Err("Expected RotCorpse type")? _ => Err("Expected RotCorpse type")?,
}; };
let corpse = match ctx.trans.find_item_by_type_code("corpse", &corpse_code).await? { let corpse = match ctx
None => { return Ok(None) } .trans
Some(r) => r .find_item_by_type_code("corpse", &corpse_code)
.await?
{
None => return Ok(None),
Some(r) => r,
}; };
destroy_container(ctx.trans, &corpse).await?; destroy_container(ctx.trans, &corpse).await?;
let msg_exp = format!("{} rots away to nothing.\n", let msg_exp = format!(
corpse.display_for_sentence(true, 1, true)); "{} rots away to nothing.\n",
let msg_nonexp = format!("{} rots away to nothing.\n", corpse.display_for_sentence(true, 1, true)
corpse.display_for_sentence(false, 1, true)); );
broadcast_to_room(ctx.trans, &corpse.location, None, &msg_exp, Some(&msg_nonexp)).await?; let msg_nonexp = format!(
"{} rots away to nothing.\n",
corpse.display_for_sentence(false, 1, true)
);
broadcast_to_room(
ctx.trans,
&corpse.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(None) Ok(None)
} }
} }

View File

@ -27,6 +27,7 @@ CREATE UNIQUE INDEX item_index ON items ((details->>'item_type'), (details->>'it
CREATE INDEX item_by_loc ON items ((details->>'location')); CREATE INDEX item_by_loc ON items ((details->>'location'));
CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean))); CREATE INDEX item_by_static ON items ((cast(details->>'is_static' as boolean)));
CREATE INDEX item_by_display ON items (lower(details->>'display')); CREATE INDEX item_by_display ON items (lower(details->>'display'));
CREATE INDEX item_by_owner ON items (lower(details->>'owner'));
CREATE INDEX item_by_display_less_explicit ON items (lower(details->>'display_less_explicit')); CREATE INDEX item_by_display_less_explicit ON items (lower(details->>'display_less_explicit'));
CREATE UNIQUE INDEX item_dynamic_entrance ON items ( CREATE UNIQUE INDEX item_dynamic_entrance ON items (
(details->'dynamic_entrance'->>'source_item'), (details->'dynamic_entrance'->>'source_item'),