Improve cut command to be queued, and always corpsify first.

This commit is contained in:
Condorra 2023-04-24 16:47:08 +10:00
parent d8b0b6bed5
commit 26cc053480
10 changed files with 357 additions and 37 deletions

View File

@ -9,7 +9,10 @@ use tokio_postgres::NoTls;
use crate::message_handler::ListenerSession;
use crate::DResult;
use crate::message_handler::user_commands::parsing::parse_offset;
use crate::static_content::room::Direction;
use crate::static_content::{
room::Direction,
possession_type::PossessionType,
};
use crate::models::{
session::Session,
user::User,
@ -1088,6 +1091,15 @@ impl DBTrans {
&[&corp_id.0, &username.to_lowercase()]).await?;
Ok(())
}
pub async fn count_matching_possessions<'a>(self: &'a Self, location: &str,
allowed_types: &'a[PossessionType]) -> DResult<i64> {
Ok(self.pg_trans()?
.query_one(
"SELECT COUNT(*) FROM items WHERE details->>'location' = $1 AND $2 @> (details->'possession_type')",
&[&location, &serde_json::to_value(allowed_types)?]
).await?.get(0))
}
pub async fn commit(mut self: Self) -> DResult<()> {
let trans_opt = self.with_trans_mut(|t| std::mem::replace(t, None));

View File

@ -18,7 +18,7 @@ mod buy;
mod c;
pub mod close;
pub mod corp;
mod cut;
pub mod cut;
pub mod drop;
pub mod get;
mod describe;

View File

@ -6,40 +6,106 @@ use crate::{
item::{Item, DeathData, SkillType},
},
db::ItemSearchParams,
static_content::possession_type::possession_data,
static_content::possession_type::{possession_data, can_butcher_possessions},
language::join_words,
services::{
destroy_container,
skills::skill_check_and_grind, comms::broadcast_to_room,
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
},
};
use ansi::ansi;
use std::{time, sync::Arc};
use mockall_double::double;
#[double] use crate::db::DBTrans;
pub struct Verb;
pub struct QueueHandler;
#[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
impl QueueCommandHandler for QueueHandler {
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())?;
}
let (from_corpse_id, what_part) = match 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
};
if corpse.location != player_item.location {
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)
.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>>())
))?;
}
};
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 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",
&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?;
Ok(time::Duration::from_secs(1))
}
let what_norm = what_raw.trim().to_lowercase();
#[allow(unreachable_patterns)]
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())?;
}
let (from_corpse_id, what_part) = match command {
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? {
None => user_error("The corpse seems to be gone".to_owned())?,
Some(it) => it
};
if corpse.location != player_item.location {
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)
.map(|pd| pd.display.to_lowercase() == what_norm ||
pd.aliases.iter().any(|a| a.to_lowercase() == what_norm))
.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))
@ -115,5 +181,73 @@ impl UserVerb for Verb {
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(ctx, &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;

View File

@ -22,32 +22,35 @@ use crate::message_handler::user_commands::{
user_error,
get_user_or_fail,
open,
close
close,
cut
};
use crate::static_content::room::Direction;
use once_cell::sync::OnceCell;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum QueueCommand {
Movement { direction: Direction },
Wield { possession_id: String },
Use { possession_id: String, target_id: String },
Get { possession_id: String },
Drop { possession_id: String },
OpenDoor { direction: Direction },
CloseDoor { direction: Direction },
Cut { from_corpse: String, what_part: String },
Drop { possession_id: String },
Get { possession_id: String },
Movement { direction: Direction },
OpenDoor { direction: Direction },
Use { possession_id: String, target_id: String },
Wield { possession_id: String },
}
impl QueueCommand {
pub fn name(&self) -> &'static str {
use QueueCommand::*;
match self {
Movement {..} => "Movement",
Wield {..} => "Wield",
Use {..} => "Use",
Get {..} => "Get",
Drop {..} => "Drop",
OpenDoor {..} => "OpenDoor",
CloseDoor {..} => "CloseDoor",
Cut {..} => "Cut",
Drop {..} => "Drop",
Get {..} => "Get",
Movement {..} => "Movement",
OpenDoor {..} => "OpenDoor",
Use {..} => "Use",
Wield {..} => "Wield",
}
}
}
@ -62,13 +65,14 @@ fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn Que
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)),
("Use", &use_cmd::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("OpenDoor", &open::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("CloseDoor", &close::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
).into_iter().collect())
}

View File

@ -444,7 +444,7 @@ pub async fn start_attack_mut(trans: &DBTrans, by_whom: &mut Item, to_whom: &mut
Ok(())
}
pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult<()> {
pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult<Item> {
let mut new_item = base_item.clone();
new_item.item_type = "corpse".to_owned();
new_item.item_code = format!("{}", trans.alloc_item_code().await?);
@ -461,7 +461,7 @@ pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult<()> {
trans.transfer_all_possessions(base_item, &new_item).await?;
Ok(())
Ok(new_item)
}
pub struct NPCRecloneTaskHandler;

View File

@ -286,3 +286,12 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, PossessionData> {
).into_iter().collect()
})
}
pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
static RELEVANT: OnceCell<Vec<PossessionType>> = OnceCell::new();
&RELEVANT.get_or_init(|| {
possession_data().iter()
.filter_map(|(pt, pd)| if pd.can_butcher { Some(pt.clone()) } else { None })
.collect()
})
}

View File

@ -7,6 +7,7 @@ pub fn butcher_data() -> PossessionData {
details: "A 30 cm long stainless steel blade, sharp on one edge with a pointy tip. It looks perfect for butchering things, and in a pinch you could probably fight with it too.",
aliases: vec!("butcher", "knife"),
weight: 250,
can_butcher: true,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 0.0,

View File

@ -22,6 +22,7 @@ pub fn steak_data() -> PossessionData {
pub fn severed_head_data() -> PossessionData {
PossessionData {
display: "severed head",
aliases: vec!("head"),
details: "A head that has been chopped clean from the body",
weight: 250,
..Default::default()

View File

@ -13,6 +13,7 @@ use crate::{
models::item::{Item, ItemFlag}
};
mod special;
mod repro_xv;
mod melbs;
mod cok_murl;
@ -27,6 +28,9 @@ static STATIC_ZONE_DETAILS: OnceCell<BTreeMap<&'static str, Zone>> = OnceCell::n
pub fn zone_details() -> &'static BTreeMap<&'static str, Zone> {
STATIC_ZONE_DETAILS.get_or_init(
|| vec!(
Zone { code: "special",
display: "Outside of Time",
outdoors: false },
Zone { code: "melbs",
display: "Melbs",
outdoors: true },
@ -253,6 +257,7 @@ pub fn room_list() -> &'static Vec<Room> {
let mut rooms = repro_xv::room_list();
rooms.append(&mut melbs::room_list());
rooms.append(&mut cok_murl::room_list());
rooms.append(&mut special::room_list());
rooms.into_iter().collect()
})
}

View File

@ -0,0 +1,154 @@
use super::{
Room, GridCoords,
};
use ansi::ansi;
// None of these are reachable except when the game or an admin puts something there.
pub fn room_list() -> Vec<Room> {
let holding_desc: &'static str = "The inside of a small pen or cage, with thick steel bars, suitable for holding an animal - or a person - securely, with no chance of escape. It is dimly lit and smells like urine, and is very cramped and indignifying. It looks like the only way out would be with the help of whoever locked you in here. [OOC: consider emailing staff@blastmud.org to discuss your situation]";
vec!(
Room {
zone: "special",
code: "valhalla",
name: "Valhalla",
short: ansi!("<bgyellow><black>VH<reset>"),
description: "Where the valiant dead NPCs go to wait recloning",
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding0",
name: "Holding Pen #0",
short: ansi!("<bgblack><red>H0<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 0, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding1",
name: "Holding Pen #1",
short: ansi!("<bgblack><red>H1<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 0, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding2",
name: "Holding Pen #2",
short: ansi!("<bgblack><red>H2<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 0, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding3",
name: "Holding Pen #3",
short: ansi!("<bgblack><red>H3<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 3, y: 0, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding4",
name: "Holding Pen #4",
short: ansi!("<bgblack><red>H4<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 1, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding5",
name: "Holding Pen #5",
short: ansi!("<bgblack><red>H5<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 1, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding6",
name: "Holding Pen #6",
short: ansi!("<bgblack><red>H6<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 1, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding7",
name: "Holding Pen #7",
short: ansi!("<bgblack><red>H7<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 3, y: 1, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding8",
name: "Holding Pen #8",
short: ansi!("<bgblack><red>H8<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 0, y: 2, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holding9",
name: "Holding Pen #9",
short: ansi!("<bgblack><red>H9<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 2, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holdinga",
name: "Holding Pen A",
short: ansi!("<bgblack><red>HA<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 2, z: -1 },
exits: vec!(),
..Default::default()
},
Room {
zone: "special",
code: "holdingb",
name: "Holding Pen B",
short: ansi!("<bgblack><red>HB<reset>"),
description: holding_desc,
description_less_explicit: None,
grid_coords: GridCoords { x: 3, y: 2, z: -1 },
exits: vec!(),
..Default::default()
},
)
}