blastmud/blastmud_game/src/message_handler/user_commands/cut.rs

386 lines
14 KiB
Rust

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::{
db::ItemSearchParams,
language::join_words,
models::item::{DeathData, Item, SkillType},
regular_tasks::queued_command::{
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
},
services::{
capacity::{check_item_capacity, CapacityLevel},
combat::corpsify_item,
comms::broadcast_to_room,
destroy_container,
skills::skill_check_and_grind,
urges::change_stress_considering_cool,
},
static_content::possession_type::{can_butcher_possessions, possession_data},
};
use ansi::ansi;
use async_trait::async_trait;
use mockall_double::double;
use std::{sync::Arc, time};
pub struct QueueHandler;
#[async_trait]
impl QueueCommandHandler for QueueHandler {
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
if ctx.item.death_data.is_some() {
user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?;
}
if ctx.item.urges.as_ref().map(|u| u.stress.value).unwrap_or(0) > 7000 {
user_error(
ansi!(
"You are too tired and stressed to consider crafts. Maybe try to \
<bold>sit<reset> or <bold>recline<reset> for a bit!"
)
.to_owned(),
)?;
}
let (from_corpse_id, what_part) = match ctx.command {
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?
{
None => user_error("The corpse seems to be gone".to_owned())?,
Some(it) => it,
};
let explicit = ctx.explicit().await?;
if corpse.location != ctx.item.location {
user_error(format!(
"You try to cut {} but realise it is no longer there.",
corpse.display_for_sentence(explicit, 1, false)
))?
}
ensure_has_butcher_tool(&ctx.trans, &ctx.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)
.map(|pd| &pd.display == &what_part)
== Some(true)
}) {
user_error(format!(
"That part ({}) is now gone. Parts you can cut: {}",
&what_part,
&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",
&ctx.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",
&ctx.item.display_for_sentence(false, 1, true),
&what_part,
&corpse.display_for_sentence(false, 1, false)
);
broadcast_to_room(
ctx.trans,
&ctx.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 QueuedCommandContext<'_>) -> UResult<()> {
if ctx.item.death_data.is_some() {
user_error(
"You butcher things while they are dead, not while YOU are dead!".to_owned(),
)?;
}
let (from_corpse_id, what_part) = match ctx.command {
QueueCommand::Cut {
from_corpse,
what_part,
} => (from_corpse, what_part),
_ => user_error("Unexpected command".to_owned())?,
};
ensure_has_butcher_tool(&ctx.trans, &ctx.item).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,
};
let explicit = ctx.explicit().await?;
if corpse.location != ctx.item.location {
user_error(format!(
"You try to cut {} but realise it is no longer there.",
corpse.display_for_sentence(explicit, 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)
.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>>()
)
))
})?,
};
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 => {}
Some(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),
)
.map(|pt| (*pt).clone())
.collect()
}
}
match check_item_capacity(&ctx.trans, &ctx.item, 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)
{
destroy_container(&ctx.trans, &corpse_mut).await?;
} else {
ctx.trans.save_item_model(&corpse_mut).await?;
}
if skill_check_and_grind(&ctx.trans, ctx.item, &SkillType::Craft, 10.0).await? < 0.0 {
change_stress_considering_cool(&ctx.trans, &mut ctx.item, 500).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} tries to cut the {} from {}, but only leaves a mutilated mess.\n",
&ctx.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",
&ctx.item.display_for_sentence(true, 1, true),
possession_data.display,
corpse.display_for_sentence(true, 1, false)
)),
)
.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 = ctx.item.refstr();
ctx.trans.save_item_model(&new_item).await?;
broadcast_to_room(
&ctx.trans,
&ctx.item.location,
None,
&format!(
"{} expertly cuts the {} from {}.\n",
&ctx.item.display_for_sentence(true, 1, true),
possession_data.display,
corpse.display_for_sentence(true, 1, false)
),
Some(&format!(
"{} expertly cuts the {} from {}.\n",
&ctx.item.display_for_sentence(true, 1, true),
possession_data.display,
corpse.display_for_sentence(true, 1, false)
)),
)
.await?;
}
Ok(())
}
}
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
{
user_error("You have nothing sharp on you suitable for butchery!".to_owned())?;
}
Ok(())
}
pub struct Verb;
#[async_trait]
impl UserVerb for Verb {
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,
};
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(),
)?
}
let possible_corpse = search_item_for_user(
ctx,
&ItemSearchParams {
include_loc_contents: true,
dead_first: true,
..ItemSearchParams::base(&player_item, corpse_raw.trim())
},
)
.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();
let corpse = if possible_corpse.item_type == "corpse" {
possible_corpse
} else if possible_corpse.item_type == "npc" || possible_corpse.item_type == "player" {
let mut possible_corpse_mut = (*possible_corpse).clone();
possible_corpse_mut.location = if possible_corpse.item_type == "npc" {
"room/valhalla"
} else {
"room/repro_xv_respawn"
}
.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)
.ok_or_else(|| UserError("That part doesn't exist anymore".to_owned()))?;
ensure_has_butcher_tool(&ctx.trans, &player_item).await?;
queue_command_and_save(
ctx,
&player_item,
&QueueCommand::Cut {
from_corpse: corpse.item_code.clone(),
what_part: possession_data.display.to_owned(),
},
)
.await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;