diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 866906ef..4219b15a 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -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 { + 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)); diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 81972b07..94fffcd2 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -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; diff --git a/blastmud_game/src/message_handler/user_commands/cut.rs b/blastmud_game/src/message_handler/user_commands/cut.rs index be51dc04..b28b529c 100644 --- a/blastmud_game/src/message_handler/user_commands/cut.rs +++ b/blastmud_game/src/message_handler/user_commands/cut.rs @@ -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: cut thing from corpse").to_owned())?, - Some(v) => v +impl QueueCommandHandler for QueueHandler { + async fn start_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())? + }; + 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::>()) + ))?; + } }; - 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: cut thing from 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::>()) + )))? + }.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; diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index e3d0b555..81cd1511 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -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> = 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()) } diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 499308b6..ee7b3a95 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -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 { 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; diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 13cfa198..6e0e6cbf 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -286,3 +286,12 @@ pub fn possession_data() -> &'static BTreeMap { ).into_iter().collect() }) } + +pub fn can_butcher_possessions() -> &'static Vec { + static RELEVANT: OnceCell> = OnceCell::new(); + &RELEVANT.get_or_init(|| { + possession_data().iter() + .filter_map(|(pt, pd)| if pd.can_butcher { Some(pt.clone()) } else { None }) + .collect() + }) +} diff --git a/blastmud_game/src/static_content/possession_type/blade.rs b/blastmud_game/src/static_content/possession_type/blade.rs index 1697f365..06f211f7 100644 --- a/blastmud_game/src/static_content/possession_type/blade.rs +++ b/blastmud_game/src/static_content/possession_type/blade.rs @@ -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, diff --git a/blastmud_game/src/static_content/possession_type/meat.rs b/blastmud_game/src/static_content/possession_type/meat.rs index ec03634b..2fdf1f91 100644 --- a/blastmud_game/src/static_content/possession_type/meat.rs +++ b/blastmud_game/src/static_content/possession_type/meat.rs @@ -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() diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index eba9d80d..6c53cc58 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -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> = 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 { 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() }) } diff --git a/blastmud_game/src/static_content/room/special.rs b/blastmud_game/src/static_content/room/special.rs new file mode 100644 index 00000000..b1ef4992 --- /dev/null +++ b/blastmud_game/src/static_content/room/special.rs @@ -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 { + 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!("VH"), + 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!("H0"), + 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!("H1"), + 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!("H2"), + 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!("H3"), + 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!("H4"), + 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!("H5"), + 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!("H6"), + 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!("H7"), + 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!("H8"), + 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!("H9"), + 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!("HA"), + 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!("HB"), + description: holding_desc, + description_less_explicit: None, + grid_coords: GridCoords { x: 3, y: 2, z: -1 }, + exits: vec!(), + ..Default::default() + }, + ) +}