From d8b0b6bed5d26c7bd5aa93318798187a56e91026 Mon Sep 17 00:00:00 2001 From: Condorra Date: Mon, 24 Apr 2023 00:56:42 +1000 Subject: [PATCH] Allow cutting parts from corpses. --- blastmud_game/src/db.rs | 6 +- .../src/message_handler/user_commands.rs | 2 + .../src/message_handler/user_commands/cut.rs | 119 ++++++++++++++++++ blastmud_game/src/services.rs | 23 ++++ blastmud_game/src/services/combat.rs | 18 +-- .../static_content/possession_type/meat.rs | 3 +- 6 files changed, 153 insertions(+), 18 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/cut.rs diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index ee940a6..866906e 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -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("\\", "\\\\") diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 8f613b8..81972b0 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -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, diff --git a/blastmud_game/src/message_handler/user_commands/cut.rs b/blastmud_game/src/message_handler/user_commands/cut.rs new file mode 100644 index 0000000..be51dc0 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/cut.rs @@ -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: cut thing from 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::>()) + )))? + }; + + 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; diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index cf22d86..ccc2a4b 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -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(()) +} diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 18cad4c..499308b 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -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) } diff --git a/blastmud_game/src/static_content/possession_type/meat.rs b/blastmud_game/src/static_content/possession_type/meat.rs index 46784f6..ec03634 100644 --- a/blastmud_game/src/static_content/possession_type/meat.rs +++ b/blastmud_game/src/static_content/possession_type/meat.rs @@ -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()