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_active_players: bool,
pub include_all_players: bool, pub include_all_players: bool,
pub item_type_only: Option<&'l str>, pub item_type_only: Option<&'l str>,
pub item_action_type_only: Option<&'l LocationActionType>,
pub limit: u8, pub limit: u8,
pub dead_first: bool, pub dead_first: bool,
} }
@ -258,6 +259,7 @@ impl ItemSearchParams<'_> {
dead_first: false, dead_first: false,
limit: 100, limit: 100,
item_type_only: None, 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 { if search.include_contents {
ctes.push(format!("contents AS (\ ctes.push(format!("contents AS (\
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\ SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
@ -758,16 +773,20 @@ impl DBTrans {
Ok(()) Ok(())
} }
pub async fn find_by_action_and_location(&self, location: &str, action_type: &LocationActionType) -> DResult<Option<Arc<Item>>> { pub async fn find_by_action_and_location(&self, location: &str, action_type: &LocationActionType) -> DResult<Vec<Arc<Item>>> {
if let Some(item) = self.pg_trans()?.query_opt( Ok(self.pg_trans()?.query(
"SELECT details FROM items WHERE \ "SELECT details FROM items WHERE \
details->>'location' = $1 AND \ details->>'location' = $1 AND \
((details->'action_type')::TEXT = $2::JSONB::TEXT)", ((details->'action_type')::TEXT = $2::JSONB::TEXT) LIMIT 100",
&[&location, &[&location,
&serde_json::to_value(action_type)?]).await? { &serde_json::to_value(action_type)?]).await?
return Ok(Some(Arc::new(serde_json::from_value::<Item>(item.get("details"))?))); .into_iter()
.filter_map(
|row| match serde_json::from_value::<Item>(row.get("details")) {
Err(_) => None,
Ok(item) => Some(Arc::new(item))
} }
Ok(None) ).collect())
} }
pub async fn list_consents(&self, consenting: &str) -> DResult<Vec<(String, ConsentType, Consent)>> { pub async fn list_consents(&self, consenting: &str) -> DResult<Vec<(String, ConsentType, Consent)>> {

View File

@ -38,6 +38,7 @@ mod page;
pub mod parsing; pub mod parsing;
mod quit; mod quit;
pub mod register; pub mod register;
pub mod remove;
pub mod rent; pub mod rent;
pub mod say; pub mod say;
mod score; mod score;
@ -46,6 +47,7 @@ mod status;
mod uninstall; mod uninstall;
pub mod use_cmd; pub mod use_cmd;
mod vacate; mod vacate;
pub mod wear;
mod whisper; mod whisper;
mod who; mod who;
pub mod wield; pub mod wield;
@ -164,6 +166,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"repl" => page::VERB, "repl" => page::VERB,
"reply" => page::VERB, "reply" => page::VERB,
"remove" => remove::VERB,
"rent" => rent::VERB, "rent" => rent::VERB,
"\'" => say::VERB, "\'" => say::VERB,
@ -186,6 +189,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"whisper" => whisper::VERB, "whisper" => whisper::VERB,
"tell" => whisper::VERB, "tell" => whisper::VERB,
"wear" => wear::VERB,
"wield" => wield::VERB, "wield" => wield::VERB,
"who" => who::VERB, "who" => who::VERB,
"write" => write::VERB, "write" => write::VERB,

View File

@ -35,6 +35,7 @@ use crate::{
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::time; use std::time;
use ansi::ansi;
use chrono::Utc; use chrono::Utc;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; #[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", let msg_exp = format!("{} prepares to drop {}\n",
&player_item.display_for_sentence(true, 1, true), &player_item.display_for_sentence(true, 1, true),
&item.display_for_sentence(true, 1, false)); &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!", 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)))? &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)) { 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())?, 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}, room::{self, Direction},
dynzone::self, dynzone::self,
possession_type::possession_data, possession_type::possession_data,
species::{SpeciesType, species_info_map},
}, },
language, language,
services::combat::max_health, services::combat::max_health,
}; };
use itertools::Itertools; use itertools::Itertools;
use std::sync::Arc; use std::sync::Arc;
use std::collections::BTreeSet;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; #[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!("{}/{}", let mut items = ctx.trans.find_items_by_location(&format!("{}/{}",
item.item_type, item.item_code)).await?; 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 let all_groups: Vec<Vec<&Arc<Item>>> = items
.iter() .iter()
@ -52,14 +55,73 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
let head = &group_items[0]; let head = &group_items[0];
let mut details = head.display_for_sentence(!ctx.session_dat.less_explicit_mode, let mut details = head.display_for_sentence(!ctx.session_dat.less_explicit_mode,
group_items.len(), false); group_items.len(), false);
if head.action_type == LocationActionType::Wielded { match head.action_type {
details.push_str(" (wielded)"); LocationActionType::Wielded => details.push_str(" (wielded)"),
LocationActionType::Worn => continue,
_ => {}
} }
phrases.push(details); phrases.push(details);
} }
let phrases_str: Vec<&str> = phrases.iter().map(|p| p.as_str()).collect(); let phrases_str: Vec<&str> = phrases.iter().map(|p| p.as_str()).collect();
contents_desc.push_str(&(language::join_words(&phrases_str) + ".\n")); 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); let health_max = max_health(&item);
if health_max > 0 { if health_max > 0 {
let health_ratio = (item.health as f64) / (health_max as f64); let health_ratio = (item.health as f64) / (health_max as f64);
@ -195,7 +257,7 @@ async fn describe_door(
&state.description); &state.description);
if let Some(lock) = ctx.trans.find_by_action_and_location( if let Some(lock) = ctx.trans.find_by_action_and_location(
&room_item.refstr(), &room_item.refstr(),
&LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await? &LocationActionType::InstalledOnDoorAsLock((*direction).clone())).await?.first()
{ {
let lock_desc = lock.display_for_session(&ctx.session_dat); let lock_desc = lock.display_for_session(&ctx.session_dat);
msg.push_str(&format!(" The door is locked with {}", 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(revdir) = direction.reverse() {
if let Some(lock) = trans.find_by_action_and_location( if let Some(lock) = trans.find_by_action_and_location(
&entering_room_loc, &entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await? &LocationActionType::InstalledOnDoorAsLock(revdir.clone())).await?.first()
{ {
if let Some(ctx) = ctx_opt { if let Some(ctx) = ctx_opt {
if let Some(lockcheck) = lock.possession_type.as_ref() 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(revdir) = direction.reverse() {
if let Some(lock) = ctx.trans.find_by_action_and_location( if let Some(lock) = ctx.trans.find_by_action_and_location(
&entering_room_loc, &entering_room_loc,
&LocationActionType::InstalledOnDoorAsLock(revdir)).await? &LocationActionType::InstalledOnDoorAsLock(revdir)).await?.first()
{ {
if let Some(lockcheck) = lock.possession_type.as_ref() if let Some(lockcheck) = lock.possession_type.as_ref()
.and_then(|pt| possession_data().get(pt)) .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 chrono::{DateTime, Utc};
use itertools::Itertools; use itertools::Itertools;
#[derive(Serialize, Deserialize, PartialEq, Debug)] #[derive(Serialize, Deserialize, PartialEq, Debug, Clone)]
pub enum ConsentType { pub enum ConsentType {
Fight, Fight,
Medicine, Medicine,

View File

@ -383,6 +383,7 @@ pub struct Item {
pub aliases: Vec<String>, pub aliases: Vec<String>,
pub location: String, // Item reference as item_type/item_code. pub location: String, // Item reference as item_type/item_code.
pub action_type: LocationActionType, pub action_type: LocationActionType,
pub action_type_started: Option<DateTime<Utc>>,
pub presence_target: Option<String>, // e.g. what are they sitting on. pub presence_target: Option<String>, // e.g. what are they sitting on.
pub is_static: bool, pub is_static: bool,
pub death_data: Option<DeathData>, pub death_data: Option<DeathData>,
@ -468,6 +469,7 @@ impl Default for Item {
aliases: vec!(), aliases: vec!(),
location: "room/storage".to_owned(), location: "room/storage".to_owned(),
action_type: LocationActionType::Normal, action_type: LocationActionType::Normal,
action_type_started: None,
presence_target: None, presence_target: None,
is_static: false, is_static: false,
death_data: None, death_data: None,

View File

@ -14,16 +14,18 @@ use crate::message_handler::user_commands::{
VerbContext, VerbContext,
CommandHandlingError, CommandHandlingError,
UResult, UResult,
get,
drop,
movement,
use_cmd,
wield,
user_error,
get_user_or_fail,
open,
close, 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 crate::static_content::room::Direction;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -36,7 +38,9 @@ pub enum QueueCommand {
Get { possession_id: String }, Get { possession_id: String },
Movement { direction: Direction }, Movement { direction: Direction },
OpenDoor { direction: Direction }, OpenDoor { direction: Direction },
Remove { possession_id: String },
Use { possession_id: String, target_id: String }, Use { possession_id: String, target_id: String },
Wear { possession_id: String },
Wield { possession_id: String }, Wield { possession_id: String },
} }
impl QueueCommand { impl QueueCommand {
@ -49,7 +53,9 @@ impl QueueCommand {
Get {..} => "Get", Get {..} => "Get",
Movement {..} => "Movement", Movement {..} => "Movement",
OpenDoor {..} => "OpenDoor", OpenDoor {..} => "OpenDoor",
Remove {..} => "Remove",
Use {..} => "Use", Use {..} => "Use",
Wear {..} => "Wear",
Wield {..} => "Wield", 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)), ("Get", &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)), ("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
("OpenDoor", &open::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)), ("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)), ("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
).into_iter().collect()) ).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; player.total_xp -= lost_xp;
user.experience.xp_change_for_this_reroll -= lost_xp as i64; user.experience.xp_change_for_this_reroll -= lost_xp as i64;
player.health = max_health(&player); player.health = max_health(&player);
player.active_climb = None;
trans.save_user_model(&user).await?; trans.save_user_model(&user).await?;
Ok(true) 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> { async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> {
if let Some(item) = trans.find_by_action_and_location( 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() if let Some(dat) = item.possession_type.as_ref()
.and_then(|pt| possession_data().get(&pt)) .and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.weapon_data.as_ref()) { .and_then(|pd| pd.weapon_data.as_ref()) {

View File

@ -3,21 +3,26 @@ use crate::{
models::item::{SkillType, Item, Pronouns}, models::item::{SkillType, Item, Pronouns},
models::consent::ConsentType, models::consent::ConsentType,
message_handler::user_commands::{UResult, VerbContext}, message_handler::user_commands::{UResult, VerbContext},
static_content::room::Direction, static_content::{
room::Direction,
species::BodyPart,
},
}; };
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use super::species::BodyPart;
use async_trait::async_trait; use async_trait::async_trait;
mod fangs; mod fangs;
mod antenna_whip; mod whip;
mod blade; mod blade;
mod trauma_kit; mod trauma_kit;
mod corp_licence; mod corp_licence;
mod lock; mod lock;
mod meat; 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 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>>; 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] #[async_trait]
pub trait WriteHandler { pub trait WriteHandler {
async fn write_cmd(&self, ctx: &mut VerbContext, player: &Item, on_what: &Item, write_what: &str) -> UResult<()>; 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 sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
pub can_butcher: bool, pub can_butcher: bool,
pub wear_data: Option<WearData>
} }
impl Default for PossessionData { impl Default for PossessionData {
@ -169,6 +180,7 @@ impl Default for PossessionData {
sign_handler: None, sign_handler: None,
write_handler: None, write_handler: None,
can_butcher: false, can_butcher: false,
wear_data: None,
} }
} }
} }
@ -201,16 +213,28 @@ pub enum PossessionType {
// Special values that substitute for possessions. // Special values that substitute for possessions.
Fangs, // Default weapon for certain animals Fangs, // Default weapon for certain animals
// Real possessions from here on: // Real possessions from here on:
// Armour
RustyMetalPot,
HockeyMask,
LeatherJacket,
LeatherPants,
// Weapons: Whips
AntennaWhip, AntennaWhip,
// Weapons: Blades
ButcherKnife,
// Medical
MediumTraumaKit, MediumTraumaKit,
EmptyMedicalBox, EmptyMedicalBox,
// Corporate
NewCorpLicence, NewCorpLicence,
CertificateOfIncorporation, CertificateOfIncorporation,
// Security
Scanlock, Scanlock,
ButcherKnife, // Crafting
Steak, Steak,
AnimalSkin, AnimalSkin,
SeveredHead, SeveredHead,
} }
impl Into<Item> for PossessionType { impl Into<Item> for PossessionType {
@ -267,23 +291,23 @@ pub fn fist() -> &'static WeaponData {
}) })
} }
pub fn possession_data() -> &'static BTreeMap<PossessionType, PossessionData> { pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static PossessionData> {
static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, PossessionData>> = OnceCell::new(); static POSSESSION_DATA: OnceCell<BTreeMap<PossessionType, &'static PossessionData>> = OnceCell::new();
use PossessionType::*; use PossessionType::*;
&POSSESSION_DATA.get_or_init(|| { &POSSESSION_DATA.get_or_init(|| {
vec!( vec!(
(Fangs, fangs::data()), (Fangs, fangs::data())
(AntennaWhip, antenna_whip::data()), ).into_iter()
(ButcherKnife, blade::butcher_data()), .chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(MediumTraumaKit, trauma_kit::medium_data()), .chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(EmptyMedicalBox, trauma_kit::empty_data()), .chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(NewCorpLicence, corp_licence::data()), .chain(corp_licence::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(CertificateOfIncorporation, corp_licence::cert_data()), .chain(lock::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(Scanlock, lock::scan()), .chain(meat::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(Steak, meat::steak_data()), .chain(head_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(AnimalSkin, meat::skin_data()), .chain(torso_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
(SeveredHead, meat::severed_head_data()), .chain(lower_armour::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
).into_iter().collect() .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,7 +1,12 @@
use super::{PossessionData, WeaponData}; use super::{PossessionData, WeaponData, PossessionType};
use crate::models::item::SkillType; use crate::models::item::SkillType;
use once_cell::sync::OnceCell;
pub fn butcher_data() -> PossessionData { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::ButcherKnife,
PossessionData { PossessionData {
display: "butcher knife", 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.", 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.",
@ -35,5 +40,6 @@ pub fn butcher_data() -> PossessionData {
..Default::default() ..Default::default()
}), }),
..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::{ use crate::{
models::{ models::{
item::{Item, ItemSpecialData}, item::{Item, ItemSpecialData},
@ -9,6 +9,7 @@ use crate::{
parsing::parse_username, parsing::parse_username,
user_error, user_error,
UResult, UResult,
CommandHandlingError::UserError,
VerbContext, VerbContext,
}, },
services::comms::broadcast_to_room, services::comms::broadcast_to_room,
@ -16,6 +17,7 @@ use crate::{
use ansi::ansi; use ansi::ansi;
use async_trait::async_trait; use async_trait::async_trait;
use chrono::Utc; use chrono::Utc;
use once_cell::sync::OnceCell;
use super::PossessionType::*; use super::PossessionType::*;
@ -102,7 +104,8 @@ impl ArglessHandler for CorpLicenceHandler {
let mut what_mut = what.clone(); let mut what_mut = what.clone();
what_mut.possession_type = Some(CertificateOfIncorporation); 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.display = cp_data.display.to_owned();
what_mut.details = Some(cp_data.details.to_owned()); what_mut.details = Some(cp_data.details.to_owned());
ctx.trans.save_item_model(&what_mut).await?; ctx.trans.save_item_model(&what_mut).await?;
@ -113,7 +116,11 @@ impl ArglessHandler for CorpLicenceHandler {
static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {}; static CORP_LICENCE_HANDLER: CorpLicenceHandler = CorpLicenceHandler {};
pub fn data() -> PossessionData { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
&D.get_or_init(|| vec!(
(NewCorpLicence,
PossessionData { PossessionData {
display: "new corp licence", 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]"), 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]"),
@ -123,14 +130,15 @@ pub fn data() -> PossessionData {
write_handler: Some(&CORP_LICENCE_HANDLER), write_handler: Some(&CORP_LICENCE_HANDLER),
sign_handler: Some(&CORP_LICENCE_HANDLER), sign_handler: Some(&CORP_LICENCE_HANDLER),
..Default::default() ..Default::default()
} }),
} (
CertificateOfIncorporation,
pub fn cert_data() -> PossessionData {
PossessionData { PossessionData {
display: "certificate of incorporation", display: "certificate of incorporation",
details: "A certificate recording the formation of a corp.", details: "A certificate recording the formation of a corp.",
weight: 10, weight: 10,
..Default::default() ..Default::default()
} }
),
))
} }

View File

@ -1,7 +1,11 @@
use super::{PossessionData, WeaponData}; use super::{PossessionData, WeaponData};
use crate::models::item::SkillType; use crate::models::item::SkillType;
use once_cell::sync::OnceCell;
pub fn data() -> PossessionData { pub fn data() -> &'static PossessionData {
static D: OnceCell<PossessionData> = OnceCell::new();
D.get_or_init(
||
PossessionData { PossessionData {
weapon_data: Some(WeaponData { weapon_data: Some(WeaponData {
uses_skill: SkillType::Fists, uses_skill: SkillType::Fists,
@ -29,4 +33,5 @@ pub fn data() -> PossessionData {
}), }),
..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::{ use crate::{
models::item::{Item, LocationActionType}, models::item::{Item, LocationActionType},
message_handler::user_commands::{user_error, VerbContext, UResult}, message_handler::user_commands::{user_error, VerbContext, UResult},
@ -10,6 +10,7 @@ use crate::{
capacity::{check_item_capacity, CapacityLevel}} capacity::{check_item_capacity, CapacityLevel}}
}; };
use async_trait::async_trait; use async_trait::async_trait;
use once_cell::sync::OnceCell;
struct ScanLockLockcheck; struct ScanLockLockcheck;
#[async_trait] #[async_trait]
@ -37,10 +38,10 @@ impl InstallHandler for ScanLockInstall {
user_error("That scanlock is already in use.".to_owned())?; 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(), &room.refstr(),
&LocationActionType::InstalledOnDoorAsLock(direction.clone()) &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())?; user_error("There's already a lock on that door - uninstall it first.".to_owned())?;
} }
@ -113,7 +114,11 @@ impl InstallHandler for ScanLockInstall {
} }
pub fn scan() -> PossessionData { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::Scanlock,
PossessionData { PossessionData {
display: "scanlock", 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.", 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.",
@ -123,4 +128,6 @@ pub fn scan() -> PossessionData {
install_handler: Some(&ScanLockInstall), install_handler: Some(&ScanLockInstall),
..Default::default() ..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,6 +1,11 @@
use super::PossessionData; use super::{PossessionData, PossessionType};
use once_cell::sync::OnceCell;
pub fn skin_data() -> PossessionData { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
&D.get_or_init(|| vec!(
(PossessionType::AnimalSkin,
PossessionData { PossessionData {
display: "animal skin", display: "animal skin",
aliases: vec!("skin"), aliases: vec!("skin"),
@ -8,18 +13,18 @@ pub fn skin_data() -> PossessionData {
weight: 100, weight: 100,
..Default::default() ..Default::default()
} }
} ),
(
pub fn steak_data() -> PossessionData { PossessionType::Steak,
PossessionData { PossessionData {
display: "steak", display: "steak",
details: "A hunk of raw red meat, dripping with blood", details: "A hunk of raw red meat, dripping with blood",
weight: 100, weight: 100,
..Default::default() ..Default::default()
} }
} ),
(
pub fn severed_head_data() -> PossessionData { PossessionType::SeveredHead,
PossessionData { PossessionData {
display: "severed head", display: "severed head",
aliases: vec!("head"), aliases: vec!("head"),
@ -27,4 +32,6 @@ pub fn severed_head_data() -> PossessionData {
weight: 250, weight: 250,
..Default::default() ..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,12 +1,17 @@
use super::{PossessionData, UseData, UseEffect, ChargeData}; use super::{PossessionData, PossessionType, UseData, UseEffect, ChargeData};
use crate::models::{ use crate::models::{
item::SkillType, item::SkillType,
consent::ConsentType, consent::ConsentType,
}; };
use once_cell::sync::OnceCell;
use super::PossessionType::*; use super::PossessionType::*;
pub fn medium_data() -> PossessionData { pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
static D: OnceCell<Vec<(PossessionType, PossessionData)>> =
OnceCell::new();
&D.get_or_init(|| vec!(
(MediumTraumaKit,
PossessionData { PossessionData {
display: "medium trauma kit", 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.", 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.",
@ -225,13 +230,15 @@ pub fn medium_data() -> PossessionData {
becomes_on_spent: Some(EmptyMedicalBox), becomes_on_spent: Some(EmptyMedicalBox),
..Default::default() ..Default::default()
} }
} ),
(
pub fn empty_data() -> PossessionData { EmptyMedicalBox,
PossessionData { PossessionData {
display: "empty medical box", display: "empty medical box",
details: "An empty box that looks like it once had something medical in it.", details: "An empty box that looks like it once had something medical in it.",
aliases: vec!("box"), aliases: vec!("box"),
..Default::default() ..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, should_caption: false,
..Default::default() ..Default::default()
}, },
Room { Room {
zone: "melbs", zone: "melbs",
secondary_zones: vec!(), secondary_zones: vec!(),
@ -2320,6 +2319,10 @@ pub fn room_list() -> Vec<Room> {
description_less_explicit: None, description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 3, z: 0 }, grid_coords: GridCoords { x: 2, y: 3, z: 0 },
exits: vec!( exits: vec!(
Exit {
direction: Direction::NORTH,
..Default::default()
},
Exit { Exit {
direction: Direction::WEST, direction: Direction::WEST,
..Default::default() ..Default::default()
@ -2332,6 +2335,41 @@ pub fn room_list() -> Vec<Room> {
should_caption: false, should_caption: false,
..Default::default() ..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 { Room {
zone: "melbs", zone: "melbs",
secondary_zones: vec!(), secondary_zones: vec!(),
@ -2407,7 +2445,12 @@ pub fn room_list() -> Vec<Room> {
possession_type: PossessionType::ButcherKnife, possession_type: PossessionType::ButcherKnife,
list_price: 120, list_price: 120,
..Default::default() ..Default::default()
} },
RoomStock {
possession_type: PossessionType::RustyMetalPot,
list_price: 400,
..Default::default()
},
), ),
should_caption: true, should_caption: true,
..Default::default() ..Default::default()

View File

@ -30,6 +30,7 @@ pub enum BodyPart {
Back, Back,
Groin, Groin,
Arms, Arms,
Hands,
Legs, Legs,
Feet Feet
} }
@ -51,10 +52,29 @@ impl BodyPart {
_ => "groin" _ => "groin"
}, },
Arms => "arms", Arms => "arms",
Hands => "hands",
Legs => "legs", Legs => "legs",
Feet => "feet" 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 { pub struct SpeciesInfo {
@ -76,6 +96,7 @@ pub fn species_info_map() -> &'static BTreeMap<SpeciesType, SpeciesInfo> {
BodyPart::Back, BodyPart::Back,
BodyPart::Groin, BodyPart::Groin,
BodyPart::Arms, BodyPart::Arms,
BodyPart::Hands,
BodyPart::Legs, BodyPart::Legs,
BodyPart::Feet BodyPart::Feet
), ),