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 crate::DResult;
#[double]
use crate::db::DBTrans;
use crate::db::{DBPool, ItemSearchParams};
use mockall_double::double;
#[double] use crate::db::DBTrans;
#[cfg(not(test))] use ansi::ansi;
use phf::phf_map;
use crate::models::{item::Item, session::Session, user::User};
use crate::DResult;
#[cfg(not(test))]
use ansi::ansi;
use async_trait::async_trait;
use crate::models::{session::Session, user::User, item::Item};
#[cfg(not(test))] use log::warn;
#[cfg(not(test))]
use log::warn;
use mockall_double::double;
use once_cell::sync::OnceCell;
use phf::phf_map;
use std::sync::Arc;
mod agree;
@ -20,10 +23,11 @@ mod c;
pub mod close;
pub mod corp;
pub mod cut;
pub mod delete;
mod describe;
pub mod drop;
mod gear;
pub mod get;
mod describe;
mod help;
mod ignore;
mod install;
@ -58,24 +62,27 @@ pub struct VerbContext<'l> {
pub session: &'l ListenerSession,
pub session_dat: &'l mut Session,
pub user_dat: &'l mut Option<User>,
pub trans: &'l DBTrans
pub trans: &'l DBTrans,
}
pub enum CommandHandlingError {
UserError(String),
SystemError(Box<dyn std::error::Error + Send + Sync>)
SystemError(Box<dyn std::error::Error + Send + Sync>),
}
use CommandHandlingError::*;
#[async_trait]
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>;
impl<T> From<T> for CommandHandlingError where T: Into<Box<dyn std::error::Error + Send + Sync>> {
impl<T> From<T> for CommandHandlingError
where
T: Into<Box<dyn std::error::Error + Send + Sync>>,
{
fn from(input: T) -> CommandHandlingError {
SystemError(input.into())
}
@ -85,7 +92,6 @@ pub fn user_error<A>(msg: String) -> UResult<A> {
Err(UserError(msg))
}
/* Verb registries list types of commands available in different circumstances. */
pub type UserVerbRef = &'static (dyn UserVerb + Sync + Send);
type UserVerbRegistry = phf::Map<&'static str, UserVerbRef>;
@ -137,6 +143,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"close" => close::VERB,
"corp" => corp::VERB,
"cut" => cut::VERB,
"delete" => delete::VERB,
"drop" => drop::VERB,
"gear" => gear::VERB,
"get" => get::VERB,
@ -224,95 +231,129 @@ pub async fn handle(session: &ListenerSession, msg: &str, pool: &DBPool) -> DRes
None => {
// If the session has been cleaned up from the database, there is
// 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(());
}
Some(v) => v
Some(v) => v,
};
let mut ctx = VerbContext { session, trans: &trans, session_dat: &mut session_dat,
user_dat: &mut user_dat };
let mut ctx = VerbContext {
session,
trans: &trans,
session_dat: &mut session_dat,
user_dat: &mut user_dat,
};
let handler_opt = resolve_handler(&ctx, cmd);
match handler_opt {
None => {
trans.queue_for_session(session,
trans
.queue_for_session(
session,
Some(ansi!(
"That's not a command I know. Try <bold>help<reset>\r\n"
))
).await?;
)),
)
.await?;
trans.commit().await?;
}
Some(handler) => {
match handler.handle(&mut ctx, cmd, params).await {
Some(handler) => match handler.handle(&mut ctx, cmd, params).await {
Ok(()) => {
trans.commit().await?;
}
Err(UserError(err_msg)) => {
pool.queue_for_session(session, Some(&(err_msg + "\r\n"))).await?;
}
Err(SystemError(e)) => Err(e)?
}
pool.queue_for_session(session, Some(&(err_msg + "\r\n")))
.await?;
}
Err(SystemError(e)) => Err(e)?,
},
}
pool.bump_session_time(&session).await?;
Ok(())
}
#[cfg(test)]
pub async fn handle(_session: &ListenerSession, _msg: &str, _pool: &DBPool) -> DResult<()> {
unimplemented!();
}
pub fn is_likely_explicit(msg: &str) -> bool {
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> =
OnceCell::new();
let markers = EXPLICIT_MARKER_WORDS.get_or_init(||
vec!("fuck", "sex", "cock", "cunt", "dick", "pussy", "whore",
"orgasm", "erection", "nipple", "boob", "tit"));
static EXPLICIT_MARKER_WORDS: OnceCell<Vec<&'static str>> = OnceCell::new();
let markers = EXPLICIT_MARKER_WORDS.get_or_init(|| {
vec![
"fuck", "sex", "cock", "cunt", "dick", "pussy", "whore", "orgasm", "erection",
"nipple", "boob", "tit",
]
});
for word in markers {
if msg.contains(word) {
return true
return true;
}
}
false
}
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()))
}
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()))
}
pub async fn get_player_item_or_fail(ctx: &VerbContext<'_>) -> UResult<Arc<Item>> {
Ok(ctx.trans.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()))?)
Ok(ctx
.trans
.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>) ->
UResult<Arc<Item>> {
Ok(match &ctx.trans.resolve_items_by_display_name_for_player(search).await?[..] {
pub async fn search_item_for_user<'l>(
ctx: &'l VerbContext<'l>,
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())?,
[match_it] => match_it.clone(),
[item1, ..] =>
item1.clone(),
})
[item1, ..] => item1.clone(),
},
)
}
pub async fn search_items_for_user<'l>(ctx: &'l VerbContext<'l>, search: &'l ItemSearchParams<'l>) ->
UResult<Vec<Arc<Item>>> {
Ok(match &ctx.trans.resolve_items_by_display_name_for_player(search).await?[..] {
pub async fn search_items_for_user<'l>(
ctx: &'l VerbContext<'l>,
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())?,
v => v.into_iter().map(|it| it.clone()).collect(),
})
},
)
}
#[cfg(test)] mod test {
#[cfg(test)]
mod test {
use crate::db::MockDBTrans;
#[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 mut user_dat: Option<User> = None;
let mut session_dat: Session = Default::default();
let ctx = VerbContext { session: &sess,
let ctx = VerbContext {
session: &sess,
trans: &trans,
session_dat: &mut session_dat,
user_dat: &mut user_dat };
assert_eq!(resolve_handler(&ctx, "less_explicit_mode").is_some(),
true);
user_dat: &mut user_dat,
};
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 async_trait::async_trait;
use ansi::ansi;
use async_trait::async_trait;
use chrono::Utc;
pub struct Verb;
@ -40,33 +40,39 @@ static REQUIRED_AGREEMENTS: [&str;4] = [
fn user_mut<'a>(ctx: &'a mut VerbContext) -> UResult<&'a mut User> {
match ctx.user_dat.as_mut() {
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> {
match ctx.user_dat.as_ref() {
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)>> {
let existing_terms = &terms(ctx)?.accepted_terms;
for agreement in REQUIRED_AGREEMENTS {
let shortcode =
base64::encode(ring::digest::digest(&ring::digest::SHA256,
agreement.as_bytes()))[0..20].to_owned();
let shortcode = base64::encode(ring::digest::digest(
&ring::digest::SHA256,
agreement.as_bytes(),
))[0..20]
.to_owned();
match existing_terms.get(&shortcode) {
None => { return Ok(Some((agreement.to_owned(), shortcode))); }
None => {
return Ok(Some((agreement.to_owned(), shortcode)));
}
Some(_) => {}
}
}
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)? {
None => {
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)?;
user.terms.terms_complete = false;
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\
\t{}\r\n\
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"),
text))).await?;
unfortunately can't play, so type <red><bold>quit<reset> to log off.\r\n"
),
text
)),
)
.await?;
Ok(false)
}
}
@ -91,7 +105,12 @@ pub async fn check_and_notify_accepts<'a, 'b>(ctx: &'a mut VerbContext<'b>) -> U
#[async_trait]
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)?;
match user.terms.last_presented_term.as_ref() {
None => {
@ -99,16 +118,26 @@ impl UserVerb for Verb {
user_error("There was nothing pending your agreement.".to_owned())?;
}
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);
if check_and_notify_accepts(ctx).await? {
ctx.trans.queue_for_session(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
.queue_for_session(
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(())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,42 +1,47 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error,
get_player_item_or_fail, search_item_for_user};
use async_trait::async_trait;
use ansi::ansi;
use crate::{
services::{
combat::start_attack,
check_consent,
},
models::{
consent::ConsentType,
item::ItemFlag,
},
db::ItemSearchParams,
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
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;
#[async_trait]
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?;
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())?;
}
let attack_whom = search_item_for_user(ctx, &ItemSearchParams {
let attack_whom = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, remaining)
}).await?;
},
)
.await?;
let (loctype, loccode) = match player_item.location.split_once("/") {
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? {
None => user_error("Your current location is invalid!".to_owned())?,
Some(l) => l
Some(l) => l,
};
if player_loc.flags.contains(&ItemFlag::NoSeeContents) {
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() {
"npc" => {}
"player" => {},
_ => user_error("Only characters (players / NPCs) can be attacked".to_string())?
"player" => {}
_ => 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())?
}
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())?
}

View File

@ -1,44 +1,54 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError,
get_player_item_or_fail, user_error, search_item_for_user,
cut::ensure_has_butcher_tool,
cut::ensure_has_butcher_tool, get_player_item_or_fail, search_item_for_user, user_error,
UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
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 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;
pub struct Verb;
#[async_trait]
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?;
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,
dead_first: true,
..ItemSearchParams::base(&player_item, remaining.trim())
}).await?;
},
)
.await?;
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))?,
Some(DeathData { parts_remaining, ..}) =>
parts_remaining
}.clone();
None => user_error(format!(
"You can't do that while {} is still alive!",
possible_corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining,
}
.clone();
let corpse = if possible_corpse.item_type == "corpse" {
possible_corpse
@ -48,7 +58,8 @@ impl UserVerb for Verb {
"room/valhalla"
} else {
"room/repro_xv_respawn"
}.to_owned();
}
.to_owned();
Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?)
} else {
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?;
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()))?;
queue_command(ctx, &QueueCommand::Cut { from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned() }).await?;
queue_command(
ctx,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
}
Ok(())
}

View File

@ -1,33 +1,43 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, user_error,
get_player_item_or_fail,
parsing::parse_offset,
get_player_item_or_fail, parsing::parse_offset, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{
static_content::room,
static_content::possession_type::possession_data,
models::item::Item,
services::capacity::{check_item_capacity, CapacityLevel},
static_content::possession_type::possession_data,
static_content::room,
};
use async_trait::async_trait;
use ansi::ansi;
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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?;
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" {
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) {
None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r
Some(r) => r,
};
if room.stock_list.is_empty() {
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 {
if let Some(possession_type) = possession_data().get(&stock.possession_type) {
if possession_type.display.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 possession_type
.display
.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 let Some(mut user) = ctx.user_dat.as_mut() {
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;
let player_item_str = format!("player/{}", &player_item.item_code);
let item_code = ctx.trans.alloc_item_code().await?;
let loc = match check_item_capacity(ctx.trans, &player_item_str,
possession_type.weight).await? {
let loc = match check_item_capacity(
ctx.trans,
&player_item_str,
possession_type.weight,
)
.await?
{
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
match check_item_capacity(ctx.trans, &player_item.location,
possession_type.weight).await? {
CapacityLevel::AboveItemLimit =>
user_error(
match check_item_capacity(
ctx.trans,
&player_item.location,
possession_type.weight,
)
.await?
{
CapacityLevel::AboveItemLimit => user_error(
"You can't carry it, and there is too much stuff \
here already".to_owned())?,
_ => &player_item.location
here already"
.to_owned(),
)?,
_ => &player_item.location,
}
}
_ => &player_item_str
_ => &player_item_str,
};
let new_item = Item {
item_code: format!("{}", item_code),
@ -74,10 +107,15 @@ impl UserVerb for Verb {
..stock.possession_type.clone().into()
};
ctx.trans.create_item(&new_item).await?;
ctx.trans.queue_for_session(
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!("Your wristpad beeps for a deduction of {} credits.\n", stock.list_price))
).await?;
Some(&format!(
"Your wristpad beeps for a deduction of {} credits.\n",
stock.list_price
)),
)
.await?;
}
return Ok(());
} 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())
}

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,179 +1,267 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
get_player_item_or_fail, user_error, search_item_for_user};
use async_trait::async_trait;
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::{
item::{Item, DeathData, SkillType},
},
db::ItemSearchParams,
static_content::possession_type::{possession_data, can_butcher_possessions},
language::join_words,
models::item::{DeathData, Item, SkillType},
regular_tasks::queued_command::{queue_command, QueueCommand, QueueCommandHandler},
services::{
capacity::{check_item_capacity, CapacityLevel},
combat::corpsify_item,
comms::broadcast_to_room,
destroy_container,
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 std::{time, sync::Arc};
use async_trait::async_trait;
use mockall_double::double;
#[double] use crate::db::DBTrans;
use std::{sync::Arc, time};
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<time::Duration> {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
QueueCommand::Cut { from_corpse, what_part } => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?
QueueCommand::Cut {
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())?,
Some(it) => it
Some(it) => it,
};
if corpse.location != player_item.location {
user_error(
format!("You try to cut {} but realise it is no longer there.",
user_error(format!(
"You try to cut {} but realise it is no longer there.",
corpse.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
)
)?
))?
}
ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
match corpse.death_data.as_ref() {
None => user_error(format!("You can't do that while {} is still alive!", corpse.pronouns.subject))?,
Some(DeathData { parts_remaining, ..}) =>
if !parts_remaining.iter().any(
|pt| possession_data().get(pt)
None => user_error(format!(
"You can't do that while {} is still alive!",
corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => {
if !parts_remaining.iter().any(|pt| {
possession_data()
.get(pt)
.map(|pd| &pd.display == &what_part)
== 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))
.map(|pd| pd.display).collect::<Vec<&'static str>>())
== 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))
.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),
&what_part,
&corpse.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} prepares to cut {} from {}\n",
&corpse.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} prepares to cut {} from {}\n",
&player_item.display_for_sentence(false, 1, true),
&what_part,
&corpse.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
&corpse.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<()> {
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
QueueCommand::Cut { from_corpse, what_part } => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?
QueueCommand::Cut {
from_corpse,
what_part,
} => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?,
};
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())?,
Some(it) => it
Some(it) => it,
};
if corpse.location != player_item.location {
user_error(
format!("You try to cut {} but realise it is no longer there.",
user_error(format!(
"You try to cut {} but realise it is no longer there.",
corpse.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
)
)?
))?
}
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))?,
Some(DeathData { parts_remaining, ..}) =>
parts_remaining.iter().find(
|pt| possession_data().get(pt)
None => user_error(format!(
"You can't do that while {} is still alive!",
corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining
.iter()
.find(|pt| {
possession_data()
.get(pt)
.map(|pd| &pd.display == &what_part)
== 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>>())
)))?
== 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>>()
)
))
})?,
};
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()))?;
let mut corpse_mut = (*corpse).clone();
match corpse_mut.death_data.as_mut() {
None => {},
None => {}
Some(dd) => {
dd.parts_remaining =
dd
dd.parts_remaining = dd
.parts_remaining
.iter().take_while(|pt| pt != &possession_type)
.chain(dd.parts_remaining.iter().skip_while(|pt| pt != &possession_type).skip(1))
.iter()
.take_while(|pt| pt != &possession_type)
.chain(
dd.parts_remaining
.iter()
.skip_while(|pt| pt != &possession_type)
.skip(1),
)
.map(|pt| (*pt).clone())
.collect()
}
}
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())?,
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())?
}
_ => {}
}
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?;
} else {
ctx.trans.save_item_model(&corpse_mut).await?;
}
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 {
broadcast_to_room(&ctx.trans, &player_item.location, None,
&format!("{} tries to cut the {} from {}, but only leaves a mutilated mess.\n",
if skill_check_and_grind(&ctx.trans, &mut player_item_mut, &SkillType::Craft, 10.0).await?
< 0.0
{
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),
possession_data.display,
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),
possession_data.display,
corpse.display_for_sentence(true, 1, false)
))
).await?;
)),
)
.await?;
} else {
let mut new_item: Item = (*possession_type).clone().into();
new_item.item_code = format!("{}", ctx.trans.alloc_item_code().await?);
new_item.location = player_item.refstr();
ctx.trans.save_item_model(&new_item).await?;
broadcast_to_room(&ctx.trans, &player_item.location, None,
&format!("{} expertly cuts the {} from {}.\n",
broadcast_to_room(
&ctx.trans,
&player_item.location,
None,
&format!(
"{} expertly cuts the {} from {}.\n",
&player_item.display_for_sentence(true, 1, true),
possession_data.display,
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),
possession_data.display,
corpse.display_for_sentence(true, 1, false)
))
).await?;
)),
)
.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<()> {
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())?;
}
Ok(())
@ -192,38 +284,67 @@ pub async fn ensure_has_butcher_tool(trans: &DBTrans, player_item: &Item) -> URe
pub struct Verb;
#[async_trait]
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 ") {
None => user_error(ansi!("Usage: <bold>cut<reset> thing <bold>from<reset> corpse").to_owned())?,
Some(v) => v
None => user_error(
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?;
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,
dead_first: true,
..ItemSearchParams::base(&player_item, corpse_raw.trim())
}).await?;
},
)
.await?;
let what_norm = what_raw.trim().to_lowercase();
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))?,
Some(DeathData { parts_remaining, ..}) =>
parts_remaining.iter().find(
|pt| possession_data().get(pt)
.map(|pd| pd.display.to_lowercase() == what_norm ||
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();
None => user_error(format!(
"You can't do that while {} is still alive!",
possible_corpse.pronouns.subject
))?,
Some(DeathData {
parts_remaining, ..
}) => parts_remaining
.iter()
.find(|pt| {
possession_data().get(pt).map(|pd| {
pd.display.to_lowercase() == what_norm
|| 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" {
possible_corpse
@ -233,19 +354,27 @@ impl UserVerb for Verb {
"room/valhalla"
} else {
"room/repro_xv_respawn"
}.to_owned();
}
.to_owned();
Arc::new(corpsify_item(&ctx.trans, &possible_corpse).await?)
} else {
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()))?;
ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
queue_command(ctx, &QueueCommand::Cut { from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned() }).await?;
queue_command(
ctx,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
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::{
get_player_item_or_fail, parsing::parse_to_space, user_error, UResult, UserVerb, UserVerbRef,
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 async_trait::async_trait;
pub struct Verb;
#[async_trait]
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 (as_word, remaining) = parse_to_space(remaining);
let remaining = ignore_special_characters(remaining.trim());
@ -22,17 +22,28 @@ impl UserVerb for Verb {
}
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 {
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();
item.details = Some(remaining);
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(())
}

View File

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

View File

@ -1,29 +1,15 @@
use super::{
VerbContext,
UserVerb,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_items_for_user,
parsing::parse_count
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
};
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,
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 std::time;
@ -31,80 +17,133 @@ use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<time::Duration> {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
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())?,
Some(it) => it
Some(it) => it,
};
if item.location != player_item.location {
user_error(
format!("You try to get {} but realise it is no longer there",
user_error(format!(
"You try to get {} but realise it is no longer there",
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),
&item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} fumbles around trying to pick up {}\n",
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} fumbles around trying to pick up {}\n",
&player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<()> {
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
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())?,
Some(it) => it
Some(it) => it,
};
if item.location != player_item.location {
user_error(format!("You try to get {} but realise it is no longer there",
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))?
user_error(format!(
"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)) {
None => user_error("That item no longer exists in the game so can't be handled".to_owned())?,
Some(pd) => pd
let possession_data = match item
.possession_type
.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);
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!(
"{} 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)
))?,
_ => ()
_ => (),
}
let msg_exp = format!("{} picks up {}\n",
let msg_exp = format!(
"{} picks up {}\n",
&player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} picks up {}\n",
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} picks up {}\n",
&player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone();
item_mut.location = player_as_loc;
item_mut.action_type = LocationActionType::Normal;
@ -116,7 +155,12 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb;
#[async_trait]
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?;
// TODO: Parse "get target from container" variant
let mut get_limit = Some(1);
@ -127,23 +171,38 @@ impl UserVerb for Verb {
get_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(ctx, &ItemSearchParams {
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
..ItemSearchParams::base(&player_item, &remaining)
}).await?;
},
)
.await?;
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;
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" {
user_error("You can't get that!".to_owned())?;
}
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 {
user_error("I didn't find anything matching.".to_owned())?;

View File

@ -1,9 +1,6 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult,
CommandHandlingError::UserError
};
use async_trait::async_trait;
use super::{CommandHandlingError::UserError, UResult, UserVerb, UserVerbRef, VerbContext};
use ansi::ansi;
use async_trait::async_trait;
use phf::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>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>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 \
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 \
@ -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."),
"uninstall" =>
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! {
@ -269,11 +281,16 @@ static EXPLICIT_HELP_PAGES: phf::Map<&'static str, &'static str> = phf_map! {
pub struct Verb;
#[async_trait]
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 is_unregistered = match ctx.user_dat {
None => true,
Some(user_dat) => !user_dat.terms.terms_complete
Some(user_dat) => !user_dat.terms.terms_complete,
};
let remaining = remaining.trim();
if is_unregistered {
@ -285,11 +302,10 @@ impl UserVerb for Verb {
}
}
help = help.or_else(|| ALWAYS_HELP_PAGES.get(remaining));
let help_final = help.ok_or(
UserError("No help available on that".to_string()))?;
ctx.trans.queue_for_session(ctx.session,
Some(&(help_final.to_string() + "\n"))
).await?;
let help_final = help.ok_or(UserError("No help available on that".to_string()))?;
ctx.trans
.queue_for_session(ctx.session, Some(&(help_final.to_string() + "\n")))
.await?;
Ok(())
}
}

View File

@ -1,10 +1,15 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult};
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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(())
}
}

View File

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

View File

@ -1,14 +1,19 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult
};
use super::{UResult, UserVerb, UserVerbRef, VerbContext};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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.trans.save_session_model(ctx.session, ctx.session_dat).await?;
ctx.trans
.save_session_model(ctx.session, ctx.session_dat)
.await?;
Ok(())
}
}

View File

@ -1,50 +1,69 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error,
get_player_item_or_fail};
use crate::{
static_content::room,
static_content::possession_type::possession_data,
language
};
use async_trait::async_trait;
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::{language, static_content::possession_type::possession_data, static_content::room};
use ansi::ansi;
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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?;
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" {
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) {
None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r
Some(r) => r,
};
if room.stock_list.is_empty() {
user_error("Can't list stock because you're not in a shop.".to_owned())?
}
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!("Price")));
ansi!("Price")
));
for stock in &room.stock_list {
if let Some(possession_type) = possession_data().get(&stock.possession_type) {
let display = if ctx.session_dat.less_explicit_mode {
possession_type.display_less_explicit.as_ref().unwrap_or(&possession_type.display)
} else { &possession_type.display };
msg.push_str(&format!("| {:20} | {:15.2} |\n",
&language::caps_first(&display), &stock.list_price))
possession_type
.display_less_explicit
.as_ref()
.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"));
ctx.trans.queue_for_session(&ctx.session, Some(&msg)).await?;
msg.push_str(ansi!(
"\nUse <bold>buy<reset> item to purchase something.\n"
));
ctx.trans
.queue_for_session(&ctx.session, Some(&msg))
.await?;
Ok(())
}
}

View File

@ -1,30 +1,59 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error};
use super::look;
use super::{user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use async_trait::async_trait;
use tokio::time;
pub struct Verb;
#[async_trait]
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>>()[..] {
[] | [_] => user_error("Too few options to login".to_owned())?,
[username, password] => (username, password),
_ => 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())?,
Some(user) => {
time::sleep(time::Duration::from_secs(5)).await;
if !bcrypt::verify(password, &user.password_hash)? {
user_error("Invalid password.".to_owned())?
}
let username_exact = user.username.clone();
*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?;
if let Some(user) = ctx.user_dat {
ctx.trans.save_user_model(user).await?;

View File

@ -1,40 +1,42 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError, user_error,
get_player_item_or_fail, search_item_for_user,
get_player_item_or_fail,
map::{render_map, render_map_dyn},
open::{is_door_in_direction, DoorSituation},
search_item_for_user, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use async_trait::async_trait;
use ansi::{ansi, flow_around, word_wrap};
#[double]
use crate::db::DBTrans;
use crate::{
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,
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack},
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 std::sync::Arc;
use std::collections::BTreeSet;
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<()> {
let mut contents_desc = String::new();
let mut items = ctx.trans.find_items_by_location(&format!("{}/{}",
item.item_type, item.item_code)).await?;
items.sort_unstable_by(|it1, it2| (&it1.action_type).cmp(&it2.action_type)
.then((&it1.display).cmp(&it2.display)));
let mut items = ctx
.trans
.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code))
.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
.iter()
@ -53,8 +55,11 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
let mut phrases = Vec::<String>::new();
for group_items in all_groups {
let head = &group_items[0];
let mut details = head.display_for_sentence(!ctx.session_dat.less_explicit_mode,
group_items.len(), false);
let mut details = head.display_for_sentence(
!ctx.session_dat.less_explicit_mode,
group_items.len(),
false,
);
match head.action_type {
LocationActionType::Wielded => details.push_str(" (wielded)"),
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"));
}
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 {
let mut any_part_text = false;
let mut seen_clothes: BTreeSet<String> = BTreeSet::new();
for part in species_info_map().get(&item.species).map(|s| s.body_parts.clone())
.unwrap_or_else(|| vec!()) {
if let Some((top_item, covering_parts)) = items.iter()
.filter_map(
|it|
for part in species_info_map()
.get(&item.species)
.map(|s| s.body_parts.clone())
.unwrap_or_else(|| vec![])
{
if let Some((top_item, covering_parts)) = items
.iter()
.filter_map(|it| {
if it.action_type != LocationActionType::Worn {
None
} else {
it.possession_type.as_ref()
it.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.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()))
} else {
None
}
})
}
})
.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;
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 {}. ",
&item.pronouns.possessive,
&language::join_words(
&covering_parts.iter().map(|p| p.display(None))
.collect::<Vec<&'static str>>()),
&covering_parts
.iter()
.map(|p| p.display(None))
.collect::<Vec<&'static str>>()
),
&display
));
}
} else {
if !ctx.session_dat.less_explicit_mode {
any_part_text = true;
contents_desc.push_str(&format!("{} {} {} completely bare. ",
contents_desc.push_str(&format!(
"{} {} {} completely bare. ",
&language::caps_first(&item.pronouns.possessive),
part.display(item.sex.clone()),
part.copula(item.sex.clone())));
part.copula(item.sex.clone())
));
}
}
}
if any_part_text {
contents_desc.push_str("\n");
}
} else if item.species == SpeciesType::Human && !ctx.session_dat.less_explicit_mode {
contents_desc.push_str(&format!("{} is completely naked.\n",
&language::caps_first(&item.pronouns.possessive)));
} else if (item.item_type == "npc" || item.item_type == "player")
&& item.species == SpeciesType::Human
&& !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);
@ -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);
if item.item_type == "player" || item.item_type == "npc" {
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 {
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 {
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 {
contents_desc.push_str(&format!("{} looks seriously injured.\n",
&language::caps_first(
&item.pronouns.subject)));
contents_desc.push_str(&format!(
"{} looks seriously injured.\n",
&language::caps_first(&item.pronouns.subject)
));
} else {
contents_desc.push_str(&format!("{} looks like {}'s on death's door.\n",
&language::caps_first(
&item.pronouns.subject),
&item.pronouns.possessive));
contents_desc.push_str(&format!(
"{} looks like {}'s on death's door.\n",
&language::caps_first(&item.pronouns.subject),
&item.pronouns.possessive
));
}
if ctx.trans.check_task_by_type_code("DelayedHealth",
&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))
);
if ctx
.trans
.check_task_by_type_code(
"DelayedHealth",
&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" {
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 {
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 {
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 {
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 {
contents_desc.push_str(&format!("{}'s nearly completely destroyed.\n",
&language::caps_first(&item.pronouns.subject)));
contents_desc.push_str(&format!(
"{}'s nearly completely destroyed.\n",
&language::caps_first(&item.pronouns.subject)
));
}
}
}
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(|pd| pd.charge_data.as_ref()) {
.and_then(|pd| pd.charge_data.as_ref())
{
let unit = if item.charges == 1 {
charge_data.charge_name_prefix.to_owned() + " " +
charge_data.charge_name_suffix
charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_suffix
} else {
language::pluralise(charge_data.charge_name_prefix) + " " +
charge_data.charge_name_suffix
language::pluralise(charge_data.charge_name_prefix)
+ " "
+ charge_data.charge_name_suffix
};
contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit));
}
}
ctx.trans.queue_for_session(
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!("{}\n{}\n{}",
Some(&format!(
"{}\n{}\n{}",
&item.display_for_session(&ctx.session_dat),
item.details_for_session(&ctx.session_dat).unwrap_or(""),
contents_desc,
))
).await?;
)),
)
.await?;
Ok(())
}
fn exits_for(room: &room::Room) -> String {
let exit_text: Vec<String> =
room.exits.iter().map(|ex| format!("{}{}",
let exit_text: Vec<String> = room
.exits
.iter()
.map(|ex| {
format!(
"{}{}",
if ex.exit_climb.is_some() {
ansi!("<red>^")
} else {
ansi!("<yellow>")
},
ex.direction.describe())).collect();
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
ex.direction.describe()
)
})
.collect();
format!(
ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"),
exit_text.join(" ")
)
}
fn exits_for_dyn(dynroom: &dynzone::Dynroom) -> String {
let exit_text: Vec<String> =
dynroom.exits.iter().map(|ex| format!(ansi!("<yellow>{}"),
ex.direction.describe())).collect();
format!(ansi!("<cyan>[ Exits: <bold>{} <reset><cyan>]<reset>"), exit_text.join(" "))
let exit_text: Vec<String> = dynroom
.exits
.iter()
.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,
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(
pub async fn describe_room(
ctx: &VerbContext<'_>,
item: &Item,
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,
Some(&flow_around(&render_map(room, 5, 5), 10, ansi!("<reset> "),
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
Some(&flow_around(
&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),
zone,
item.details_for_session(
&ctx.session_dat).unwrap_or(""),
contents, exits_for(room)),
|row| if row >= 5 { 80 } else { 68 }), 68))
).await?;
item.details_for_session(&ctx.session_dat).unwrap_or(""),
contents,
exits_for(room)
),
|row| if row >= 5 { 80 } else { 68 },
),
68,
)),
)
.await?;
Ok(())
}
pub async fn describe_dynroom(ctx: &VerbContext<'_>,
pub async fn describe_dynroom(
ctx: &VerbContext<'_>,
item: &Item,
dynzone: &dynzone::Dynzone,
dynroom: &dynzone::Dynroom,
contents: &str) -> UResult<()> {
ctx.trans.queue_for_session(
contents: &str,
) -> UResult<()> {
ctx.trans
.queue_for_session(
ctx.session,
Some(&flow_around(&render_map_dyn(dynzone, dynroom, 5, 5), 10, ansi!("<reset> "),
&word_wrap(&format!(ansi!("<yellow>{}<reset> (<blue>{}<reset>)\n{}.{}\n{}\n"),
Some(&flow_around(
&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),
dynzone.zonename,
item.details_for_session(
&ctx.session_dat).unwrap_or(""),
contents, exits_for_dyn(dynroom)),
|row| if row >= 5 { 80 } else { 68 }), 68))
).await?;
item.details_for_session(&ctx.session_dat).unwrap_or(""),
contents,
exits_for_dyn(dynroom)
),
|row| if row >= 5 { 80 } else { 68 },
),
68,
)),
)
.await?;
Ok(())
}
@ -253,23 +361,22 @@ async fn describe_door(
state: &DoorState,
direction: &Direction,
) -> UResult<()> {
let mut msg = format!("That exit is blocked by {}.",
&state.description);
if let Some(lock) = ctx.trans.find_by_action_and_location(
let mut msg = format!("That exit is blocked by {}.", &state.description);
if let Some(lock) = ctx
.trans
.find_by_action_and_location(
&room_item.refstr(),
&LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await?.first()
&LocationActionType::InstalledOnDoorAsLock((*direction).clone()),
)
.await?
.first()
{
let lock_desc = lock.display_for_session(&ctx.session_dat);
msg.push_str(&format!(" The door is locked with {}",
&lock_desc
));
msg.push_str(&format!(" The door is locked with {}", &lock_desc));
}
msg.push('\n');
ctx.trans.queue_for_session(
ctx.session,
Some(&msg)).await?;
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
Ok(())
}
async fn list_room_contents<'l>(ctx: &'l VerbContext<'_>, item: &'l Item) -> UResult<String> {
@ -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());
}
let mut buf = String::new();
let mut items = ctx.trans.find_items_by_location(&format!("{}/{}",
item.item_type, item.item_code)).await?;
let mut items = ctx
.trans
.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code))
.await?;
items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display));
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 is_creature = head.item_type == "player" || head.item_type.starts_with("npc");
buf.push(' ');
buf.push_str(&head.display_for_sentence(!ctx.session_dat.less_explicit_mode,
group_items.len(), true));
buf.push_str(if group_items.len() > 1 { " are " } else { " is "});
buf.push_str(&head.display_for_sentence(
!ctx.session_dat.less_explicit_mode,
group_items.len(),
true,
));
buf.push_str(if group_items.len() > 1 {
" are "
} else {
" is "
});
match head.action_type {
LocationActionType::Sitting => buf.push_str("sitting "),
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::Grabbing => buf.push_str(", grabbing "),
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())
.or_else(|| head.presence_target.clone()) {
.or_else(|| head.presence_target.clone())
{
None => buf.push_str("someone"),
Some(who) => match who.split_once("/") {
None => buf.push_str("someone"),
Some((ttype, tcode)) =>
Some((ttype, tcode)) => {
match ctx.trans.find_item_by_type_code(ttype, tcode).await? {
None => buf.push_str("someone"),
Some(it) => buf.push_str(
&it.display_for_session(&ctx.session_dat)
)
Some(it) => buf.push_str(&it.display_for_session(&ctx.session_dat)),
}
}
},
}
}
buf.push('.');
@ -348,90 +466,152 @@ pub async fn direction_to_item(
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" {
let old_dynroom_item = match trans.find_item_by_type_code(heretype, herecode).await? {
None => user_error("Your current room has vanished!".to_owned())?,
Some(v) => v
Some(v) => v,
};
let (dynzone_code, dynroom_code) = match old_dynroom_item.special_data.as_ref() {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => (dynzone_code, dynroom_code),
_ => user_error("Your current room is invalid!".to_owned())?
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => (dynzone_code, dynroom_code),
_ => user_error("Your current room is invalid!".to_owned())?,
};
let dynzone = dynzone::dynzone_by_type()
.get(&dynzone::DynzoneType::from_str(dynzone_code)
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?)
.ok_or_else(|| UserError("The type of your current zone no longer exists".to_owned()))?;
let dynroom = dynzone.dyn_rooms.get(dynroom_code.as_str())
.get(
&dynzone::DynzoneType::from_str(dynzone_code).ok_or_else(|| {
UserError("The type of your current zone no longer exists".to_owned())
})?,
)
.ok_or_else(|| {
UserError("The type of your current zone no longer exists".to_owned())
})?;
let dynroom = dynzone
.dyn_rooms
.get(dynroom_code.as_str())
.ok_or_else(|| UserError("Your current room type no longer exists".to_owned()))?;
let exit = dynroom.exits.iter().find(|ex| ex.direction == *direction)
let exit = dynroom
.exits
.iter()
.find(|ex| ex.direction == *direction)
.ok_or_else(|| UserError("There is nothing in that direction".to_owned()))?;
return match exit.target {
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()))?;
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()))?;
let zone_exit = match zoneitem.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit: None, .. }) =>
user_error("That exit doesn't seem to go anywhere".to_owned())?,
Some(ItemSpecialData::DynzoneData { zone_exit: Some(zone_exit), .. }) => zone_exit,
_ => user_error("The zone you are in has invalid data associated with it".to_owned())?,
Some(ItemSpecialData::DynzoneData {
zone_exit: None, ..
}) => user_error("That exit doesn't seem to go anywhere".to_owned())?,
Some(ItemSpecialData::DynzoneData {
zone_exit: Some(zone_exit),
..
}) => zone_exit,
_ => user_error(
"The zone you are in has invalid data associated with it".to_owned(),
)?,
};
let (zone_exit_type, zone_exit_code) = zone_exit.split_once("/").ok_or_else(
|| 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?)
},
let (zone_exit_type, zone_exit_code) =
zone_exit.split_once("/").ok_or_else(|| {
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 } => {
let to_item = trans.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode).await?
.ok_or_else(|| UserError("Can't find the room in that direction.".to_owned()))?;
let to_item = trans
.find_item_by_location_dynroom_code(&old_dynroom_item.location, &subcode)
.await?
.ok_or_else(|| {
UserError("Can't find the room in that direction.".to_owned())
})?;
Ok(Some(Arc::new(to_item)))
}
}
};
}
if heretype != "room" {
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()))?;
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()))?;
let new_room =
room::resolve_exit(room, exit).ok_or_else(|| UserError("Can't find that room".to_owned()))?;
let new_room = room::resolve_exit(room, exit)
.ok_or_else(|| UserError("Can't find that room".to_owned()))?;
Ok(trans.find_item_by_type_code("room", new_room.code).await?)
}
pub struct Verb;
#[async_trait]
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 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
};
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 == "" {
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()))?
} else if let Some(dir) = Direction::parse(&rem_trim) {
match is_door_in_direction(&ctx.trans, &dir, use_location).await? {
DoorSituation::NoDoor |
DoorSituation::DoorOutOfRoom { state: DoorState { open: true, .. }, .. } |
DoorSituation::DoorIntoRoom { state: DoorState { open: true, .. }, .. } => {},
DoorSituation::DoorIntoRoom { state, room_with_door, .. } => {
DoorSituation::NoDoor
| DoorSituation::DoorOutOfRoom {
state: DoorState { open: true, .. },
..
}
| DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. },
..
} => {}
DoorSituation::DoorIntoRoom {
state,
room_with_door,
..
} => {
if let Some(rev_dir) = dir.reverse() {
return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
}
},
DoorSituation::DoorOutOfRoom { state, room_with_door, .. } => {
}
DoorSituation::DoorOutOfRoom {
state,
room_with_door,
..
} => {
return describe_door(ctx, &room_with_door, &state, &dir).await;
}
}
direction_to_item(&ctx.trans, use_location, &dir).await?
direction_to_item(&ctx.trans, use_location, &dir)
.await?
.ok_or_else(|| UserError("There's nothing in that direction".to_owned()))?
} else if rem_trim == "me" || rem_trim == "self" {
player_item.clone()
@ -443,27 +623,34 @@ impl UserVerb for Verb {
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &rem_trim)
}
).await?
},
)
.await?
};
if item.item_type == "room" {
let room =
room::room_map_by_code().get(item.item_code.as_str())
let room = room::room_map_by_code()
.get(item.item_code.as_str())
.ok_or_else(|| UserError("Sorry, that room no longer exists".to_owned()))?;
describe_room(ctx, &item, &room, &list_room_contents(ctx, &item).await?).await?;
} else if item.item_type == "dynroom" {
let (dynzone, dynroom) = match &item.special_data {
Some(ItemSpecialData::DynroomData { dynzone_code, dynroom_code }) => {
dynzone::DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dz_t|
dynzone::dynzone_by_type().get(&dz_t))
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => 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)))
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?
},
_ => user_error("Expected dynroom to have DynroomData".to_owned())?
.ok_or_else(|| UserError("Dynamic room doesn't exist anymore.".to_owned()))?,
_ => user_error("Expected dynroom to have DynroomData".to_owned())?,
};
describe_dynroom(ctx, &item, &dynzone, &dynroom,
&list_room_contents(ctx, &item).await?).await?;
describe_dynroom(
ctx,
&item,
&dynzone,
&dynroom,
&list_room_contents(ctx, &item).await?,
)
.await?;
} else {
describe_normal_item(ctx, &item).await?;
}

View File

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

View File

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

View File

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

View File

@ -1,19 +1,27 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
ItemSearchParams, user_error,
get_player_item_or_fail, is_likely_explicit,
search_item_for_user,
parsing::parse_to_space};
use super::{
get_player_item_or_fail, is_likely_explicit, parsing::parse_to_space, search_item_for_user,
user_error, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
};
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
pub struct Verb;
#[async_trait]
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 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())?,
Some(m) => (*m).clone()
Some(m) => (*m).clone(),
};
(last_page_from, remaining)
} else {
@ -28,22 +36,31 @@ impl UserVerb for Verb {
if player_item.death_data.is_some() {
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,
limit: 1,
..ItemSearchParams::base(&player_item, &to_whom_name)
}).await?;
},
)
.await?;
match to_whom.item_type.as_str() {
"player" => {},
_ => user_error("Only players accept pages".to_string())?
"player" => {}
_ => 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"),
to_whom.display_for_session(&ctx.session_dat),
say_what
))).await?;
)),
)
.await?;
if player_item == to_whom {
return Ok(());
@ -51,7 +68,11 @@ impl UserVerb for Verb {
match to_whom.item_type.as_str() {
"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())?,
Some((other_session, other_session_dets)) => {
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."
.to_owned())?
} 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());
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::{
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,
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},
sequence::terminated,
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) {
fn parse(input: &str) -> IResult<&str, &str> {
let (input, _) = space0(input)?;
let (input, cmd) = alt((
recognize(one_of("-\"':.")),
take_till1(|c| c == ' ' || c == '\t')
take_till1(|c| c == ' ' || c == '\t'),
))(input)?;
let (input, _) = space0(input)?;
Ok((input, cmd))
@ -24,7 +24,7 @@ pub fn parse_command_name(input: &str) -> (&str, &str) {
match parse(input) {
/* This parser only fails on empty / whitespace only strings. */
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) {
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) {
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) {
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 _";
fn parse_valid(input: &str) -> IResult<&str, (), VerboseError<&str>> {
let (input, l1) = context("Must start with a letter", alpha1)(input)?;
let (input, l2) = context(CATCHALL_ERROR,
take_while(|c: char| c.is_alphanumeric() || c == '_'))(input)?;
let (input, l2) = context(
CATCHALL_ERROR,
take_while(|c: char| c.is_alphanumeric() || c == '_'),
)(input)?;
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, ()))
}
match terminated(recognize(parse_valid), alt((space1, eof)))(input) {
Ok((input, username)) => Ok((username, input)),
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) =>
Err(e.errors.into_iter().find_map(|k| match k.1 {
Err(nom::Err::Error(e)) | Err(nom::Err::Failure(e)) => Err(e
.errors
.into_iter()
.find_map(|k| match k.1 {
VerboseErrorKind::Context(s) => Some(s),
_ => None
}).unwrap_or(CATCHALL_ERROR)),
Err(_) => Err(CATCHALL_ERROR)
_ => None,
})
.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> {
let (input, number) = match u16::<&'l str, ()>(input) {
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(" ") {
None => (input, ""),
Some(v) => v
Some(v) => v,
};
Ok((match tok.to_lowercase().as_str() {
"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.");
let (consent_type_s, input) = match input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v)
Some(v) => Ok(v),
}?;
let consent_type = match ConsentType::from_str(&consent_type_s.trim().to_lowercase()) {
None => Err(
if is_explicit { "Invalid consent type - options are fight, medicine, gifts, visit and sex" } else {
None => Err(if is_explicit {
"Invalid consent type - options are fight, medicine, gifts, visit and sex"
} else {
"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(" ") {
None => Err(usage),
Some(v) => Ok(v)
Some(v) => Ok(v),
}?;
let tok_trim = tok.trim_start().to_lowercase();
let consent_target =
if tok_trim == "against" {
let consent_target = if tok_trim == "against" {
if consent_type != ConsentType::Fight {
Err("corps can only currently consent to fight, no other actions")?
} else {
let (my_corp_raw, new_input) = match input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v)
Some(v) => Ok(v),
}?;
let my_corp = my_corp_raw.trim_start();
let (tok, new_input) = match new_input.trim_start().split_once(" ") {
None => Err(usage),
Some(v) => Ok(v)
Some(v) => Ok(v),
}?;
if tok.trim_start().to_lowercase() != "by" {
Err(usage)?;
}
let (target_corp_raw, new_input) = match new_input.trim_start().split_once(" ") {
None => (new_input.trim_start(), ""),
Some(v) => v
Some(v) => v,
};
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" {
let (target_user_raw, new_input) = match input.trim_start().split_once(" ") {
None => (input.trim_start(), ""),
Some(v) => v
Some(v) => v,
};
input = new_input;
ConsentTarget::UserTarget { to_user: target_user_raw.trim_start() }
ConsentTarget::UserTarget {
to_user: target_user_raw.trim_start(),
}
} else {
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(" ") {
None => (input, ""),
Some(v) => v
Some(v) => v,
};
match tok.to_lowercase().as_str() {
"for" => {
@ -180,7 +193,7 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"until" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v
Some(v) => v,
};
if tok.trim_start().to_lowercase() != "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" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v
Some(v) => v,
};
match tok.trim_start().to_lowercase().as_str() {
"private" => {
@ -210,7 +223,7 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"disallow" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v
Some(v) => v,
};
match tok.trim_start().to_lowercase().as_str() {
"private" => {
@ -226,17 +239,23 @@ pub fn parse_allow<'l>(input: &'l str, is_explicit: bool) -> Result<AllowCommand
"in" => {
let (tok, new_input) = match new_input.split_once(" ") {
None => (new_input, ""),
Some(v) => v
Some(v) => v,
};
consent_details.only_in.push(tok);
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 { consent_type: consent_type, consent_target: consent_target, consent_details: consent_details })
Ok(AllowCommand {
consent_type: consent_type,
consent_target: consent_target,
consent_details: consent_details,
})
}
#[cfg(test)]
@ -245,28 +264,29 @@ mod tests {
#[test]
fn it_parses_normal_command() {
assert_eq!(parse_command_name("help"),
("help", ""));
assert_eq!(parse_command_name("help"), ("help", ""));
}
#[test]
fn it_parses_normal_command_with_arg() {
assert_eq!(parse_command_name("help \t testing stuff"),
("help", "testing stuff"));
assert_eq!(
parse_command_name("help \t testing stuff"),
("help", "testing stuff")
);
}
#[test]
fn it_parses_commands_with_leading_whitespace() {
assert_eq!(parse_command_name(" \t \thelp \t testing stuff"),
("help", "testing stuff"));
assert_eq!(
parse_command_name(" \t \thelp \t testing stuff"),
("help", "testing stuff")
);
}
#[test]
fn it_parses_empty_command_names() {
assert_eq!(parse_command_name(""),
("", ""));
assert_eq!(parse_command_name(" \t "),
("", ""));
assert_eq!(parse_command_name(""), ("", ""));
assert_eq!(parse_command_name(" \t "), ("", ""));
}
#[test]
@ -276,7 +296,10 @@ mod tests {
#[test]
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]
@ -306,12 +329,18 @@ mod tests {
#[test]
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]
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]
@ -339,22 +368,31 @@ mod tests {
#[test]
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 {
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)
}))
})
)
}
#[test]
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 {
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)
}))
})
)
}
#[test]

View File

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

View File

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

View File

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

View File

@ -1,44 +1,44 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError,
user_error, get_player_item_or_fail, get_user_or_fail,
get_player_item_or_fail, get_user_or_fail, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
DResult,
models::{
item::{Item, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
static_content::{
room::{room_map_by_code, Direction},
dynzone::dynzone_by_type,
npc::npc_by_code,
room::{room_map_by_code, Direction},
},
models::{
task::{TaskDetails, TaskMeta, Task},
item::{Item, ItemSpecialData},
},
regular_tasks::{
TaskHandler,
TaskRunContext,
},
DResult,
};
use log::info;
use async_trait::async_trait;
use chrono::{Utc, Duration};
use std::time;
use ansi::ansi;
use async_recursion::async_recursion;
use async_trait::async_trait;
use chrono::{Duration, Utc};
use itertools::Itertools;
use log::info;
use std::time;
#[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();
match item.item_type.as_str() {
"npc" => {
let npc = match npc_by_code().get(item.item_code.as_str()) {
None => { return Ok(()) },
Some(r) => r
None => return Ok(()),
Some(r) => r,
};
item_mut.location = npc.spawn_location.to_owned();
ctx.trans.save_item_model(&item_mut).await?;
return Ok(());
},
}
"player" => {
let session = ctx.trans.find_session_for_player(&item.item_code).await?;
match session.as_ref() {
@ -47,17 +47,19 @@ async fn recursively_destroy_or_move_item(ctx: &mut TaskRunContext<'_>, item: &I
&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"))
).await?;
},
}
None => {}
}
item_mut.location = "room/melbs_homelessshelter".to_owned();
ctx.trans.save_item_model(&item_mut).await?;
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);
// It's paginated so we loop to get everything...
loop {
@ -78,47 +80,59 @@ pub struct ChargeRoomTaskHandler;
impl TaskHandler for ChargeRoomTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let (zone_item_ref, daily_price) = match &mut ctx.task.details {
TaskDetails::ChargeRoom { zone_item, daily_price } => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?
TaskDetails::ChargeRoom {
zone_item,
daily_price,
} => (zone_item, daily_price),
_ => Err("Expected ChargeRoom type")?,
};
let zone_item_code = match zone_item_ref.split_once("/") {
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 => {
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);
}
Some(it) => it
Some(it) => it,
};
let vacate_after = match zone_item.special_data {
Some(ItemSpecialData::DynzoneData { vacate_after, .. }) => vacate_after,
_ => Err("Expected ChargeRoom dynzone to have DynzoneData")?
_ => Err("Expected ChargeRoom dynzone to have DynzoneData")?,
};
match vacate_after {
Some(t) if t < Utc::now() => {
recursively_destroy_or_move_item(ctx, &zone_item).await?;
return Ok(None);
},
_ => ()
}
_ => (),
}
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" =>
player_item_code,
Some((player_item_type, player_item_code)) if player_item_type == "player" => {
player_item_code
}
_ => {
info!("Can't charge rent for dynzone {}, owner {:?} isn't chargeable", zone_item_code,
&zone_item.owner
info!(
"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? {
None => return Ok(None),
Some(user) => user
Some(user) => user,
};
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 {
vacate_after: Some(Utc::now() + Duration::days(1)),
zone_exit: match zone_item.special_data.as_ref() {
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) =>
zone_exit.clone(),
_ => None
}});
Some(ItemSpecialData::DynzoneData { zone_exit, .. }) => zone_exit.clone(),
_ => None,
},
});
ctx.trans.save_item_model(&zone_item_mut).await?;
match ctx.trans.find_item_by_type_code("dynroom",
&(zone_item.item_code.clone() + "/doorstep")).await? {
None => {},
match ctx
.trans
.find_item_by_type_code("dynroom", &(zone_item.item_code.clone() + "/doorstep"))
.await?
{
None => {}
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
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?;
}
@ -184,78 +201,116 @@ pub static CHARGE_ROOM_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &Char
pub struct Verb;
#[async_trait]
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 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()))?;
if loc_type != "room" {
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()))?;
if room.rentable_dynzone.is_empty() {
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) {
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 rentinfo = match room
.rentable_dynzone
.iter()
.find(|ri| ri.rent_what == item_name)
{
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)?;
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())?
}
let zone = dynzone_by_type().get(&rentinfo.dynzone)
.ok_or_else(|| UserError("That seems to no longer exist, so you can't rent it.".to_owned()))?;
let zone = dynzone_by_type().get(&rentinfo.dynzone).ok_or_else(|| {
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,
&Direction::IN { item: player_item.display.clone() })
&Direction::IN {
item: player_item.display.clone(),
},
)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
{
None => {},
None => {}
Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) =
ctx.trans.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await? {
if let Some(ex_zone) = ctx
.trans
.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData {
vacate_after: None, .. }) =>
user_error(
"You can only rent one apartment here, and you already have one!".to_owned())?,
vacate_after: None, ..
}) => user_error(
"You can only rent one apartment here, and you already have one!"
.to_owned(),
)?,
Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_), zone_exit: ref ex }) => {
vacate_after: Some(_),
zone_exit: ref ex,
}) => {
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans.save_item_model(
&Item {
special_data:
Some(ItemSpecialData::DynzoneData {
ctx.trans
.save_item_model(&Item {
special_data: Some(ItemSpecialData::DynzoneData {
vacate_after: None,
zone_exit: ex.clone()
}), ..(*ex_zone).clone() }
).await?;
zone_exit: ex.clone(),
}),
..(*ex_zone).clone()
})
.await?;
match ctx.trans.find_item_by_type_code("dynroom",
&(ex_zone.item_code.clone() + "/doorstep")).await? {
None => {},
match ctx
.trans
.find_item_by_type_code(
"dynroom",
&(ex_zone.item_code.clone() + "/doorstep"),
)
.await?
{
None => {}
Some(doorstep_room) => {
let mut doorstep_mut = (*doorstep_room).clone();
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.queue_for_session(
ctx.session,
Some(&format!(ansi!(
@ -263,20 +318,27 @@ impl UserVerb for Verb {
<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"), rentinfo.setup_fee))
).await?;
return Ok(());
},
}
_ => {}
}
}
}
}
let zonecode = zone.create_instance(
ctx.trans, &player_item.location,
let zonecode = zone
.create_instance(
ctx.trans,
&player_item.location,
"You can only rent one apartment here, and you already have one!",
&player_item, &Direction::IN { item: player_item.display.clone() }
).await?;
&player_item,
&Direction::IN {
item: player_item.display.clone(),
},
)
.await?;
ctx.trans.upsert_task(&Task {
ctx.trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("charge_rent/{}", &zonecode),
is_static: false,
@ -286,18 +348,23 @@ impl UserVerb for Verb {
},
details: TaskDetails::ChargeRoom {
zone_item: zonecode.clone(),
daily_price: rentinfo.daily_price
}
}).await?;
daily_price: rentinfo.daily_price,
},
})
.await?;
let mut user_mut = user.clone();
user_mut.credits -= rentinfo.setup_fee;
ctx.trans.save_user_model(&user_mut).await?;
ctx.trans.queue_for_session(
ctx.trans
.queue_for_session(
ctx.session,
Some(&format!(ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee))
).await?;
Some(&format!(
ansi!("<yellow>Your wristpad beeps for a deduction of ${}<reset>\n"),
rentinfo.setup_fee
)),
)
.await?;
Ok(())
}

View File

@ -1,29 +1,37 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
user_error,
get_player_item_or_fail, is_likely_explicit};
use super::{
get_player_item_or_fail, is_likely_explicit, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
#[double]
use crate::db::DBTrans;
use crate::{
models::item::{Item, ItemFlag},
services::comms::broadcast_to_room,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
use mockall_double::double;
pub async fn say_to_room<'l>(
trans: &DBTrans,
from_item: &Item,
location: &str,
say_what: &str,
is_explicit: bool
is_explicit: bool,
) -> 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()))?;
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()))?;
if room_item.flags.contains(&ItemFlag::NoSay) {
user_error("Your wristpad vibrates and flashes up an error - apparently it has \
been programmed to block your voice from working here.".to_owned())?
user_error(
"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!(
ansi!("<yellow>{} says: <reset><bold>\"{}\"<reset>\n"),
@ -41,15 +49,25 @@ pub async fn say_to_room<'l>(
location,
Some(from_item),
&msg_exp,
if is_explicit { None } else { Some(&msg_lessexp) }
).await?;
if is_explicit {
None
} else {
Some(&msg_lessexp)
},
)
.await?;
Ok(())
}
pub struct Verb;
#[async_trait]
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);
if say_what == "" {
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() {
user_error("Shush, the dead can't talk!".to_string())?;
}
say_to_room(ctx.trans, &player_item, &player_item.location,
&say_what, is_likely_explicit(&say_what)).await
say_to_room(
ctx.trans,
&player_item,
&player_item.location,
&say_what,
is_likely_explicit(&say_what),
)
.await
}
}
static VERB_INT: Verb = Verb;

View File

@ -1,28 +1,32 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
user_error, get_player_item_or_fail};
use crate::{
models::item::{StatType, SkillType}
};
use async_trait::async_trait;
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::models::item::{SkillType, StatType};
use ansi::ansi;
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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 user = match ctx.user_dat {
None => user_error("Log in first".to_owned())?,
Some(user) => user
Some(user) => user,
};
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"
));
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(),
user.raw_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(&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"
));
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(),
user.raw_skills.get(st).unwrap_or(&0.0),
player_item.total_skills.get(st).unwrap_or(&0.0)
));
}
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, \
{} spent since re-roll.\n"),
{} spent since re-roll.\n"
),
player_item.total_xp,
user.experience.xp_change_for_this_reroll,
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 crate::{
db::ItemSearchParams,
static_content::possession_type::possession_data,
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{db::ItemSearchParams, static_content::possession_type::possession_data};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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();
if item_name == "" {
user_error("Sign what? Try: sign something".to_owned())?;
}
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,
..ItemSearchParams::base(&player_item, item_name)
}).await?;
},
)
.await?;
if item.item_type != "possession" {
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(|pd| pd.sign_handler) {
.and_then(|pd| pd.sign_handler)
{
None => user_error("You can't sign that!".to_owned())?,
Some(h) => h
Some(h) => h,
};
handler.cmd(ctx, &player_item, &item).await?;

View File

@ -1,10 +1,7 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
user_error, get_player_item_or_fail};
use crate::{
services::combat::max_health,
};
use async_trait::async_trait;
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::services::combat::max_health;
use ansi::ansi;
use async_trait::async_trait;
fn bar_n_of_m(mut actual: u64, max: u64) -> String {
if actual > max {
@ -23,21 +20,31 @@ fn bar_n_of_m(mut actual: u64, max: u64) -> String {
pub struct Verb;
#[async_trait]
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 user = match ctx.user_dat {
None => user_error("Log in first".to_owned())?,
Some(user) => user
Some(user) => user,
};
let mut msg = String::new();
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),
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?;
Ok(())

View File

@ -1,64 +1,92 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
UserError,
user_error, get_player_item_or_fail, search_items_for_user};
use super::{
get_player_item_or_fail, search_items_for_user, user_error, UResult, UserError, UserVerb,
UserVerbRef, VerbContext,
};
use crate::{
db::ItemSearchParams,
static_content::{
possession_type::possession_data,
room::Direction,
},
models::item::ItemFlag,
static_content::{possession_type::possession_data, room::Direction},
};
use async_trait::async_trait;
use ansi::ansi;
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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 ") {
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
};
let player_item = get_player_item_or_fail(ctx).await?;
if player_item.death_data.is_some() {
user_error("Apparently, you have to be alive to work as an uninstaller. \
So discriminatory!".to_owned())?;
user_error(
"Apparently, you have to be alive to work as an uninstaller. \
So discriminatory!"
.to_owned(),
)?;
}
let (loc_t, loc_c) = player_item.location.split_once("/")
let (loc_t, loc_c) = player_item
.location
.split_once("/")
.ok_or_else(|| UserError("Invalid current location".to_owned()))?;
let loc_item = ctx.trans.find_item_by_type_code(loc_t, loc_c).await?
let loc_item = ctx
.trans
.find_item_by_type_code(loc_t, loc_c)
.await?
.ok_or_else(|| UserError("Can't find your location".to_owned()))?;
if loc_item.owner.as_ref() != Some(&player_item.refstr()) || !loc_item.flags.contains(&ItemFlag::PrivatePlace) {
user_error("You can only uninstall things while standing in a private room you own. \
if loc_item.owner.as_ref() != Some(&player_item.refstr())
|| !loc_item.flags.contains(&ItemFlag::PrivatePlace)
{
user_error(
"You can only uninstall things while standing in a private room you own. \
If you are outside, try uninstalling from the inside."
.to_owned())?;
.to_owned(),
)?;
}
let dir = Direction::parse(what_dir_raw.trim()).ok_or_else(
|| UserError("Invalid direction.".to_owned()))?;
let dir = Direction::parse(what_dir_raw.trim())
.ok_or_else(|| UserError("Invalid direction.".to_owned()))?;
let cand_items = search_items_for_user(ctx, &ItemSearchParams {
let cand_items = search_items_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
..ItemSearchParams::base(&player_item, uninstall_what_raw.trim())
}).await?;
let item = cand_items.iter().find(|it| it.action_type.is_in_direction(&dir))
.ok_or_else(
|| UserError(
"Sorry, I couldn't find anything matching installed on that door.".to_owned()))?;
},
)
.await?;
let item = cand_items
.iter()
.find(|it| it.action_type.is_in_direction(&dir))
.ok_or_else(|| {
UserError(
"Sorry, I couldn't find anything matching installed on that door.".to_owned(),
)
})?;
if item.item_type != "possession" {
user_error("You can't uninstall that!".to_owned())?;
}
let handler = match item.possession_type.as_ref()
let handler = match item
.possession_type
.as_ref()
.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())?,
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(())
}

View File

@ -1,33 +1,15 @@
use super::{
VerbContext,
UserVerb,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_item_for_user,
parsing,
get_player_item_or_fail, parsing, search_item_for_user, user_error, ItemSearchParams, UResult,
UserVerb, UserVerbRef, VerbContext,
};
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,
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 std::time;
@ -35,74 +17,115 @@ use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<time::Duration> {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
QueueCommand::Use { possession_id, target_id } => (possession_id, target_id),
_ => user_error("Unexpected command".to_owned())?
QueueCommand::Use {
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())?,
Some(it) => it
Some(it) => it,
};
if item.location != format!("player/{}", player_item.item_code) {
user_error(format!("You try to use {} but realise you no longer have it",
item.display_for_sentence(
!ctx.session_dat.less_explicit_mode,
1, false
)
user_error(format!(
"You try to use {} but realise you no longer have it",
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)
))?
}
let (target_type, target_code) = match target_type_code.split_once("/") {
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 target =
if is_self_use {
let target = if is_self_use {
player_item.clone()
} else {
match ctx.trans.find_item_by_type_code(&target_type, &target_code).await? {
None => user_error(format!("Couldn't handle use command (target {} missing)",
target_type_code))?,
Some(it) => it
match ctx
.trans
.find_item_by_type_code(&target_type, &target_code)
.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 &&
target.location != format!("player/{}", player_item.item_code) {
let target_name = target.display_for_sentence(!ctx.session_dat.less_explicit_mode,
1, false);
user_error(format!("You try to use {} on {}, but realise {} is no longer here",
item.display_for_sentence(
!ctx.session_dat.less_explicit_mode,
1, false),
target_name, target_name
if !is_self_use
&& target.location != player_item.location
&& target.location != format!("player/{}", player_item.item_code)
{
let target_name =
target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false);
user_error(format!(
"You try to use {} on {}, but realise {} is no longer here",
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.pronouns.possessive,
&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)
});
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.pronouns.possessive,
&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)
});
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 skill_result =
skill_check_and_grind(ctx.trans, &mut player_item_mut, &SkillType::Quickdraw, draw_level).await?;
let skill_result = skill_check_and_grind(
ctx.trans,
&mut player_item_mut,
&SkillType::Quickdraw,
draw_level,
)
.await?;
if skill_result < -0.5 {
draw_level -= 2.0;
} else if skill_result < -0.25 {
@ -115,72 +138,97 @@ impl QueueCommandHandler for QueueHandler {
ctx.trans.save_item_model(&player_item_mut).await?;
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)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<()> {
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
QueueCommand::Use { possession_id, target_id } => (possession_id, target_id),
_ => user_error("Unexpected command".to_owned())?
QueueCommand::Use {
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())?,
Some(it) => it
Some(it) => it,
};
if item.location != format!("player/{}", player_item.item_code) {
user_error(format!("You try to use {} but realise you no longer have it",
item.display_for_sentence(!ctx.session_dat.less_explicit_mode,
1, false)
user_error(format!(
"You try to use {} but realise you no longer have it",
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("/") {
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())?,
Some(it) => it
Some(it) => it,
};
if target.location != player_item.location &&
target.location != format!("player/{}", player_item.item_code) {
let target_name = target.display_for_sentence(!ctx.session_dat.less_explicit_mode,
1, false);
user_error(format!("You try to use {} on {}, but realise {} is no longer here",
item.display_for_sentence(
!ctx.session_dat.less_explicit_mode,
1, false),
target_name, target_name
if target.location != player_item.location
&& target.location != format!("player/{}", player_item.item_code)
{
let target_name =
target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false);
user_error(format!(
"You try to use {} on {}, but realise {} is no longer here",
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false),
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_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())?,
Some(d) => d
Some(d) => d,
};
if let Some(consent_type) = use_data.needs_consent_check.as_ref() {
if !check_consent(ctx.trans, "use", consent_type, &player_item, &target).await? {
user_error(format!("{} doesn't allow {} from you",
&target.display_for_sentence(!ctx.session_dat.less_explicit_mode,
1, true),
consent_type.to_str()))?
user_error(format!(
"{} doesn't allow {} from you",
&target.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true),
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_data| poss_data.charge_data.as_ref()) {
.and_then(|poss_data| poss_data.charge_data.as_ref())
{
if item.charges < 1 {
user_error(
format!("{} has no {} {} left",
item.display_for_sentence(!ctx.session_dat.less_explicit_mode,
1, true),
user_error(format!(
"{} has no {} {} left",
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true),
&language::pluralise(charge_data.charge_name_prefix),
charge_data.charge_name_suffix
))?;
@ -189,22 +237,32 @@ impl QueueCommandHandler for QueueHandler {
if let Some(err) = (use_data.errorf)(&item, &target) {
user_error(err)?;
}
if ctx.trans.check_task_by_type_code(
if ctx
.trans
.check_task_by_type_code(
"DelayedHealth",
&format!("{}/{}/{}", &target.item_type, &target.item_code,
use_data.task_ref)
).await? {
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)
&format!(
"{}/{}/{}",
&target.item_type, &target.item_code, use_data.task_ref
),
)
.await?
{
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 mut player_mut = (*player_item).clone();
let skillcheck = skill_check_and_grind(&ctx.trans, &mut player_mut,
&use_data.uses_skill, use_data.diff_level).await?;
let skillcheck = skill_check_and_grind(
&ctx.trans,
&mut player_mut,
&use_data.uses_skill,
use_data.diff_level,
)
.await?;
let (effects, skilllvl) = if skillcheck <= -0.5 {
// 0-1 how bad was the crit fail?
(&use_data.crit_fail_effects, (-0.5 - skillcheck) * 2.0)
@ -214,9 +272,21 @@ impl QueueCommandHandler for QueueHandler {
(&use_data.success_effects, skillcheck)
};
let mut target_mut = if is_self_use { None } else { Some((*target).clone()) };
run_effects(ctx.trans, &effects, &mut player_mut, &item, &mut target_mut, skilllvl,
use_data.task_ref).await?;
let mut target_mut = if is_self_use {
None
} 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 {
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 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_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;
save_item = true;
}
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(|poss_data| poss_data.becomes_on_spent.as_ref())
.and_then(|poss_type| possession_data().get(&poss_type)
.map(|poss_dat| (poss_type, poss_dat)))
.and_then(|poss_type| {
possession_data()
.get(&poss_type)
.map(|poss_dat| (poss_type, poss_dat))
})
{
item_mut.possession_type = Some(new_poss.clone());
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_less_explicit = 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.details_less_explicit =
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.weight = new_poss_dat.weight;
save_item = true;
@ -258,42 +343,67 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb;
#[async_trait]
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?;
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 item = search_item_for_user(ctx, &ItemSearchParams {
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: 1,
..ItemSearchParams::base(&player_item, &what_name)
}).await?;
},
)
.await?;
let target = if whom_name == "me" || whom_name == "self" { player_item.clone() } else {
search_item_for_user(ctx, &ItemSearchParams {
let target = if whom_name == "me" || whom_name == "self" {
player_item.clone()
} else {
search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: 1,
..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_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())?,
Some(d) => d
Some(d) => d,
};
if let Some(err) = (use_data.errorf)(&item, &target) {
user_error(err)?;
}
queue_command(ctx, &QueueCommand::Use {
queue_command(
ctx,
&QueueCommand::Use {
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(())
}
}

View File

@ -1,75 +1,95 @@
use super::{
VerbContext, UserVerb, UserVerbRef, UResult, UserError,
user_error, get_player_item_or_fail,
get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
use crate::{
static_content::{
room::{room_map_by_code, Direction},
},
models::{
item::{Item, ItemSpecialData},
},
models::item::{Item, ItemSpecialData},
static_content::room::{room_map_by_code, Direction},
};
use chrono::{Utc, Duration};
use async_trait::async_trait;
use chrono::{Duration, Utc};
use itertools::Itertools;
pub struct Verb;
#[async_trait]
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 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()))?;
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()))?;
if room.rentable_dynzone.is_empty() {
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) {
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 room
.rentable_dynzone
.iter()
.find(|ri| ri.rent_what == item_name)
{
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,
&Direction::IN { item: player_item.display.clone() })
&Direction::IN {
item: player_item.display.clone(),
},
)
.await?
.as_ref()
.and_then(|it| it.location.split_once("/"))
{
None => {
user_error("You aren't renting anything from here!".to_owned())?
},
None => user_error("You aren't renting anything from here!".to_owned())?,
Some((ref ex_zone_t, ref ex_zone_c)) => {
if let Some(ex_zone) =
ctx.trans.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await? {
if let Some(ex_zone) = ctx
.trans
.find_item_by_type_code(ex_zone_t, ex_zone_c)
.await?
{
match ex_zone.special_data {
Some(ItemSpecialData::DynzoneData {
vacate_after: Some(_), .. }) => {
user_error("Your lease is already up for termination.".to_owned())?
},
vacate_after: Some(_),
..
}) => user_error("Your lease is already up for termination.".to_owned())?,
Some(ItemSpecialData::DynzoneData {
vacate_after: None, zone_exit: ref ex }) => {
ctx.trans.save_item_model(
&Item {
vacate_after: None,
zone_exit: ref ex,
}) => {
ctx.trans
.save_item_model(&Item {
special_data: Some(ItemSpecialData::DynzoneData {
zone_exit: ex.clone(),
vacate_after: Some(Utc::now() + Duration::days(1))
vacate_after: Some(Utc::now() + Duration::days(1)),
}),
..(*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?
},
_ => 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::{
VerbContext,
UserVerb,
UserVerbRef,
UResult,
ItemSearchParams,
UserError,
user_error,
get_player_item_or_fail,
search_items_for_user,
parsing::parse_count
get_player_item_or_fail, parsing::parse_count, search_items_for_user, user_error,
ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
};
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,
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 chrono::Utc;
@ -36,99 +15,141 @@ use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<time::Duration> {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
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())?,
Some(it) => it
Some(it) => it,
};
if item.location != player_item.refstr() {
user_error(
format!("You try to wear {} but realise you no longer have it",
user_error(format!(
"You try to wear {} 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 {
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))
.ok_or_else(|| UserError(
"That item no longer exists in the game so can't be handled".to_owned()))?;
.ok_or_else(|| {
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(
|| UserError("You can't wear that!".to_owned()))?;
poss_data
.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),
&item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} fumbles around trying to put on {}\n",
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} fumbles around trying to put on {}\n",
&player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
Ok(time::Duration::from_secs(1))
}
#[allow(unreachable_patterns)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<()> {
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
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())?,
Some(it) => it
Some(it) => it,
};
if item.location != player_item.refstr() {
user_error(format!("You try to wear {} but realise it is no longer there.",
&item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))?
user_error(format!(
"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 {
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))
.ok_or_else(|| UserError(
"That item no longer exists in the game so can't be handled".to_owned()))?;
.ok_or_else(|| {
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(
|| UserError("You can't wear that!".to_owned()))?;
let wear_data = poss_data
.wear_data
.as_ref()
.ok_or_else(|| UserError("You can't wear that!".to_owned()))?;
let other_clothes =
ctx.trans.find_by_action_and_location(
&player_item.refstr(), &LocationActionType::Worn).await?;
let other_clothes = ctx
.trans
.find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn)
.await?;
for part in &wear_data.covers_parts {
let thickness: f64 =
other_clothes.iter().fold(
wear_data.thickness,
|tot, other_item|
match other_item.possession_type.as_ref()
other_clothes
.iter()
.fold(wear_data.thickness, |tot, other_item| {
match other_item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.wear_data.as_ref())
{
Some(wd) if wd.covers_parts.contains(&part) =>
tot + wd.thickness,
Some(wd) if wd.covers_parts.contains(&part) => tot + wd.thickness,
_ => tot,
}
);
});
if thickness > 12.0 {
user_error(format!(
"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),
&item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} wears {}\n",
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} wears {}\n",
&player_item.display_for_sentence(false, 1, true),
&item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
&item.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&player_item.location,
None,
&msg_exp,
Some(&msg_nonexp),
)
.await?;
let mut item_mut = (*item).clone();
item_mut.action_type = LocationActionType::Worn;
item_mut.action_type_started = Some(Utc::now());
@ -156,10 +188,10 @@ impl QueueCommandHandler for QueueHandler {
item_type: item_mut.item_type.clone(),
item_code: item_mut.item_code.clone(),
},
impacts: vec!(BuffImpact::ChangeSkill {
impacts: vec![BuffImpact::ChangeSkill {
skill: SkillType::Dodge,
magnitude: -wear_data.dodge_penalty
})
magnitude: -wear_data.dodge_penalty,
}],
});
if let Some(ref usr) = ctx.user_dat {
calculate_total_stats_skills_for_user(&mut player_item_mut, usr);
@ -175,7 +207,12 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb;
#[async_trait]
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 mut get_limit = Some(1);
if remaining == "all" || remaining.starts_with("all ") {
@ -185,24 +222,37 @@ impl UserVerb for Verb {
get_limit = Some(n);
remaining = remaining2;
}
let targets = search_items_for_user(ctx, &ItemSearchParams {
let targets = search_items_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
limit: get_limit.unwrap_or(100),
item_action_type_only: Some(&LocationActionType::Normal),
..ItemSearchParams::base(&player_item, &remaining)
}).await?;
},
)
.await?;
if player_item.death_data.is_some() {
user_error("The dead don't dress themselves".to_owned())?;
}
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" {
user_error("You can't wear that!".to_owned())?;
}
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 {
user_error("I didn't find anything matching.".to_owned())?;

View File

@ -1,16 +1,20 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult,
ItemSearchParams, user_error,
get_player_item_or_fail, is_likely_explicit,
search_item_for_user,
parsing::parse_to_space};
use super::{
get_player_item_or_fail, is_likely_explicit, parsing::parse_to_space, search_item_for_user,
user_error, ItemSearchParams, UResult, UserVerb, UserVerbRef, VerbContext,
};
use crate::static_content::npc::npc_by_code;
use ansi::{ansi, ignore_special_characters};
use async_trait::async_trait;
use ansi::{ignore_special_characters, ansi};
pub struct Verb;
#[async_trait]
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 say_what = ignore_special_characters(say_what_raw);
if say_what == "" {
@ -20,24 +24,33 @@ impl UserVerb for Verb {
if player_item.death_data.is_some() {
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,
limit: 1,
..ItemSearchParams::base(&player_item, &to_whom_name)
}).await?;
},
)
.await?;
match to_whom.item_type.as_str() {
"npc" => {}
"player" => {},
_ => user_error("Only characters (players / NPCs) accept whispers".to_string())?
"player" => {}
_ => 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"),
player_item.display_for_session(&ctx.session_dat),
to_whom.display_for_session(&ctx.session_dat),
say_what
))).await?;
)),
)
.await?;
if player_item == to_whom {
return Ok(());
@ -45,15 +58,22 @@ impl UserVerb for Verb {
match to_whom.item_type.as_str() {
"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)
.unwrap_or_else(|| user_error("That NPC is no longer available".to_owned()))?;
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" => {
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())?,
Some((other_session, other_session_dets)) => {
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."
.to_owned())?
} 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"),
player_item.display_for_session(&ctx.session_dat),
to_whom.display_for_session(&ctx.session_dat),
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 ansi::{ignore_special_characters, ansi};
use chrono::Utc;
pub struct Verb;
#[async_trait]
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();
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!("Corp"),
ansi!("Idle")));
ansi!("Idle")
));
for online in ctx.trans.get_online_info().await? {
if let Some(online_time) = online.time {
let diff =
humantime::format_duration(
std::time::Duration::from_secs(
(Utc::now() - online_time).num_seconds() as u64));
let diff = humantime::format_duration(std::time::Duration::from_secs(
(Utc::now() - online_time).num_seconds() as u64,
));
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())),
&format!("{}", &diff)));
&format!("{}", &diff)
));
}
}
msg.push_str("\n");

View File

@ -1,28 +1,12 @@
use super::{
VerbContext,
UserVerb,
UserVerbRef,
UResult,
ItemSearchParams,
user_error,
get_player_item_or_fail,
search_item_for_user,
get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult, UserVerb,
UserVerbRef, VerbContext,
};
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,
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 std::time;
@ -30,37 +14,66 @@ use std::time;
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<time::Duration> {
async fn start_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<time::Duration> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
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())?,
Some(it) => it
Some(it) => it,
};
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())?
}
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.pronouns.possessive,
&item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} fumbles around with {} {}\n",
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} fumbles around with {} {}\n",
&player_item.display_for_sentence(false, 1, true),
&player_item.pronouns.possessive,
&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);
&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);
let mut player_item_mut = (*player_item).clone();
let skill_result =
skill_check_and_grind(ctx.trans, &mut player_item_mut, &SkillType::Quickdraw, draw_level).await?;
let skill_result = skill_check_and_grind(
ctx.trans,
&mut player_item_mut,
&SkillType::Quickdraw,
draw_level,
)
.await?;
if skill_result < -0.5 {
draw_level -= 2.0;
} else if skill_result < -0.25 {
@ -73,37 +86,63 @@ impl QueueCommandHandler for QueueHandler {
ctx.trans.save_item_model(&player_item_mut).await?;
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)]
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
-> UResult<()> {
async fn finish_command(
&self,
ctx: &mut VerbContext<'_>,
command: &QueueCommand,
) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?;
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 {
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())?,
Some(it) => it
Some(it) => it,
};
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())?
}
let msg_exp = format!("{} wields {}\n",
let msg_exp = format!(
"{} wields {}\n",
&player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} wields {}\n",
&item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} wields {}\n",
&player_item.display_for_sentence(false, 1, true),
&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,
&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,
&LocationActionType::Wielded,
&LocationActionType::Normal).await?;
&LocationActionType::Normal,
)
.await?;
Ok(())
}
}
@ -111,27 +150,47 @@ impl QueueCommandHandler for QueueHandler {
pub struct Verb;
#[async_trait]
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 weapon = search_item_for_user(ctx, &ItemSearchParams {
let weapon = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &remaining)
}).await?;
},
)
.await?;
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 {
user_error("You're actually already wielding it.".to_owned())?;
}
if weapon.item_type != "possession" ||
weapon.possession_type.as_ref()
if weapon.item_type != "possession"
|| weapon
.possession_type
.as_ref()
.and_then(|poss_type| possession_data().get(&poss_type))
.and_then(|poss_data| poss_data.weapon_data.as_ref())
.is_none() {
.is_none()
{
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(())
}
}

View File

@ -1,34 +1,48 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, user_error, get_player_item_or_fail, search_item_for_user};
use crate::{
db::ItemSearchParams,
static_content::possession_type::possession_data,
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{db::ItemSearchParams, static_content::possession_type::possession_data};
use async_trait::async_trait;
pub struct Verb;
#[async_trait]
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 ") {
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 item = search_item_for_user(ctx, &ItemSearchParams {
let item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
..ItemSearchParams::base(&player_item, on_what_raw.trim())
}).await?;
},
)
.await?;
if item.item_type != "possession" {
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(|pd| pd.write_handler) {
.and_then(|pd| pd.write_handler)
{
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(())
}

View File

@ -1,13 +1,13 @@
use serde::{Serialize, Deserialize};
use serde_json::Value;
use chrono::{DateTime, Utc};
use crate::services::effect::DelayedHealthEffect;
use std::collections::VecDeque;
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)]
pub enum TaskRecurrence {
FixedDuration { seconds: u32 }
FixedDuration { seconds: u32 },
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
@ -16,7 +16,7 @@ pub enum TaskDetails {
RunQueuedCommand,
NPCSay {
npc_code: String,
say_code: String
say_code: String,
},
NPCWander {
npc_code: String,
@ -26,26 +26,29 @@ pub enum TaskDetails {
},
AttackTick,
RecloneNPC {
npc_code: String
npc_code: String,
},
RotCorpse {
corpse_code: String
corpse_code: String,
},
DelayedHealth {
item: String,
effect_series: VecDeque<DelayedHealthEffect>
effect_series: VecDeque<DelayedHealthEffect>,
},
ExpireItem {
item_code: String
item_code: String,
},
ChargeRoom {
zone_item: String,
daily_price: u64
daily_price: u64,
},
SwingShut {
room_item: String,
direction: Direction
}
direction: Direction,
},
DestroyUser {
username: String,
},
}
impl TaskDetails {
pub fn name(self: &Self) -> &'static str {
@ -62,6 +65,7 @@ impl TaskDetails {
ExpireItem { .. } => "ExpireItem",
ChargeRoom { .. } => "ChargeRoom",
SwingShut { .. } => "SwingShut",
DestroyUser { .. } => "DestroyUser",
// 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,
recurrence: None,
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)]
pub meta: TaskMeta,
pub task_type: String,
pub task_details: Value
pub task_details: Value,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
#[serde(untagged)]
pub enum TaskParse {
Known(Task),
Unknown(TaskOther)
Unknown(TaskOther),
}

View File

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

View File

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

View File

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

View File

@ -1,81 +1,99 @@
#[double]
use crate::db::DBTrans;
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::{
comms::broadcast_to_room,
skills::{
skill_check_and_grind,
skill_check_only,
calculate_total_stats_skills_for_user,
},
destroy_container,
},
models::{
item::{Item, LocationActionType, Subattack, SkillType, DeathData},
task::{Task, TaskMeta, TaskDetails},
journal::JournalType,
skills::{calculate_total_stats_skills_for_user, skill_check_and_grind, skill_check_only},
},
static_content::{
possession_type::{WeaponData, WeaponAttackData, DamageType, possession_data, fist},
journals::{award_journal_if_needed, check_journal_for_kill},
npc::npc_by_code,
species::species_info_map,
journals::{check_journal_for_kill, award_journal_if_needed}
possession_type::{fist, possession_data, DamageType, WeaponAttackData, WeaponData},
species::{species_info_map, BodyPart},
},
message_handler::user_commands::{user_error, UResult, CommandHandlingError},
regular_tasks::{TaskRunContext, TaskHandler},
DResult,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
use ansi::ansi;
use async_recursion::async_recursion;
use async_trait::async_trait;
use chrono::Utc;
use async_recursion::async_recursion;
use std::time;
use ansi::ansi;
use mockall_double::double;
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> {
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>()));
async fn soak_damage(
ctx: &mut TaskRunContext<'_>,
attack: &WeaponAttackData,
victim: &Item,
presoak_amount: 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> =
ctx.trans.find_by_action_and_location(&victim.refstr(),
&LocationActionType::Worn).await?
.iter().map(|cl| (*cl.as_ref()).clone()).collect();
let mut clothes: Vec<Item> = ctx
.trans
.find_by_action_and_location(&victim.refstr(), &LocationActionType::Worn)
.await?
.iter()
.map(|cl| (*cl.as_ref()).clone())
.collect();
clothes.sort_unstable_by(|c1, c2| c2.action_type_started.cmp(&c1.action_type_started));
let mut total_damage = 0.0;
for (damage_type, mut damage_amount) in &damage_by_type {
for mut clothing in &mut clothes {
if let Some(soak) = clothing.possession_type
if let Some(soak) = clothing
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.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 {
break;
}
let soak_amount: f64 = ((soak.max_soak - soak.min_soak) *
rand::thread_rng().gen::<f64>()).min(damage_amount);
let soak_amount: f64 = ((soak.max_soak - soak.min_soak)
* rand::thread_rng().gen::<f64>())
.min(damage_amount);
damage_amount -= soak_amount;
let clothes_damage = ((0..(soak_amount as i64))
.filter(|_| rand::thread_rng().gen::<f64>() <
soak.damage_probability_per_soak).count()
as u64).min(clothing.health);
.filter(|_| rand::thread_rng().gen::<f64>() < soak.damage_probability_per_soak)
.count() as u64)
.min(clothing.health);
if clothes_damage > 0 {
clothing.health -= clothes_damage;
if victim.item_type == "player" {
if let Some((vic_sess, sess_dat)) =
ctx.trans.find_session_for_player(&victim.item_code).await?
{
ctx.trans.queue_for_session(
ctx.trans
.queue_for_session(
&vic_sess,
Some(&format!("A few bits and pieces fly off your {}.\n",
clothing.display_for_session(&sess_dat)))
).await?;
Some(&format!(
"A few bits and pieces fly off your {}.\n",
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 {
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 let Some((vic_sess, sess_dat)) =
ctx.trans.find_session_for_player(&victim.item_code).await?
{
ctx.trans.queue_for_session(
ctx.trans
.queue_for_session(
&vic_sess,
Some(&format!("Your {} is completely destroyed; it crumbles away to nothing.\n",
clothing.display_for_session(&sess_dat)))
).await?;
Some(&format!(
"Your {} is completely destroyed; it crumbles away to nothing.\n",
clothing.display_for_session(&sess_dat)
)),
)
.await?;
}
}
}
@ -104,21 +128,39 @@ async fn soak_damage(ctx: &mut TaskRunContext<'_>, attack: &WeaponAttackData, vi
Ok(total_damage)
}
async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item, 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);
async fn process_attack(
ctx: &mut TaskRunContext<'_>,
attacker_item: &mut Item,
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,
attack_skill).await?;
let dodge_result =
skill_check_and_grind(ctx.trans, victim_item, &SkillType::Dodge, attack_skill).await?;
let user_opt = if attacker_item.item_type == "player" {
ctx.trans.find_by_username(&attacker_item.item_code).await?
} else { None };
} else {
None
};
let attack_result = if let Some(user) = user_opt {
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 {
skill_check_and_grind(ctx.trans, attacker_item, &weapon.uses_skill,
victim_dodge_skill).await?
skill_check_and_grind(
ctx.trans,
attacker_item,
&weapon.uses_skill,
victim_dodge_skill,
)
.await?
} else {
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 {
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),
attacker_item.display_for_sentence(true, 1, false));
let msg_nonexp = format!("{} dodges out of the way of {}'s attack.\n",
attacker_item.display_for_sentence(true, 1, false)
);
let msg_nonexp = format!(
"{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(false, 1, true),
attacker_item.display_for_sentence(false, 1, false));
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
attacker_item.display_for_sentence(false, 1, false)
);
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(&victim_item).await?;
} else {
@ -146,21 +199,30 @@ async fn process_attack(ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item,
let mut mean_damage: f64 = attack.mean_damage;
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 {
mean_damage += (skill - scaling.min_skill) * scaling.mean_damage_per_point_over_min;
}
}
let actual_damage_presoak = Normal::new(mean_damage,
attack.stdev_damage)?
.sample(&mut rand::thread_rng()).floor().max(1.0) as i64;
let actual_damage_presoak = Normal::new(mean_damage, attack.stdev_damage)?
.sample(&mut rand::thread_rng())
.floor()
.max(1.0) as i64;
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 msg_exp = attack.success_message(&attacker_item, victim_item,
&part, true);
let msg_nonexp = attack.success_message(&attacker_item, victim_item,
&part, false);
let actual_damage = soak_damage(
ctx,
&attack,
victim_item,
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 {
let msg_exp = format!(
"{}'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),
&part.display(victim_item.sex.clone())
);
let msg_nonexp =
format!(
let msg_nonexp = format!(
"{}'s attack bounces off {}'s {}.\n",
attacker_item.display_for_sentence(false, 1, true),
victim_item.display_for_sentence(false, 1, false),
&part.display(None)
);
broadcast_to_room(&ctx.trans, &victim_item.location,
None, &msg_exp, Some(&msg_nonexp)).await?;
} else if change_health(ctx.trans, -actual_damage, victim_item,
&msg_exp, &msg_nonexp).await? {
broadcast_to_room(
&ctx.trans,
&victim_item.location,
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?;
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_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)
}
@ -196,30 +277,50 @@ pub struct AttackTaskHandler;
#[async_trait]
impl TaskHandler for AttackTaskHandler {
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")?;
let mut attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? {
None => { return Ok(None); } // Player is gone
Some(item) => (*item).clone()
None => {
return Ok(None);
} // Player is gone
Some(item) => (*item).clone(),
};
let (vtype, vcode) =
match attacker_item.active_combat.as_ref().and_then(|ac| ac.attacking.as_ref()).and_then(|v| v.split_once("/")) {
let (vtype, vcode) = match attacker_item
.active_combat
.as_ref()
.and_then(|ac| ac.attacking.as_ref())
.and_then(|v| v.split_once("/"))
{
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? {
None => { return Ok(None); }
Some(item) => (*item).clone()
None => {
return Ok(None);
}
Some(item) => (*item).clone(),
};
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?;
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)
} else {
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,
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 new_health = ((victim.health as i64 + change).max(0) as u64).min(maxh);
if change >= 0 && new_health == victim.health {
return Ok(false);
}
let colour = if change > 0 { ansi!("<green>") } else { ansi!("<red>") };
let msg_exp = format!(ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
let colour = if change > 0 {
ansi!("<green>")
} else {
ansi!("<red>")
};
let msg_exp = format!(
ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
colour,
change,
new_health,
max_health(&victim),
reason_exp);
let msg_nonexp =
format!(ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
colour,
change,
new_health,
maxh,
reason_nonexp);
reason_exp
);
let msg_nonexp = format!(
ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
colour, change, new_health, maxh, reason_nonexp
);
broadcast_to_room(trans, &victim.location, None, &msg_exp, Some(&msg_nonexp)).await?;
victim.health = new_health;
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" {
return Ok(());
}
let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? {
None => return Ok(()),
Some(r) => r
Some(r) => r,
};
let mut user = match trans.find_by_username(&by_item.item_code).await? {
None => return Ok(()),
Some(r) => r
Some(r) => r,
};
let xp_gain = if by_item.total_xp >= for_item.total_xp {
0
} else {
let xp_gain =
(((for_item.total_xp - by_item.total_xp) as f64 * 10.0 / (by_item.total_xp + 1) as f64) as u64)
let xp_gain = (((for_item.total_xp - by_item.total_xp) as f64 * 10.0
/ (by_item.total_xp + 1) as f64) as u64)
.min(100);
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(bonus) = &npc.kill_bonus {
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?;
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 {
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(())
@ -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?;
whom.death_data = Some(DeathData {
parts_remaining: species_info_map().get(&whom.species)
.map(|sp| sp.corpse_butchers_into.clone()).unwrap_or_else(|| vec!()),
parts_remaining: species_info_map()
.get(&whom.species)
.map(|sp| sp.corpse_butchers_into.clone())
.unwrap_or_else(|| vec![]),
..Default::default()
});
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 {
trans.upsert_task(&Task {
trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: whom.item_code.clone(),
next_scheduled: Utc::now() + chrono::Duration::seconds(120),
..Default::default()
},
details: TaskDetails::RecloneNPC {
npc_code: whom.item_code.clone()
}
}).await?;
npc_code: whom.item_code.clone(),
},
})
.await?;
} else if whom.item_type == "player" {
trans.revoke_until_death_consent(&whom.item_code).await?;
match trans.find_by_username(&whom.item_code).await? {
None => {},
None => {}
Some(mut user) => {
if award_journal_if_needed(trans, &mut user, whom, JournalType::Died).await? {
trans.save_user_model(&user).await?;
}
}
}
}
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 (session, _) = match trans.find_session_for_player(&player.item_code).await? {
None => return Ok(false),
Some(r) => r
Some(r) => r,
};
let mut user = match trans.find_by_username(&player.item_code).await? {
None => return Ok(false),
Some(r) => r
Some(r) => r,
};
trans.queue_for_session(&session,
Some(&format!("You lost {} experience points by dying.\n", lost_xp))).await?;
trans
.queue_for_session(
&session,
Some(&format!(
"You lost {} experience points by dying.\n",
lost_xp
)),
)
.await?;
player.total_xp -= lost_xp;
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);
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 {
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)
.unwrap_or(24)
} else if whom.item_type == "player" {
(22.0 + (whom.total_xp as f64).log(1.4)).min(60.0) as u64
} 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)
.unwrap_or(10)
} else {
@ -415,14 +557,18 @@ pub fn max_health(whom: &Item) -> u64 {
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,
auto_refocus: bool) ->
DResult<()>
{
trans.delete_task("AttackTick",
&format!("{}/{}",
new_by_whom.item_type,
new_by_whom.item_code)).await?;
pub async fn stop_attacking_mut(
trans: &DBTrans,
new_by_whom: &mut Item,
new_to_whom: &mut Item,
auto_refocus: bool,
) -> DResult<()> {
trans
.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() {
let old_attacker = format!("{}/{}", new_by_whom.item_type, new_by_whom.item_code);
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;
if auto_refocus {
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((vtype, vcode)) = new_vic.split_once("/") {
if let Some(vic_item) = trans.find_item_by_type_code(vtype, vcode).await? {
let mut new_vic_item = (*vic_item).clone();
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),
}
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(())
}
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_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> {
if let Some(item) = trans.find_by_action_and_location(
&who.refstr(), &LocationActionType::Wielded).await?.first() {
if let Some(dat) = item.possession_type.as_ref()
if let Some(item) = trans
.find_by_action_and_location(&who.refstr(), &LocationActionType::Wielded)
.await?
.first()
{
if let Some(dat) = item
.possession_type
.as_ref()
.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);
}
}
// TODO: Search inventory for wielded item first.
if who.item_type == "npc" {
if let Some(intrinsic) = npc_by_code().get(who.item_code.as_str())
.and_then(|npc| npc.intrinsic_weapon.as_ref()) {
if let Some(weapon) = possession_data().get(intrinsic).and_then(|p| p.weapon_data.as_ref()) {
return Ok(weapon)
if let Some(intrinsic) = npc_by_code()
.get(who.item_code.as_str())
.and_then(|npc| npc.intrinsic_weapon.as_ref())
{
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]
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_nonexp = String::new();
let mut verb: String = "attacks".to_string();
match by_whom.action_type {
LocationActionType::Sitting | LocationActionType::Reclining => {
msg_exp.push_str(&format!(ansi!("{} stands up.\n"), &by_whom.display));
msg_nonexp.push_str(&format!(ansi!("{} stands up.\n"),
by_whom.display_less_explicit.as_ref().unwrap_or(&by_whom.display)));
},
msg_nonexp.push_str(&format!(
ansi!("{} stands up.\n"),
by_whom
.display_less_explicit
.as_ref()
.unwrap_or(&by_whom.display)
));
}
LocationActionType::Attacking(_) => {
match by_whom.active_combat.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))?,
match by_whom
.active_combat
.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)) => {
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?;
}
}
_ => {}
}
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"),
&by_whom.display_for_sentence(true, 1, true),
verb,
&to_whom.display_for_sentence(true, 1, false))
);
&to_whom.display_for_sentence(true, 1, false)
));
msg_nonexp.push_str(&format!(
ansi!("<red>{} {} {}.<reset>\n"),
&by_whom.display_for_sentence(false, 1, true),
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?;
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"));
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 =
Some(format!("{}/{}",
&to_whom.item_type, &to_whom.item_code));
by_whom
.active_combat
.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);
to_whom.active_combat.get_or_insert_with(|| Default::default()).attacked_by.push(
format!("{}/{}",
&by_whom.item_type, &by_whom.item_code)
);
to_whom
.active_combat
.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 {
task_code: format!("{}/{}", by_whom.item_type, by_whom.item_code),
next_scheduled: Utc::now() + chrono::Duration::milliseconds(
attack_speed(by_whom).as_millis() as i64),
next_scheduled: Utc::now()
+ chrono::Duration::milliseconds(attack_speed(by_whom).as_millis() as i64),
..Default::default()
},
details: TaskDetails::AttackTick
}).await?;
details: TaskDetails::AttackTick,
})
.await?;
// 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?;
}
@ -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.is_static = false;
trans.save_item_model(&new_item).await?;
trans.upsert_task(&Task {
trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: new_item.item_code.clone(),
next_scheduled: Utc::now() + chrono::Duration::minutes(5),
..Default::default()
},
details: TaskDetails::RotCorpse { corpse_code: new_item.item_code.clone() }
}).await?;
details: TaskDetails::RotCorpse {
corpse_code: new_item.item_code.clone(),
},
})
.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>> {
let npc_code = match &ctx.task.details {
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? {
None => { return Ok(None) },
Some(r) => (*r).clone()
None => return Ok(None),
Some(r) => (*r).clone(),
};
let npc = match npc_by_code().get(npc_code.as_str()) {
None => { return Ok(None) },
Some(r) => r
None => return Ok(None),
Some(r) => r,
};
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>> {
let corpse_code = match &ctx.task.details {
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? {
None => { return Ok(None) }
Some(r) => r
let corpse = match ctx
.trans
.find_item_by_type_code("corpse", &corpse_code)
.await?
{
None => return Ok(None),
Some(r) => r,
};
destroy_container(ctx.trans, &corpse).await?;
let msg_exp = format!("{} rots away to nothing.\n",
corpse.display_for_sentence(true, 1, true));
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?;
let msg_exp = format!(
"{} rots away to nothing.\n",
corpse.display_for_sentence(true, 1, true)
);
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)
}
}

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_static ON items ((cast(details->>'is_static' as boolean)));
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 UNIQUE INDEX item_dynamic_entrance ON items (
(details->'dynamic_entrance'->>'source_item'),