Allow buying, wearing and removing clothes.

Step 2 will be to make clothes serve a functional purpose as armour.
This commit is contained in:
Condorra 2023-05-23 20:37:27 +10:00
parent 79b0ed8540
commit 2747dddd90
25 changed files with 1127 additions and 435 deletions

View File

@ -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<serde_json::Value> = 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<Option<Arc<Item>>> {
if let Some(item) = self.pg_trans()?.query_opt(
pub async fn find_by_action_and_location(&self, location: &str, action_type: &LocationActionType) -> DResult<Vec<Arc<Item>>> {
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>(item.get("details"))?)));
}
Ok(None)
&serde_json::to_value(action_type)?]).await?
.into_iter()
.filter_map(
|row| match serde_json::from_value::<Item>(row.get("details")) {
Err(_) => None,
Ok(item) => Some(Arc::new(item))
}
).collect())
}
pub async fn list_consents(&self, consenting: &str) -> DResult<Vec<(String, ConsentType, Consent)>> {

View File

@ -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,

View File

@ -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 <bold>remove<reset> 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 <bold>remove<reset> 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())?,

View File

@ -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<Vec<&Arc<Item>>> = 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<String> = 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::<Vec<&'static str>>()),
&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 {}",

View File

@ -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))

View File

@ -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<time::Duration> {
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;

View File

@ -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<time::Duration> {
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;

View File

@ -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,

View File

@ -383,6 +383,7 @@ pub struct Item {
pub aliases: Vec<String>,
pub location: String, // Item reference as item_type/item_code.
pub action_type: LocationActionType,
pub action_type_started: Option<DateTime<Utc>>,
pub presence_target: Option<String>, // e.g. what are they sitting on.
pub is_static: bool,
pub death_data: Option<DeathData>,
@ -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,

View File

@ -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())
}

View File

@ -287,6 +287,7 @@ pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult<boo
player.total_xp -= lost_xp;
user.experience.xp_change_for_this_reroll -= lost_xp as i64;
player.health = max_health(&player);
player.active_climb = None;
trans.save_user_model(&user).await?;
Ok(true)
@ -354,7 +355,7 @@ pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) ->
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()) {

View File

@ -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<Box<dyn Fn(&Item, &Item, bool) -> String + 'static + Sync + Send>>;
pub type AttackMessageChoicePart = Vec<Box<dyn Fn(&Item, &Item, &BodyPart, bool) -> String + 'static + Sync + Send>>;
@ -115,6 +120,11 @@ impl Default for UseData {
}
}
pub struct WearData {
pub covers_parts: Vec<BodyPart>,
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<WearData>
}
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<Item> for PossessionType {
@ -267,23 +291,23 @@ pub fn fist() -> &'static WeaponData {
})
}
pub fn possession_data() -> &'static BTreeMap<PossessionType, PossessionData> {
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, PossessionData>> = OnceCell::new();
pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static PossessionData> {
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, &'static PossessionData>> = 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()
})
}

View File

@ -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()
}
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
})
))
}

View File

@ -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 <bold>use<reset> to establish a new corp. It rests on a clipboard with a pencil attached by a chain. There is a space to <bold>write<reset> on it [try <bold>write Blah on licence<reset> followed by <bold>sign licence<reset> 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<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
&D.get_or_init(|| vec!(
(NewCorpLicence,
PossessionData {
display: "new corp licence",
details: ansi!("A blank form that you can <bold>use<reset> to establish a new corp. It rests on a clipboard with a pencil attached by a chain. There is a space to <bold>write<reset> on it [try <bold>write Blah on licence<reset> followed by <bold>sign licence<reset> 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()
}
),
))
}

View File

@ -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<PossessionData> = 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()
}
)
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
)
))
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
)
))
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
),
))
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
),
))
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
),
))
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
)
))
}

View File

@ -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<Vec<(PossessionType, PossessionData)>> =
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()
}
)
))
}

View File

@ -2309,7 +2309,6 @@ pub fn room_list() -> Vec<Room> {
should_caption: false,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),
@ -2320,6 +2319,10 @@ pub fn room_list() -> Vec<Room> {
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<Room> {
should_caption: false,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_riotready",
name: "Riot Ready",
short: ansi!("<bggreen><red>RR<reset>"),
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 <bold>list<reset> 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<Room> {
possession_type: PossessionType::ButcherKnife,
list_price: 120,
..Default::default()
}
},
RoomStock {
possession_type: PossessionType::RustyMetalPot,
list_price: 400,
..Default::default()
},
),
should_caption: true,
..Default::default()

View File

@ -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<Sex>) -> &'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<SpeciesType, SpeciesInfo> {
BodyPart::Back,
BodyPart::Groin,
BodyPart::Arms,
BodyPart::Hands,
BodyPart::Legs,
BodyPart::Feet
),