From 2747dddd90d4506d5966a8d3cbe44e2c8176037e Mon Sep 17 00:00:00 2001 From: Condorra Date: Tue, 23 May 2023 20:37:27 +1000 Subject: [PATCH] Allow buying, wearing and removing clothes. Step 2 will be to make clothes serve a functional purpose as armour. --- blastmud_game/src/db.rs | 33 +- .../src/message_handler/user_commands.rs | 4 + .../src/message_handler/user_commands/drop.rs | 9 + .../src/message_handler/user_commands/look.rs | 70 ++- .../src/message_handler/user_commands/open.rs | 4 +- .../message_handler/user_commands/remove.rs | 179 +++++++ .../src/message_handler/user_commands/wear.rs | 189 +++++++ blastmud_game/src/models/consent.rs | 2 +- blastmud_game/src/models/item.rs | 2 + .../src/regular_tasks/queued_command.rs | 26 +- blastmud_game/src/services/combat.rs | 3 +- .../src/static_content/possession_type.rs | 60 ++- .../possession_type/antenna_whip.rs | 38 -- .../static_content/possession_type/blade.rs | 78 +-- .../possession_type/corp_licence.rs | 52 +- .../static_content/possession_type/fangs.rs | 57 ++- .../possession_type/head_armour.rs | 36 ++ .../static_content/possession_type/lock.rs | 33 +- .../possession_type/lower_armour.rs | 23 + .../static_content/possession_type/meat.rs | 63 +-- .../possession_type/torso_armour.rs | 25 + .../possession_type/trauma_kit.rs | 463 +++++++++--------- .../static_content/possession_type/whip.rs | 45 ++ .../src/static_content/room/melbs.rs | 47 +- blastmud_game/src/static_content/species.rs | 21 + 25 files changed, 1127 insertions(+), 435 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/remove.rs create mode 100644 blastmud_game/src/message_handler/user_commands/wear.rs delete mode 100644 blastmud_game/src/static_content/possession_type/antenna_whip.rs create mode 100644 blastmud_game/src/static_content/possession_type/head_armour.rs create mode 100644 blastmud_game/src/static_content/possession_type/lower_armour.rs create mode 100644 blastmud_game/src/static_content/possession_type/torso_armour.rs create mode 100644 blastmud_game/src/static_content/possession_type/whip.rs diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 4219b15..af25801 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -243,6 +243,7 @@ pub struct ItemSearchParams<'l> { pub include_active_players: bool, pub include_all_players: bool, pub item_type_only: Option<&'l str>, + pub item_action_type_only: Option<&'l LocationActionType>, pub limit: u8, pub dead_first: bool, } @@ -258,6 +259,7 @@ impl ItemSearchParams<'_> { dead_first: false, limit: 100, item_type_only: None, + item_action_type_only: None, } } } @@ -602,6 +604,19 @@ impl DBTrans { } } + let item_action_type_value: Option = match search.item_action_type_only { + None => None, + Some(v) => Some(serde_json::to_value(v)?) + }; + match item_action_type_value { + None => {} + Some(ref item_action_type) => { + extra_where.push_str(&format!(" AND (details->'action_type')::TEXT = ${}::JSONB::TEXT", param_no)); + param_no += 1; + params.push(item_action_type); + } + } + if search.include_contents { ctes.push(format!("contents AS (\ SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\ @@ -758,16 +773,20 @@ impl DBTrans { Ok(()) } - pub async fn find_by_action_and_location(&self, location: &str, action_type: &LocationActionType) -> DResult>> { - if let Some(item) = self.pg_trans()?.query_opt( + pub async fn find_by_action_and_location(&self, location: &str, action_type: &LocationActionType) -> DResult>> { + Ok(self.pg_trans()?.query( "SELECT details FROM items WHERE \ details->>'location' = $1 AND \ - ((details->'action_type')::TEXT = $2::JSONB::TEXT)", + ((details->'action_type')::TEXT = $2::JSONB::TEXT) LIMIT 100", &[&location, - &serde_json::to_value(action_type)?]).await? { - return Ok(Some(Arc::new(serde_json::from_value::(item.get("details"))?))); - } - Ok(None) + &serde_json::to_value(action_type)?]).await? + .into_iter() + .filter_map( + |row| match serde_json::from_value::(row.get("details")) { + Err(_) => None, + Ok(item) => Some(Arc::new(item)) + } + ).collect()) } pub async fn list_consents(&self, consenting: &str) -> DResult> { diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 74a3dcb..25ed0fe 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -38,6 +38,7 @@ mod page; pub mod parsing; mod quit; pub mod register; +pub mod remove; pub mod rent; pub mod say; mod score; @@ -46,6 +47,7 @@ mod status; mod uninstall; pub mod use_cmd; mod vacate; +pub mod wear; mod whisper; mod who; pub mod wield; @@ -164,6 +166,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "repl" => page::VERB, "reply" => page::VERB, + "remove" => remove::VERB, "rent" => rent::VERB, "\'" => say::VERB, @@ -186,6 +189,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "whisper" => whisper::VERB, "tell" => whisper::VERB, + "wear" => wear::VERB, "wield" => wield::VERB, "who" => who::VERB, "write" => write::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/drop.rs b/blastmud_game/src/message_handler/user_commands/drop.rs index 09f9be7..8295c27 100644 --- a/blastmud_game/src/message_handler/user_commands/drop.rs +++ b/blastmud_game/src/message_handler/user_commands/drop.rs @@ -35,6 +35,7 @@ use crate::{ }; use async_trait::async_trait; use std::time; +use ansi::ansi; use chrono::Utc; use mockall_double::double; #[double] use crate::db::DBTrans; @@ -137,6 +138,10 @@ impl QueueCommandHandler for QueueHandler { ) )? } + if item.action_type == LocationActionType::Worn { + user_error( + ansi!("You're wearing it - try using remove first").to_owned())?; + } let msg_exp = format!("{} prepares to drop {}\n", &player_item.display_for_sentence(true, 1, true), &item.display_for_sentence(true, 1, false)); @@ -167,6 +172,10 @@ impl QueueCommandHandler for QueueHandler { user_error(format!("You try to drop {} but realise you no longer have it!", &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? } + if item.action_type == LocationActionType::Worn { + user_error( + ansi!("You're wearing it - try using remove first").to_owned())?; + } let possession_data = match item.possession_type.as_ref().and_then(|pt| possession_data().get(&pt)) { None => user_error("That item no longer exists in the game so can't be handled".to_owned())?, diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs index c65533e..4c9e8a3 100644 --- a/blastmud_game/src/message_handler/user_commands/look.rs +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -16,12 +16,14 @@ use crate::{ room::{self, Direction}, dynzone::self, possession_type::possession_data, + species::{SpeciesType, species_info_map}, }, language, services::combat::max_health, }; use itertools::Itertools; use std::sync::Arc; +use std::collections::BTreeSet; use mockall_double::double; #[double] use crate::db::DBTrans; @@ -31,7 +33,8 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult let mut items = ctx.trans.find_items_by_location(&format!("{}/{}", item.item_type, item.item_code)).await?; - items.sort_unstable_by(|it1, it2| (&it1.display).cmp(&it2.display)); + items.sort_unstable_by(|it1, it2| (&it1.action_type).cmp(&it2.action_type) + .then((&it1.display).cmp(&it2.display))); let all_groups: Vec>> = items .iter() @@ -52,14 +55,73 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult let head = &group_items[0]; let mut details = head.display_for_sentence(!ctx.session_dat.less_explicit_mode, group_items.len(), false); - if head.action_type == LocationActionType::Wielded { - details.push_str(" (wielded)"); + match head.action_type { + LocationActionType::Wielded => details.push_str(" (wielded)"), + LocationActionType::Worn => continue, + _ => {} } phrases.push(details); } let phrases_str: Vec<&str> = phrases.iter().map(|p| p.as_str()).collect(); contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n")); } + + let anything_worn = items.iter().any(|it| it.action_type == LocationActionType::Worn); + if anything_worn { + let mut any_part_text = false; + let mut seen_clothes: BTreeSet = BTreeSet::new(); + for part in species_info_map().get(&item.species).map(|s| s.body_parts.clone()) + .unwrap_or_else(|| vec!()) { + if let Some((top_item, covering_parts)) = items.iter() + .filter_map( + |it| + if it.action_type != LocationActionType::Worn { + None + } else { + it.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.wear_data.as_ref()) + .and_then(|wd| if wd.covers_parts.contains(&part) { + Some((it, wd.covers_parts.clone())) + } else { + None + }) + }) + .filter_map(|(it, parts)| it.action_type_started.map(|st| ((it, parts), st))) + .max_by_key(|(_it, st)| st.clone()).map(|(it, _)| it) + { + any_part_text = true; + let display = top_item.display_for_session(&ctx.session_dat); + if !seen_clothes.contains(&display) { + seen_clothes.insert(display.clone()); + contents_desc.push_str(&format!( + "On {} {}, you see {}. ", + &item.pronouns.possessive, + &language::join_words( + &covering_parts.iter().map(|p| p.display(None)) + .collect::>()), + &display + )); + } + } else { + if !ctx.session_dat.less_explicit_mode { + any_part_text = true; + contents_desc.push_str(&format!("{} {} {} completely bare. ", + &language::caps_first(&item.pronouns.possessive), + part.display(item.sex.clone()), + part.copula(item.sex.clone()))); + } + } + + } + if any_part_text { + contents_desc.push_str("\n"); + } + } else if item.species == SpeciesType::Human && !ctx.session_dat.less_explicit_mode { + contents_desc.push_str(&format!("{} is completely naked.\n", + &language::caps_first(&item.pronouns.possessive))); + } + let health_max = max_health(&item); if health_max > 0 { let health_ratio = (item.health as f64) / (health_max as f64); @@ -195,7 +257,7 @@ async fn describe_door( &state.description); if let Some(lock) = ctx.trans.find_by_action_and_location( &room_item.refstr(), - &LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await? + &LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await?.first() { let lock_desc = lock.display_for_session(&ctx.session_dat); msg.push_str(&format!(" The door is locked with {}", diff --git a/blastmud_game/src/message_handler/user_commands/open.rs b/blastmud_game/src/message_handler/user_commands/open.rs index e7cf09d..eeae66e 100644 --- a/blastmud_game/src/message_handler/user_commands/open.rs +++ b/blastmud_game/src/message_handler/user_commands/open.rs @@ -90,7 +90,7 @@ pub async fn attempt_open_immediate(trans: &DBTrans, ctx_opt: &mut Option<&mut V if let Some(revdir) = direction.reverse() { if let Some(lock) = trans.find_by_action_and_location( &entering_room_loc, - &LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await? + &LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await?.first() { if let Some(ctx) = ctx_opt { if let Some(lockcheck) = lock.possession_type.as_ref() @@ -188,7 +188,7 @@ impl QueueCommandHandler for QueueHandler { if let Some(revdir) = direction.reverse() { if let Some(lock) = ctx.trans.find_by_action_and_location( &entering_room_loc, - &LocationActionType::InstalledOnDoorAsLock(revdir)).await? + &LocationActionType::InstalledOnDoorAsLock(revdir)).await?.first() { if let Some(lockcheck) = lock.possession_type.as_ref() .and_then(|pt| possession_data().get(pt)) diff --git a/blastmud_game/src/message_handler/user_commands/remove.rs b/blastmud_game/src/message_handler/user_commands/remove.rs new file mode 100644 index 0000000..ff96069 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/remove.rs @@ -0,0 +1,179 @@ +use super::{ + VerbContext, + UserVerb, + UserVerbRef, + UResult, + ItemSearchParams, + UserError, + user_error, + get_player_item_or_fail, + search_items_for_user, + parsing::parse_count +}; +use crate::{ + static_content::possession_type::{possession_data, WearData}, + regular_tasks::queued_command::{ + QueueCommandHandler, + QueueCommand, + queue_command + }, + services::{ + comms::broadcast_to_room, + }, + models::item::{Item, LocationActionType}, +}; +use async_trait::async_trait; +use std::time; + +async fn check_removeable(ctx: &mut VerbContext<'_>, + item: &Item, player_item: &Item) -> UResult<()> { + if item.location != player_item.refstr() { + user_error(format!("You try to remove {} but realise you no longer have it.", + &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? + } + + if item.action_type != LocationActionType::Worn { + user_error("You realise you're not wearing it!".to_owned())?; + } + + let poss_data = item.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .ok_or_else(|| UserError( + "That item no longer exists in the game so can't be handled. Ask staff for help.".to_owned()))?; + + let wear_data = poss_data.wear_data.as_ref().ok_or_else( + || UserError("You seem to be wearing something that isn't clothes! Ask staff for help.".to_owned()))?; + + let other_clothes = + ctx.trans.find_by_action_and_location( + &player_item.refstr(), &LocationActionType::Worn).await?; + + if let Some(my_worn_since) = item.action_type_started { + for part in &wear_data.covers_parts { + if let Some(other_item) = other_clothes.iter().find( + |other_item| + match other_item.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.wear_data.as_ref()) { + None => false, + Some(WearData { covers_parts, .. }) => + covers_parts.contains(&part) && + other_item.action_type_started + .map(|other_worn_since| other_worn_since < my_worn_since) + .unwrap_or(false) + } + ) { + user_error(format!( + "You can't do that without first removing your {} from your {}.", + &other_item.display_for_session(&ctx.session_dat), + part.display(player_item.sex.clone()) + ))?; + } + } + } + Ok(()) +} + +pub struct QueueHandler; +#[async_trait] +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 try to remove it, but your ghostly hands slip through it uselessly".to_owned())?; + } + let item_id = match command { + QueueCommand::Remove { possession_id } => possession_id, + _ => user_error("Unexpected command".to_owned())? + }; + let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { + None => user_error("Item not found".to_owned())?, + Some(it) => it + }; + + check_removeable(ctx, &item, &player_item).await?; + + let msg_exp = format!("{} fumbles around trying to take off {}\n", + &player_item.display_for_sentence(true, 1, true), + &item.display_for_sentence(true, 1, false)); + let msg_nonexp = format!("{} fumbles around trying to take off {}\n", + &player_item.display_for_sentence(false, 1, true), + &item.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)) + } + + #[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 try to remove it, but your ghostly hands slip through it uselessly".to_owned())?; + } + let item_id = match command { + QueueCommand::Remove { possession_id } => possession_id, + _ => user_error("Unexpected command".to_owned())? + }; + let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { + None => user_error("Item not found".to_owned())?, + Some(it) => it + }; + + check_removeable(ctx, &item, &player_item).await?; + + let msg_exp = format!("{} removes {}\n", + &player_item.display_for_sentence(true, 1, true), + &item.display_for_sentence(true, 1, false)); + let msg_nonexp = format!("{} removes {}\n", + &player_item.display_for_sentence(false, 1, true), + &item.display_for_sentence(false, 1, false)); + broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; + let mut item_mut = (*item).clone(); + item_mut.action_type = LocationActionType::Normal; + item_mut.action_type_started = None; + ctx.trans.save_item_model(&item_mut).await?; + Ok(()) + } +} + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, mut remaining: &str) -> UResult<()> { + let player_item = get_player_item_or_fail(ctx).await?; + let mut get_limit = Some(1); + if remaining == "all" || remaining.starts_with("all ") { + remaining = remaining[3..].trim(); + get_limit = None; + } else if let (Some(n), remaining2) = parse_count(remaining) { + get_limit = Some(n); + remaining = remaining2; + } + let targets = search_items_for_user(ctx, &ItemSearchParams { + include_contents: true, + item_type_only: Some("possession"), + item_action_type_only: Some(&LocationActionType::Worn), + limit: get_limit.unwrap_or(100), + ..ItemSearchParams::base(&player_item, &remaining) + }).await?; + if player_item.death_data.is_some() { + user_error("The dead don't undress themselves".to_owned())?; + } + + let mut did_anything: bool = false; + for target in targets.iter().filter(|t| t.action_type.is_visible_in_look()) { + if target.item_type != "possession" { + user_error("You can't remove that!".to_owned())?; + } + did_anything = true; + queue_command(ctx, &QueueCommand::Remove { possession_id: target.item_code.clone() }).await?; + } + if !did_anything { + user_error("I didn't find anything matching.".to_owned())?; + } + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/message_handler/user_commands/wear.rs b/blastmud_game/src/message_handler/user_commands/wear.rs new file mode 100644 index 0000000..ff67daf --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/wear.rs @@ -0,0 +1,189 @@ +use super::{ + VerbContext, + UserVerb, + UserVerbRef, + UResult, + ItemSearchParams, + UserError, + user_error, + get_player_item_or_fail, + search_items_for_user, + parsing::parse_count +}; +use crate::{ + static_content::possession_type::possession_data, + regular_tasks::queued_command::{ + QueueCommandHandler, + QueueCommand, + queue_command + }, + services::{ + comms::broadcast_to_room, + }, + models::item::LocationActionType, +}; +use async_trait::async_trait; +use chrono::Utc; +use log::info; +use std::time; + +pub struct QueueHandler; +#[async_trait] +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 try to wear it, but your ghostly hands slip through it uselessly".to_owned())?; + } + let item_id = match command { + QueueCommand::Wear { possession_id } => possession_id, + _ => user_error("Unexpected command".to_owned())? + }; + let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { + None => user_error("Item not found".to_owned())?, + Some(it) => it + }; + if item.location != player_item.refstr() { + user_error( + format!("You try to wear {} but realise you no longer have it", + item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false) + ) + )? + } + + if item.action_type == LocationActionType::Worn { + user_error("You realise you're already wearing it!".to_owned())?; + } + + let poss_data = item.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .ok_or_else(|| UserError( + "That item no longer exists in the game so can't be handled".to_owned()))?; + + poss_data.wear_data.as_ref().ok_or_else( + || UserError("You can't wear that!".to_owned()))?; + + let msg_exp = format!("{} fumbles around trying to put on {}\n", + &player_item.display_for_sentence(true, 1, true), + &item.display_for_sentence(true, 1, false)); + let msg_nonexp = format!("{} fumbles around trying to put on {}\n", + &player_item.display_for_sentence(false, 1, true), + &item.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)) + } + + #[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 try to wear it, but your ghostly hands slip through it uselessly".to_owned())?; + } + let item_id = match command { + QueueCommand::Wear { possession_id } => possession_id, + _ => user_error("Unexpected command".to_owned())? + }; + let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? { + None => user_error("Item not found".to_owned())?, + Some(it) => it + }; + if item.location != player_item.refstr() { + user_error(format!("You try to wear {} but realise it is no longer there.", + &item.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, false)))? + } + + if item.action_type == LocationActionType::Worn { + user_error("You realise you're already wearing it!".to_owned())?; + } + + let poss_data = item.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .ok_or_else(|| UserError( + "That item no longer exists in the game so can't be handled".to_owned()))?; + + let wear_data = poss_data.wear_data.as_ref().ok_or_else( + || UserError("You can't wear that!".to_owned()))?; + + let other_clothes = + ctx.trans.find_by_action_and_location( + &player_item.refstr(), &LocationActionType::Worn).await?; + for part in &wear_data.covers_parts { + let thickness: f64 = + other_clothes.iter().fold( + wear_data.thickness, + |tot, other_item| + match other_item.possession_type.as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.wear_data.as_ref()) + { + Some(wd) if wd.covers_parts.contains(&part) => + tot + wd.thickness, + _ => tot, + } + ); + info!("Thickness with item: {}", thickness); + if thickness > 12.0 { + user_error(format!( + "You're wearing too much on your {} already.", + part.display(player_item.sex.clone()) + ))?; + } + } + + let msg_exp = format!("{} wears {}\n", + &player_item.display_for_sentence(true, 1, true), + &item.display_for_sentence(true, 1, false)); + let msg_nonexp = format!("{} wears {}\n", + &player_item.display_for_sentence(false, 1, true), + &item.display_for_sentence(false, 1, false)); + broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?; + let mut item_mut = (*item).clone(); + item_mut.action_type = LocationActionType::Worn; + item_mut.action_type_started = Some(Utc::now()); + ctx.trans.save_item_model(&item_mut).await?; + Ok(()) + } +} + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle(self: &Self, ctx: &mut VerbContext, _verb: &str, mut remaining: &str) -> UResult<()> { + let player_item = get_player_item_or_fail(ctx).await?; + let mut get_limit = Some(1); + if remaining == "all" || remaining.starts_with("all ") { + remaining = remaining[3..].trim(); + get_limit = None; + } else if let (Some(n), remaining2) = parse_count(remaining) { + get_limit = Some(n); + remaining = remaining2; + } + let targets = search_items_for_user(ctx, &ItemSearchParams { + include_contents: true, + item_type_only: Some("possession"), + limit: get_limit.unwrap_or(100), + item_action_type_only: Some(&LocationActionType::Normal), + ..ItemSearchParams::base(&player_item, &remaining) + }).await?; + if player_item.death_data.is_some() { + user_error("The dead don't dress themselves".to_owned())?; + } + + let mut did_anything: bool = false; + for target in targets.iter().filter(|t| t.action_type.is_visible_in_look()) { + if target.item_type != "possession" { + user_error("You can't wear that!".to_owned())?; + } + did_anything = true; + queue_command(ctx, &QueueCommand::Wear { possession_id: target.item_code.clone() }).await?; + } + if !did_anything { + user_error("I didn't find anything matching.".to_owned())?; + } + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/models/consent.rs b/blastmud_game/src/models/consent.rs index 2e32f23..ee9dc45 100644 --- a/blastmud_game/src/models/consent.rs +++ b/blastmud_game/src/models/consent.rs @@ -2,7 +2,7 @@ use serde::{Serialize, Deserialize}; use chrono::{DateTime, Utc}; use itertools::Itertools; -#[derive(Serialize, Deserialize, PartialEq, Debug)] +#[derive(Serialize, Deserialize, PartialEq, Debug, Clone)] pub enum ConsentType { Fight, Medicine, diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 9455863..5090aac 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -383,6 +383,7 @@ pub struct Item { pub aliases: Vec, pub location: String, // Item reference as item_type/item_code. pub action_type: LocationActionType, + pub action_type_started: Option>, pub presence_target: Option, // e.g. what are they sitting on. pub is_static: bool, pub death_data: Option, @@ -468,6 +469,7 @@ impl Default for Item { aliases: vec!(), location: "room/storage".to_owned(), action_type: LocationActionType::Normal, + action_type_started: None, presence_target: None, is_static: false, death_data: None, diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index 81cd151..95a42f7 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -14,16 +14,18 @@ use crate::message_handler::user_commands::{ VerbContext, CommandHandlingError, UResult, - get, - drop, - movement, - use_cmd, - wield, - user_error, - get_user_or_fail, - open, close, - cut + cut, + drop, + get, + get_user_or_fail, + movement, + open, + remove, + use_cmd, + user_error, + wear, + wield, }; use crate::static_content::room::Direction; use once_cell::sync::OnceCell; @@ -36,7 +38,9 @@ pub enum QueueCommand { Get { possession_id: String }, Movement { direction: Direction }, OpenDoor { direction: Direction }, + Remove { possession_id: String }, Use { possession_id: String, target_id: String }, + Wear { possession_id: String }, Wield { possession_id: String }, } impl QueueCommand { @@ -49,7 +53,9 @@ impl QueueCommand { Get {..} => "Get", Movement {..} => "Movement", OpenDoor {..} => "OpenDoor", + Remove {..} => "Remove", Use {..} => "Use", + Wear {..} => "Wear", Wield {..} => "Wield", } } @@ -71,7 +77,9 @@ fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn Que ("Get", &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), ("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), ("OpenDoor", &open::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), + ("Remove", &remove::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), ("Use", &use_cmd::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), + ("Wear", &wear::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), ("Wield", &wield::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 ce6f145..5e3ebe7 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -287,6 +287,7 @@ pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> { if let Some(item) = trans.find_by_action_and_location( - &who.refstr(), &LocationActionType::Wielded).await? { + &who.refstr(), &LocationActionType::Wielded).await?.first() { if let Some(dat) = item.possession_type.as_ref() .and_then(|pt| possession_data().get(&pt)) .and_then(|pd| pd.weapon_data.as_ref()) { diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 6e0e6cb..272748d 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -3,21 +3,26 @@ use crate::{ models::item::{SkillType, Item, Pronouns}, models::consent::ConsentType, message_handler::user_commands::{UResult, VerbContext}, - static_content::room::Direction, + static_content::{ + room::Direction, + species::BodyPart, + }, }; use once_cell::sync::OnceCell; use std::collections::BTreeMap; use rand::seq::SliceRandom; -use super::species::BodyPart; use async_trait::async_trait; mod fangs; -mod antenna_whip; +mod whip; mod blade; mod trauma_kit; mod corp_licence; mod lock; mod meat; +pub mod head_armour; +pub mod torso_armour; +pub mod lower_armour; pub type AttackMessageChoice = Vec String + 'static + Sync + Send>>; pub type AttackMessageChoicePart = Vec String + 'static + Sync + Send>>; @@ -115,6 +120,11 @@ impl Default for UseData { } } +pub struct WearData { + pub covers_parts: Vec, + pub thickness: f64, +} + #[async_trait] pub trait WriteHandler { async fn write_cmd(&self, ctx: &mut VerbContext, player: &Item, on_what: &Item, write_what: &str) -> UResult<()>; @@ -148,6 +158,7 @@ pub struct PossessionData { pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, pub can_butcher: bool, + pub wear_data: Option } impl Default for PossessionData { @@ -169,6 +180,7 @@ impl Default for PossessionData { sign_handler: None, write_handler: None, can_butcher: false, + wear_data: None, } } } @@ -201,16 +213,28 @@ pub enum PossessionType { // Special values that substitute for possessions. Fangs, // Default weapon for certain animals // Real possessions from here on: + // Armour + RustyMetalPot, + HockeyMask, + LeatherJacket, + LeatherPants, + // Weapons: Whips AntennaWhip, + // Weapons: Blades + ButcherKnife, + // Medical MediumTraumaKit, EmptyMedicalBox, + // Corporate NewCorpLicence, CertificateOfIncorporation, + // Security Scanlock, - ButcherKnife, + // Crafting Steak, AnimalSkin, SeveredHead, + } impl Into for PossessionType { @@ -267,23 +291,23 @@ pub fn fist() -> &'static WeaponData { }) } -pub fn possession_data() -> &'static BTreeMap { - static POSSESSION_DATA: OnceCell> = OnceCell::new(); +pub fn possession_data() -> &'static BTreeMap { + static POSSESSION_DATA: OnceCell> = OnceCell::new(); use PossessionType::*; &POSSESSION_DATA.get_or_init(|| { vec!( - (Fangs, fangs::data()), - (AntennaWhip, antenna_whip::data()), - (ButcherKnife, blade::butcher_data()), - (MediumTraumaKit, trauma_kit::medium_data()), - (EmptyMedicalBox, trauma_kit::empty_data()), - (NewCorpLicence, corp_licence::data()), - (CertificateOfIncorporation, corp_licence::cert_data()), - (Scanlock, lock::scan()), - (Steak, meat::steak_data()), - (AnimalSkin, meat::skin_data()), - (SeveredHead, meat::severed_head_data()), - ).into_iter().collect() + (Fangs, fangs::data()) + ).into_iter() + .chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(corp_licence::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(head_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(torso_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .chain(lower_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) + .collect() }) } diff --git a/blastmud_game/src/static_content/possession_type/antenna_whip.rs b/blastmud_game/src/static_content/possession_type/antenna_whip.rs deleted file mode 100644 index 078abe4..0000000 --- a/blastmud_game/src/static_content/possession_type/antenna_whip.rs +++ /dev/null @@ -1,38 +0,0 @@ -use super::{PossessionData, WeaponData}; -use crate::models::item::SkillType; - -pub fn data() -> PossessionData { - PossessionData { - display: "antenna whip", - details: "A crudely fashioned whip made from a broken metal antenna. It looks a bit flimsy, but it \ - might do you until you get a better weapon!", - aliases: vec!("whip"), - weapon_data: Some(WeaponData { - uses_skill: SkillType::Whips, - raw_min_to_learn: 0.0, - raw_max_to_learn: 2.0, - normal_attack_start_messages: vec!( - Box::new(|attacker, victim, exp| - format!("{} lines up {} antenna whip for a strike on {}", - &attacker.display_for_sentence(exp, 1, true), - &attacker.pronouns.possessive, - &victim.display_for_sentence(exp, 1, false), - ) - ) - ), - normal_attack_success_messages: vec!( - Box::new(|attacker, victim, part, exp| - format!("{}'s antenna whip scores a painful red line across {}'s {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &part.display(victim.sex.clone()) - ) - ) - ), - normal_attack_mean_damage: 3.0, - normal_attack_stdev_damage: 3.0, - ..Default::default() - }), - ..Default::default() - } -} diff --git a/blastmud_game/src/static_content/possession_type/blade.rs b/blastmud_game/src/static_content/possession_type/blade.rs index 9238cf0..025904b 100644 --- a/blastmud_game/src/static_content/possession_type/blade.rs +++ b/blastmud_game/src/static_content/possession_type/blade.rs @@ -1,39 +1,45 @@ -use super::{PossessionData, WeaponData}; +use super::{PossessionData, WeaponData, PossessionType}; use crate::models::item::SkillType; +use once_cell::sync::OnceCell; -pub fn butcher_data() -> PossessionData { - PossessionData { - display: "butcher knife", - 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, - raw_max_to_learn: 2.0, - normal_attack_start_messages: vec!( - Box::new(|attacker, victim, exp| - format!("{} raises {} butcher knife menancingly, preparing to attack {}", - &attacker.display_for_sentence(exp, 1, true), - &attacker.pronouns.possessive, - &victim.display_for_sentence(exp, 1, false), - ) - ) - ), - normal_attack_success_messages: vec!( - Box::new(|attacker, victim, part, exp| - format!("{}'s butcher knife cuts into {}'s {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &part.display(victim.sex.clone()) - ) - ) - ), - normal_attack_mean_damage: 2.0, - normal_attack_stdev_damage: 2.0, - ..Default::default() - }), - ..Default::default() - } +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + (PossessionType::ButcherKnife, + PossessionData { + display: "butcher knife", + 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, + raw_max_to_learn: 2.0, + normal_attack_start_messages: vec!( + Box::new(|attacker, victim, exp| + format!("{} raises {} butcher knife menancingly, preparing to attack {}", + &attacker.display_for_sentence(exp, 1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(exp, 1, false), + ) + ) + ), + normal_attack_success_messages: vec!( + Box::new(|attacker, victim, part, exp| + format!("{}'s butcher knife cuts into {}'s {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + ) + ), + normal_attack_mean_damage: 2.0, + normal_attack_stdev_damage: 2.0, + ..Default::default() + }), + ..Default::default() + }) + )) } diff --git a/blastmud_game/src/static_content/possession_type/corp_licence.rs b/blastmud_game/src/static_content/possession_type/corp_licence.rs index 4b77b5b..ea9a6bf 100644 --- a/blastmud_game/src/static_content/possession_type/corp_licence.rs +++ b/blastmud_game/src/static_content/possession_type/corp_licence.rs @@ -1,4 +1,4 @@ -use super::{PossessionData, WriteHandler, ArglessHandler}; +use super::{PossessionData, PossessionType, WriteHandler, ArglessHandler, possession_data}; use crate::{ models::{ item::{Item, ItemSpecialData}, @@ -9,6 +9,7 @@ use crate::{ parsing::parse_username, user_error, UResult, + CommandHandlingError::UserError, VerbContext, }, services::comms::broadcast_to_room, @@ -16,6 +17,7 @@ use crate::{ use ansi::ansi; use async_trait::async_trait; use chrono::Utc; +use once_cell::sync::OnceCell; use super::PossessionType::*; @@ -102,7 +104,8 @@ impl ArglessHandler for CorpLicenceHandler { let mut what_mut = what.clone(); what_mut.possession_type = Some(CertificateOfIncorporation); - let cp_data = cert_data(); + let cp_data = possession_data().get(&CertificateOfIncorporation) + .ok_or_else(|| UserError("Certificate of Incorporation no longer exists as an item".to_owned()))?; what_mut.display = cp_data.display.to_owned(); what_mut.details = Some(cp_data.details.to_owned()); ctx.trans.save_item_model(&what_mut).await?; @@ -113,24 +116,29 @@ impl ArglessHandler for CorpLicenceHandler { static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {}; -pub fn data() -> PossessionData { - PossessionData { - display: "new corp licence", - details: ansi!("A blank form that you can use to establish a new corp. It rests on a clipboard with a pencil attached by a chain. There is a space to write on it [try write Blah on licence followed by sign licence to create a corp named Blah]"), - aliases: vec!("form", "license", "licence", "new"), - weight: 10, - becomes_on_spent: Some(CertificateOfIncorporation), - write_handler: Some(&CORP_LICENCE_HANDLER), - sign_handler: Some(&CORP_LICENCE_HANDLER), - ..Default::default() - } -} - -pub fn cert_data() -> PossessionData { - PossessionData { - display: "certificate of incorporation", - details: "A certificate recording the formation of a corp.", - weight: 10, - ..Default::default() - } +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + (NewCorpLicence, + PossessionData { + display: "new corp licence", + details: ansi!("A blank form that you can use to establish a new corp. It rests on a clipboard with a pencil attached by a chain. There is a space to write on it [try write Blah on licence followed by sign licence to create a corp named Blah]"), + aliases: vec!("form", "license", "licence", "new"), + weight: 10, + becomes_on_spent: Some(CertificateOfIncorporation), + write_handler: Some(&CORP_LICENCE_HANDLER), + sign_handler: Some(&CORP_LICENCE_HANDLER), + ..Default::default() + }), + ( + CertificateOfIncorporation, + PossessionData { + display: "certificate of incorporation", + details: "A certificate recording the formation of a corp.", + weight: 10, + ..Default::default() + } + ), + )) } diff --git a/blastmud_game/src/static_content/possession_type/fangs.rs b/blastmud_game/src/static_content/possession_type/fangs.rs index 3aedbb2..1c88843 100644 --- a/blastmud_game/src/static_content/possession_type/fangs.rs +++ b/blastmud_game/src/static_content/possession_type/fangs.rs @@ -1,32 +1,37 @@ use super::{PossessionData, WeaponData}; use crate::models::item::SkillType; +use once_cell::sync::OnceCell; -pub fn data() -> PossessionData { - PossessionData { - weapon_data: Some(WeaponData { - uses_skill: SkillType::Fists, - raw_min_to_learn: 0.0, - raw_max_to_learn: 2.0, - normal_attack_start_messages: vec!( - Box::new(|attacker, victim, exp| - format!("{} bares {} teeth and lunges at {}", - &attacker.display_for_sentence(exp, 1, true), - &attacker.pronouns.possessive, - &victim.display_for_sentence(exp, 1, false), +pub fn data() -> &'static PossessionData { + static D: OnceCell = OnceCell::new(); + D.get_or_init( + || + PossessionData { + weapon_data: Some(WeaponData { + uses_skill: SkillType::Fists, + raw_min_to_learn: 0.0, + raw_max_to_learn: 2.0, + normal_attack_start_messages: vec!( + Box::new(|attacker, victim, exp| + format!("{} bares {} teeth and lunges at {}", + &attacker.display_for_sentence(exp, 1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(exp, 1, false), + ) ) - ) - ), - normal_attack_success_messages: vec!( - Box::new(|attacker, victim, part, exp| - format!("{}'s teeth connect and tear at the flesh of {}'s {}", - &attacker.display_for_sentence(exp, 1, true), - &victim.display_for_sentence(exp, 1, false), - &part.display(victim.sex.clone()) + ), + normal_attack_success_messages: vec!( + Box::new(|attacker, victim, part, exp| + format!("{}'s teeth connect and tear at the flesh of {}'s {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) ) - ) - ), - ..Default::default() - }), - ..Default::default() - } + ), + ..Default::default() + }), + ..Default::default() + } + ) } diff --git a/blastmud_game/src/static_content/possession_type/head_armour.rs b/blastmud_game/src/static_content/possession_type/head_armour.rs new file mode 100644 index 0000000..b469290 --- /dev/null +++ b/blastmud_game/src/static_content/possession_type/head_armour.rs @@ -0,0 +1,36 @@ +use super::{PossessionData, PossessionType, WearData}; +use crate::static_content::species::BodyPart; +use once_cell::sync::OnceCell; + +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + ( + PossessionType::RustyMetalPot, + PossessionData { + display: "rusty metal pot", + details: "A metal pot that has rusted and is a bit dinged up - it looks like someone that way inclined could wear it as a serviceable helmet.", + aliases: vec!("pot", "rusted", "rusty"), + wear_data: Some(WearData { + covers_parts: vec!(BodyPart::Head), + thickness: 4.0, + }), + ..Default::default() + } + ), + ( + PossessionType::HockeyMask, + PossessionData { + display: "hockey mask", + details: "A white face-hugging fibreglass hockey mask, with small air holes across the face, but no specific hole for the mouth. It looks like it would give a degree of protection to the face, but it might also make someone look like a serial killer!", + aliases: vec!("mask"), + wear_data: Some(WearData { + covers_parts: vec!(BodyPart::Face), + thickness: 4.0, + }), + ..Default::default() + } + ) + )) +} diff --git a/blastmud_game/src/static_content/possession_type/lock.rs b/blastmud_game/src/static_content/possession_type/lock.rs index 31e8780..63650bb 100644 --- a/blastmud_game/src/static_content/possession_type/lock.rs +++ b/blastmud_game/src/static_content/possession_type/lock.rs @@ -1,4 +1,4 @@ -use super::{PossessionData, ArglessHandler}; +use super::{PossessionData, ArglessHandler, PossessionType}; use crate::{ models::item::{Item, LocationActionType}, message_handler::user_commands::{user_error, VerbContext, UResult}, @@ -10,6 +10,7 @@ use crate::{ capacity::{check_item_capacity, CapacityLevel}} }; use async_trait::async_trait; +use once_cell::sync::OnceCell; struct ScanLockLockcheck; #[async_trait] @@ -37,10 +38,10 @@ impl InstallHandler for ScanLockInstall { user_error("That scanlock is already in use.".to_owned())?; } - if ctx.trans.find_by_action_and_location( + if !ctx.trans.find_by_action_and_location( &room.refstr(), &LocationActionType::InstalledOnDoorAsLock(direction.clone()) - ).await?.is_some() { + ).await?.is_empty() { user_error("There's already a lock on that door - uninstall it first.".to_owned())?; } @@ -113,14 +114,20 @@ impl InstallHandler for ScanLockInstall { } -pub fn scan() -> PossessionData { - PossessionData { - display: "scanlock", - details: "A relatively basic lock with a fingerprint scanner built into it, made to ensure only the owner of something can enter or use it.", - aliases: vec!("lock"), - weight: LOCK_WEIGHT, - lockcheck_handler: Some(&ScanLockLockcheck), - install_handler: Some(&ScanLockInstall), - ..Default::default() - } +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + (PossessionType::Scanlock, + PossessionData { + display: "scanlock", + details: "A relatively basic lock with a fingerprint scanner built into it, made to ensure only the owner of something can enter or use it.", + aliases: vec!("lock"), + weight: LOCK_WEIGHT, + lockcheck_handler: Some(&ScanLockLockcheck), + install_handler: Some(&ScanLockInstall), + ..Default::default() + } + ) + )) } diff --git a/blastmud_game/src/static_content/possession_type/lower_armour.rs b/blastmud_game/src/static_content/possession_type/lower_armour.rs new file mode 100644 index 0000000..39079a3 --- /dev/null +++ b/blastmud_game/src/static_content/possession_type/lower_armour.rs @@ -0,0 +1,23 @@ +use super::{PossessionData, PossessionType, WearData}; +use crate::static_content::species::BodyPart; +use once_cell::sync::OnceCell; + +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + ( + PossessionType::LeatherPants, + PossessionData { + display: "pair of leather pants", + details: "Black leather pants that looks like they would protect you from falling off a motorbike, or maybe even offer some protection against certain weapons", + aliases: vec!("leather pants", "pants"), + wear_data: Some(WearData { + covers_parts: vec!(BodyPart::Groin, BodyPart::Legs), + thickness: 4.0, + }), + ..Default::default() + } + ), + )) +} diff --git a/blastmud_game/src/static_content/possession_type/meat.rs b/blastmud_game/src/static_content/possession_type/meat.rs index 2fdf1f9..9167f19 100644 --- a/blastmud_game/src/static_content/possession_type/meat.rs +++ b/blastmud_game/src/static_content/possession_type/meat.rs @@ -1,30 +1,37 @@ -use super::PossessionData; +use super::{PossessionData, PossessionType}; +use once_cell::sync::OnceCell; -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() - } -} - -pub fn steak_data() -> PossessionData { - PossessionData { - display: "steak", - details: "A hunk of raw red meat, dripping with blood", - weight: 100, - ..Default::default() - } -} - -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() - } +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + (PossessionType::AnimalSkin, + 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() + } + ), + ( + PossessionType::Steak, + PossessionData { + display: "steak", + details: "A hunk of raw red meat, dripping with blood", + weight: 100, + ..Default::default() + } + ), + ( + PossessionType::SeveredHead, + 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/possession_type/torso_armour.rs b/blastmud_game/src/static_content/possession_type/torso_armour.rs new file mode 100644 index 0000000..cab25f7 --- /dev/null +++ b/blastmud_game/src/static_content/possession_type/torso_armour.rs @@ -0,0 +1,25 @@ +use super::{PossessionData, PossessionType, WearData}; +use crate::static_content::species::BodyPart; +use once_cell::sync::OnceCell; + +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + ( + PossessionType::LeatherJacket, + PossessionData { + display: "leather jacket", + details: "A black leather jacket that looks like it would protect you from falling off a motorbike, or maybe even offer some protection against certain weapons", + aliases: vec!("jacket"), + wear_data: Some(WearData { + covers_parts: vec!(BodyPart::Arms, + BodyPart::Chest, + BodyPart::Back), + thickness: 4.0, + }), + ..Default::default() + } + ), + )) +} diff --git a/blastmud_game/src/static_content/possession_type/trauma_kit.rs b/blastmud_game/src/static_content/possession_type/trauma_kit.rs index 3364f2f..f4a276c 100644 --- a/blastmud_game/src/static_content/possession_type/trauma_kit.rs +++ b/blastmud_game/src/static_content/possession_type/trauma_kit.rs @@ -1,237 +1,244 @@ -use super::{PossessionData, UseData, UseEffect, ChargeData}; +use super::{PossessionData, PossessionType, UseData, UseEffect, ChargeData}; use crate::models::{ item::SkillType, consent::ConsentType, }; +use once_cell::sync::OnceCell; use super::PossessionType::*; -pub fn medium_data() -> PossessionData { - PossessionData { - display: "medium trauma kit", - details: "A collection of bandages and and small gadgets that look like they could, in the right hands, make an unhealthy person healthy again. It looks like when brand new, it could be used 5 times.", - aliases: vec!("trauma"), - charge_data: Some(ChargeData { - max_charges: 5, - charge_name_prefix: "treatment", - charge_name_suffix: "worth of supplies", - ..Default::default() - }), - use_data: Some(UseData { - uses_skill: SkillType::Medic, - diff_level: 10.0, - crit_fail_effects: vec!( - UseEffect::BroadcastMessage { - messagef: Box::new(|player, _item, target| ( - format!( - "{} attempts to heal {} with a trauma kit, but fucks it up badly\n", - &player.display_for_sentence(true, 1, true), - &if target.item_type == player.item_type && target.item_code == player.item_code { - player.pronouns.intensive.clone() - } else { - target.display_for_sentence(true, 1, false) - } - ), - format!("{} attempts to heal {} with a trauma kit, but messes it up badly\n", - &player.display_for_sentence(false, 1, true), - &if target.item_type == player.item_type && target.item_code == player.item_code { - player.pronouns.intensive.clone() - } else { +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + (MediumTraumaKit, + PossessionData { + display: "medium trauma kit", + details: "A collection of bandages and and small gadgets that look like they could, in the right hands, make an unhealthy person healthy again. It looks like when brand new, it could be used 5 times.", + aliases: vec!("trauma"), + charge_data: Some(ChargeData { + max_charges: 5, + charge_name_prefix: "treatment", + charge_name_suffix: "worth of supplies", + ..Default::default() + }), + use_data: Some(UseData { + uses_skill: SkillType::Medic, + diff_level: 10.0, + crit_fail_effects: vec!( + UseEffect::BroadcastMessage { + messagef: Box::new(|player, _item, target| ( + format!( + "{} attempts to heal {} with a trauma kit, but fucks it up badly\n", + &player.display_for_sentence(true, 1, true), + &if target.item_type == player.item_type && target.item_code == player.item_code { + player.pronouns.intensive.clone() + } else { + target.display_for_sentence(true, 1, false) + } + ), + format!("{} attempts to heal {} with a trauma kit, but messes it up badly\n", + &player.display_for_sentence(false, 1, true), + &if target.item_type == player.item_type && target.item_code == player.item_code { + player.pronouns.intensive.clone() + } else { + target.display_for_sentence(false, 1, false) + } + ))) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 0, base_effect: -2, skill_multiplier: -3.0, + max_effect: -5, + message: Box::new( + |target| + (format!( + "Fuck! The trauma kit makes {}'s condition worse", + target.display_for_sentence(true, 1, false)), + format!( + "The trauma kit makes {}'s condition worse", target.display_for_sentence(false, 1, false) - } - ))) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 0, base_effect: -2, skill_multiplier: -3.0, - max_effect: -5, - message: Box::new( - |target| - (format!( - "Fuck! The trauma kit makes {}'s condition worse", - target.display_for_sentence(true, 1, false)), - format!( - "The trauma kit makes {}'s condition worse", - target.display_for_sentence(false, 1, false) - ) - )) - } - ), - fail_effects: vec!( - UseEffect::BroadcastMessage { - messagef: Box::new(|player, _item, target| ( - format!( - "{} attempts unsuccessfully to heal {} with a trauma kit\n", - &player.display_for_sentence(true, 1, true), - &if target.item_type == player.item_type && target.item_code == player.item_code { - player.pronouns.intensive.clone() - } else { - target.display_for_sentence(true, 1, false) - } - ), - format!("{} attempts unsuccessfully to heal {} with a trauma kit\n", - &player.display_for_sentence(false, 1, true), - &if target.item_type == player.item_type && target.item_code == player.item_code { - player.pronouns.intensive.clone() - } else { - target.display_for_sentence(false, 1, false) - } - ))) - }, - ), - success_effects: vec!( - UseEffect::BroadcastMessage { - messagef: Box::new(|player, _item, target| ( - format!( - "{} expertly heals {} with a trauma kit\n", - &player.display_for_sentence(true, 1, true), - &if target.item_type == player.item_type && target.item_code == player.item_code { - player.pronouns.intensive.clone() - } else { - target.display_for_sentence(true, 1, false) - } - ), - format!("{} expertly heals {} with a trauma kit\n", - &player.display_for_sentence(false, 1, true), - &if target.item_type == player.item_type && target.item_code == player.item_code { - player.pronouns.intensive.clone() - } else { - target.display_for_sentence(false, 1, false) - } - ))) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 0, base_effect: 2, skill_multiplier: 8.0, - max_effect: 10, - message: Box::new( - |target| - (format!( - "FUUUCK! It hurts {}, but also starts to soothe {}", - target.display_for_sentence(true, 1, false), - &target.pronouns.object - ), - format!( - "It hurts {}, but also starts to soothe {}", - target.display_for_sentence(true, 1, false), - &target.pronouns.object - )) - ) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 10, base_effect: 2, skill_multiplier: 7.0, - max_effect: 9, - message: Box::new( - |target| - (format!( - "FUUUCK! It hurts {}, but also starts to soothe {}", - target.display_for_sentence(true, 1, false), - &target.pronouns.object - ), - format!( - "It hurts {}, but also starts to soothe {}", - target.display_for_sentence(true, 1, false), - &target.pronouns.object - )) - ) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 20, base_effect: 1, skill_multiplier: 6.0, - max_effect: 7, - message: Box::new( - |target| - (format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(true, 1, false), - ), - format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(false, 1, false), - )) - ) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 30, base_effect: 1, skill_multiplier: 5.0, - max_effect: 6, - message: Box::new( - |target| - (format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(true, 1, false), - ), - format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(false, 1, false), - )) - ) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 40, base_effect: 0, skill_multiplier: 4.0, - max_effect: 4, - message: Box::new( - |target| - (format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(true, 1, false), - ), - format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(false, 1, false), - )) - ) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 50, base_effect: 0, skill_multiplier: 3.0, - max_effect: 3, - message: Box::new( - |target| - (format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(true, 1, false), - ), - format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(false, 1, false), - )) - ) - }, - UseEffect::ChangeTargetHealth { - delay_secs: 60, base_effect: 0, skill_multiplier: 2.0, - max_effect: 2, - message: Box::new( - |target| - (format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(true, 1, false), - ), - format!( - "The bandages soothe {}'s wounds", - target.display_for_sentence(false, 1, false), - )) - ) - }, - ), - task_ref: "bandage", - errorf: Box::new( - |_item, target| - if target.death_data.is_some() { - Some(format!("It is too late, {}'s dead", target.pronouns.subject)) - } else if target.item_type != "player" && target.item_type != "npc" { - Some("It only works on animals.".to_owned()) - } else { - None - }), - needs_consent_check: Some(ConsentType::Medicine), - ..Default::default() - }), - becomes_on_spent: Some(EmptyMedicalBox), - ..Default::default() - } -} - -pub fn empty_data() -> PossessionData { - PossessionData { - display: "empty medical box", - details: "An empty box that looks like it once had something medical in it.", - aliases: vec!("box"), - ..Default::default() - } + ) + )) + } + ), + fail_effects: vec!( + UseEffect::BroadcastMessage { + messagef: Box::new(|player, _item, target| ( + format!( + "{} attempts unsuccessfully to heal {} with a trauma kit\n", + &player.display_for_sentence(true, 1, true), + &if target.item_type == player.item_type && target.item_code == player.item_code { + player.pronouns.intensive.clone() + } else { + target.display_for_sentence(true, 1, false) + } + ), + format!("{} attempts unsuccessfully to heal {} with a trauma kit\n", + &player.display_for_sentence(false, 1, true), + &if target.item_type == player.item_type && target.item_code == player.item_code { + player.pronouns.intensive.clone() + } else { + target.display_for_sentence(false, 1, false) + } + ))) + }, + ), + success_effects: vec!( + UseEffect::BroadcastMessage { + messagef: Box::new(|player, _item, target| ( + format!( + "{} expertly heals {} with a trauma kit\n", + &player.display_for_sentence(true, 1, true), + &if target.item_type == player.item_type && target.item_code == player.item_code { + player.pronouns.intensive.clone() + } else { + target.display_for_sentence(true, 1, false) + } + ), + format!("{} expertly heals {} with a trauma kit\n", + &player.display_for_sentence(false, 1, true), + &if target.item_type == player.item_type && target.item_code == player.item_code { + player.pronouns.intensive.clone() + } else { + target.display_for_sentence(false, 1, false) + } + ))) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 0, base_effect: 2, skill_multiplier: 8.0, + max_effect: 10, + message: Box::new( + |target| + (format!( + "FUUUCK! It hurts {}, but also starts to soothe {}", + target.display_for_sentence(true, 1, false), + &target.pronouns.object + ), + format!( + "It hurts {}, but also starts to soothe {}", + target.display_for_sentence(true, 1, false), + &target.pronouns.object + )) + ) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 10, base_effect: 2, skill_multiplier: 7.0, + max_effect: 9, + message: Box::new( + |target| + (format!( + "FUUUCK! It hurts {}, but also starts to soothe {}", + target.display_for_sentence(true, 1, false), + &target.pronouns.object + ), + format!( + "It hurts {}, but also starts to soothe {}", + target.display_for_sentence(true, 1, false), + &target.pronouns.object + )) + ) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 20, base_effect: 1, skill_multiplier: 6.0, + max_effect: 7, + message: Box::new( + |target| + (format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(true, 1, false), + ), + format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(false, 1, false), + )) + ) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 30, base_effect: 1, skill_multiplier: 5.0, + max_effect: 6, + message: Box::new( + |target| + (format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(true, 1, false), + ), + format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(false, 1, false), + )) + ) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 40, base_effect: 0, skill_multiplier: 4.0, + max_effect: 4, + message: Box::new( + |target| + (format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(true, 1, false), + ), + format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(false, 1, false), + )) + ) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 50, base_effect: 0, skill_multiplier: 3.0, + max_effect: 3, + message: Box::new( + |target| + (format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(true, 1, false), + ), + format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(false, 1, false), + )) + ) + }, + UseEffect::ChangeTargetHealth { + delay_secs: 60, base_effect: 0, skill_multiplier: 2.0, + max_effect: 2, + message: Box::new( + |target| + (format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(true, 1, false), + ), + format!( + "The bandages soothe {}'s wounds", + target.display_for_sentence(false, 1, false), + )) + ) + }, + ), + task_ref: "bandage", + errorf: Box::new( + |_item, target| + if target.death_data.is_some() { + Some(format!("It is too late, {}'s dead", target.pronouns.subject)) + } else if target.item_type != "player" && target.item_type != "npc" { + Some("It only works on animals.".to_owned()) + } else { + None + }), + needs_consent_check: Some(ConsentType::Medicine), + ..Default::default() + }), + becomes_on_spent: Some(EmptyMedicalBox), + ..Default::default() + } + ), + ( + EmptyMedicalBox, + PossessionData { + display: "empty medical box", + details: "An empty box that looks like it once had something medical in it.", + aliases: vec!("box"), + ..Default::default() + } + ) + )) } diff --git a/blastmud_game/src/static_content/possession_type/whip.rs b/blastmud_game/src/static_content/possession_type/whip.rs new file mode 100644 index 0000000..aed53f1 --- /dev/null +++ b/blastmud_game/src/static_content/possession_type/whip.rs @@ -0,0 +1,45 @@ +use super::{PossessionData, PossessionType, WeaponData}; +use crate::models::item::SkillType; +use once_cell::sync::OnceCell; + +pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = + OnceCell::new(); + &D.get_or_init(|| vec!( + (PossessionType::AntennaWhip, + PossessionData { + display: "antenna whip", + details: "A crudely fashioned whip made from a broken metal antenna. It looks a bit flimsy, but it \ + might do you until you get a better weapon!", + aliases: vec!("whip"), + weapon_data: Some(WeaponData { + uses_skill: SkillType::Whips, + raw_min_to_learn: 0.0, + raw_max_to_learn: 2.0, + normal_attack_start_messages: vec!( + Box::new(|attacker, victim, exp| + format!("{} lines up {} antenna whip for a strike on {}", + &attacker.display_for_sentence(exp, 1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(exp, 1, false), + ) + ) + ), + normal_attack_success_messages: vec!( + Box::new(|attacker, victim, part, exp| + format!("{}'s antenna whip scores a painful red line across {}'s {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + ) + ), + normal_attack_mean_damage: 3.0, + normal_attack_stdev_damage: 3.0, + ..Default::default() + }), + ..Default::default() + } + ) + )) +} diff --git a/blastmud_game/src/static_content/room/melbs.rs b/blastmud_game/src/static_content/room/melbs.rs index 10e0255..c09ddff 100644 --- a/blastmud_game/src/static_content/room/melbs.rs +++ b/blastmud_game/src/static_content/room/melbs.rs @@ -2309,7 +2309,6 @@ pub fn room_list() -> Vec { should_caption: false, ..Default::default() }, - Room { zone: "melbs", secondary_zones: vec!(), @@ -2320,6 +2319,10 @@ pub fn room_list() -> Vec { description_less_explicit: None, grid_coords: GridCoords { x: 2, y: 3, z: 0 }, exits: vec!( + Exit { + direction: Direction::NORTH, + ..Default::default() + }, Exit { direction: Direction::WEST, ..Default::default() @@ -2332,6 +2335,41 @@ pub fn room_list() -> Vec { should_caption: false, ..Default::default() }, + Room { + zone: "melbs", + secondary_zones: vec!(), + code: "melbs_riotready", + name: "Riot Ready", + short: ansi!("RR"), + description: ansi!("A small shop filled with shelves, racks, and hangers that seem to hold all kinds of safety and tactical defensive equipment that one might need to survive in a post-apocalyptic world. A weary looking middle aged lady stands on the display floor, herself clad in black tactical helmets and vests, making you wonder if it is to showcase the wares, or protect herself from looters. Across her right breast is a name tag reading \"Sharon\" with a logo featuring a smiley face below it. [Type list to see what's for sale]"), + description_less_explicit: None, + grid_coords: GridCoords { x: 2, y: 2, z: 0 }, + exits: vec!( + Exit { + direction: Direction::SOUTH, + ..Default::default() + }, + ), + should_caption: true, + stock_list: vec!( + RoomStock { + possession_type: PossessionType::HockeyMask, + list_price: 1000, + ..Default::default() + }, + RoomStock { + possession_type: PossessionType::LeatherJacket, + list_price: 500, + ..Default::default() + }, + RoomStock { + possession_type: PossessionType::LeatherPants, + list_price: 500, + ..Default::default() + }, + ), + ..Default::default() + }, Room { zone: "melbs", secondary_zones: vec!(), @@ -2407,7 +2445,12 @@ pub fn room_list() -> Vec { possession_type: PossessionType::ButcherKnife, list_price: 120, ..Default::default() - } + }, + RoomStock { + possession_type: PossessionType::RustyMetalPot, + list_price: 400, + ..Default::default() + }, ), should_caption: true, ..Default::default() diff --git a/blastmud_game/src/static_content/species.rs b/blastmud_game/src/static_content/species.rs index bdae2a2..28e012c 100644 --- a/blastmud_game/src/static_content/species.rs +++ b/blastmud_game/src/static_content/species.rs @@ -30,6 +30,7 @@ pub enum BodyPart { Back, Groin, Arms, + Hands, Legs, Feet } @@ -51,10 +52,29 @@ impl BodyPart { _ => "groin" }, Arms => "arms", + Hands => "hands", Legs => "legs", Feet => "feet" } } + + pub fn copula(&self, sex: Option) -> &'static str { + use BodyPart::*; + match self { + Head => "is", + Face => "is", + Chest => match sex { + Some(Sex::Female) => "are", + _ => "is", + }, + Back => "is", + Groin => "is", + Arms => "are", + Hands => "are", + Legs => "are", + Feet => "are" + } + } } pub struct SpeciesInfo { @@ -76,6 +96,7 @@ pub fn species_info_map() -> &'static BTreeMap { BodyPart::Back, BodyPart::Groin, BodyPart::Arms, + BodyPart::Hands, BodyPart::Legs, BodyPart::Feet ),