Allow cutting parts from corpses.

This commit is contained in:
Condorra 2023-04-24 00:56:42 +10:00
parent 936fcc6dde
commit d8b0b6bed5
6 changed files with 153 additions and 18 deletions

View File

@ -569,11 +569,11 @@ impl DBTrans {
dead_only = true;
}
if dead_only {
extra_where.push_str(" AND COALESCE(CAST(details->>'is_dead' AS boolean), false) = true");
extra_where.push_str(" AND COALESCE(details->>'death_data' IS NOT NULL, false) = true");
} else if search.dead_first {
extra_order.push_str(" COALESCE(CAST(details->>'is_dead' AS boolean), false) DESC,");
extra_order.push_str(" COALESCE(details->>'death_data' IS NOT NULL, false) DESC,");
} else {
extra_order.push_str(" COALESCE(CAST(details->>'is_dead' AS boolean), false) ASC,");
extra_order.push_str(" COALESCE(details->>'death_data' IS NOT NULL, false) ASC,");
}
let query_wildcard = query.replace("\\", "\\\\")

View File

@ -18,6 +18,7 @@ mod buy;
mod c;
pub mod close;
pub mod corp;
mod cut;
pub mod drop;
pub mod get;
mod describe;
@ -130,6 +131,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"c" => c::VERB,
"close" => close::VERB,
"corp" => corp::VERB,
"cut" => cut::VERB,
"drop" => drop::VERB,
"get" => get::VERB,
"install" => install::VERB,

View File

@ -0,0 +1,119 @@
use super::{VerbContext, UserVerb, UserVerbRef, UResult, UserError,
get_player_item_or_fail, user_error, search_item_for_user};
use async_trait::async_trait;
use crate::{
models::{
item::{Item, DeathData, SkillType},
},
db::ItemSearchParams,
static_content::possession_type::possession_data,
language::join_words,
services::{
destroy_container,
skills::skill_check_and_grind, comms::broadcast_to_room,
capacity::{CapacityLevel, check_item_capacity}},
};
use ansi::ansi;
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?;
let 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 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.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>>())
)))?
};
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, &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) {
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",
&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",
&player_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 = 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",
&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",
&player_item.display_for_sentence(true, 1, true),
possession_data.display,
corpse.display_for_sentence(true, 1, false)
))
).await?;
}
ctx.trans.save_item_model(&player_item_mut).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -3,6 +3,7 @@ use crate::{
models::item::Item,
models::consent::{Consent, ConsentType, ConsentStatus},
static_content::npc::npc_by_code,
message_handler::user_commands::drop::consider_expire_job_for_item,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
@ -78,3 +79,25 @@ pub async fn check_consent(trans: &DBTrans, action: &str,
Ok(false)
}
pub async fn destroy_container(trans: &DBTrans, container: &Item) -> DResult<()> {
trans.delete_item(&container.item_type, &container.item_code).await?;
for item in trans.find_items_by_location(
&container.refstr()
).await?.into_iter() {
let mut item_mut = (*item).clone();
// We only update this to support consider_expire_job - it gets updated in bulk
// by transfer_all_possession below.
item_mut.location = container.location.clone();
match capacity::check_item_capacity(trans, &container.location, item_mut.weight).await? {
capacity::CapacityLevel::OverBurdened | capacity::CapacityLevel::AboveItemLimit =>
trans.delete_item(&item_mut.item_type, &item_mut.item_code).await?,
_ => consider_expire_job_for_item(trans, &item_mut).await?
}
}
trans.transfer_all_possessions_code(
&container.refstr(),
&container.location).await?;
Ok(())
}

View File

@ -3,6 +3,7 @@ use crate::{
comms::broadcast_to_room,
skills::skill_check_and_grind,
skills::skill_check_only,
destroy_container,
},
models::{
item::{Item, LocationActionType, Subattack, SkillType, DeathData},
@ -13,7 +14,7 @@ use crate::{
npc::npc_by_code,
species::species_info_map,
},
message_handler::user_commands::{user_error, UResult, drop::consider_expire_job_for_item},
message_handler::user_commands::{user_error, UResult},
regular_tasks::{TaskRunContext, TaskHandler},
DResult,
};
@ -506,23 +507,12 @@ impl TaskHandler for RotCorpseTaskHandler {
None => { return Ok(None) }
Some(r) => r
};
ctx.trans.delete_item("corpse", &corpse_code).await?;
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));
for item in ctx.trans.find_items_by_location(
&format!("{}/{}", &corpse.item_type, &corpse.item_code)).await?.into_iter() {
let mut item_mut = (*item).clone();
// We only update this to support consider_expire_job - it gets updated in bulk
// by transfer_all_possession below.
item_mut.location = corpse.location.clone();
consider_expire_job_for_item(ctx.trans, &item_mut).await?;
}
ctx.trans.transfer_all_possessions_code(
&format!("{}/{}", &corpse.item_type, &corpse.item_code),
&corpse.location).await?;
broadcast_to_room(ctx.trans, &corpse.location, None, &msg_exp, Some(&msg_nonexp)).await?;
Ok(None)
}

View File

@ -3,6 +3,7 @@ use super::PossessionData;
pub fn skin_data() -> PossessionData {
PossessionData {
display: "animal skin",
aliases: vec!("skin"),
details: "The skin of an animal of some kind. It looks like you could make something out of this",
weight: 100,
..Default::default()
@ -20,7 +21,7 @@ pub fn steak_data() -> PossessionData {
pub fn severed_head_data() -> PossessionData {
PossessionData {
display: "steak",
display: "severed head",
details: "A head that has been chopped clean from the body",
weight: 250,
..Default::default()