Implement craft on benches
Initially just a stove Also update Rust.
This commit is contained in:
parent
bfc1d4d4b5
commit
590d4640dd
1234
Cargo.lock
generated
1234
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -183,7 +183,6 @@ impl<'l> Iterator for AnsiIterator<'l> {
|
|||||||
}
|
}
|
||||||
_ => continue,
|
_ => continue,
|
||||||
}
|
}
|
||||||
drop(st);
|
|
||||||
return Some(AnsiEvent::<'l>(
|
return Some(AnsiEvent::<'l>(
|
||||||
AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]),
|
AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]),
|
||||||
self.state.clone(),
|
self.state.clone(),
|
||||||
|
@ -3,7 +3,7 @@ use crate::message_handler::ListenerSession;
|
|||||||
use crate::models::{
|
use crate::models::{
|
||||||
consent::{Consent, ConsentType},
|
consent::{Consent, ConsentType},
|
||||||
corp::{Corp, CorpCommType, CorpId, CorpMembership},
|
corp::{Corp, CorpCommType, CorpId, CorpMembership},
|
||||||
item::{Item, LocationActionType},
|
item::{Item, ItemFlag, LocationActionType},
|
||||||
session::Session,
|
session::Session,
|
||||||
task::{Task, TaskParse},
|
task::{Task, TaskParse},
|
||||||
user::User,
|
user::User,
|
||||||
@ -297,6 +297,7 @@ pub struct ItemSearchParams<'l> {
|
|||||||
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 item_action_type_only: Option<&'l LocationActionType>,
|
||||||
|
pub flagged_only: Option<ItemFlag>,
|
||||||
pub limit: u8,
|
pub limit: u8,
|
||||||
pub dead_first: bool,
|
pub dead_first: bool,
|
||||||
}
|
}
|
||||||
@ -314,6 +315,7 @@ impl ItemSearchParams<'_> {
|
|||||||
limit: 100,
|
limit: 100,
|
||||||
item_type_only: None,
|
item_type_only: None,
|
||||||
item_action_type_only: None,
|
item_action_type_only: None,
|
||||||
|
flagged_only: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -717,6 +719,33 @@ impl DBTrans {
|
|||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_items_by_location_possession_type_excluding<'a>(
|
||||||
|
self: &'a Self,
|
||||||
|
location: &'a str,
|
||||||
|
possession_type: &'a PossessionType,
|
||||||
|
exclude_codes: &'a Vec<&'a str>,
|
||||||
|
) -> DResult<Vec<Arc<Item>>> {
|
||||||
|
Ok(self
|
||||||
|
.pg_trans()?
|
||||||
|
.query(
|
||||||
|
"SELECT details FROM items WHERE details->>'location' = $1 AND \
|
||||||
|
details->'possession_type' = $2 AND NOT \
|
||||||
|
($3::JSONB @> (details->'item_code')) \
|
||||||
|
ORDER BY details->>'display' \
|
||||||
|
LIMIT 100",
|
||||||
|
&[
|
||||||
|
&location,
|
||||||
|
&serde_json::to_value(possession_type)?,
|
||||||
|
&serde_json::to_value(exclude_codes)?,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
|
||||||
|
.map(Arc::new)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn find_item_by_location_dynroom_code<'a>(
|
pub async fn find_item_by_location_dynroom_code<'a>(
|
||||||
self: &'a Self,
|
self: &'a Self,
|
||||||
location: &'a str,
|
location: &'a str,
|
||||||
@ -878,6 +907,16 @@ impl DBTrans {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let flagged_only_value: Option<serde_json::Value> = match search.flagged_only.as_ref() {
|
||||||
|
None => None,
|
||||||
|
Some(v) => Some(serde_json::to_value(v)?),
|
||||||
|
};
|
||||||
|
if let Some(flag) = flagged_only_value.as_ref() {
|
||||||
|
extra_where.push_str(&format!(" AND details->'flags' @> (${}::JSONB)", param_no));
|
||||||
|
param_no += 1;
|
||||||
|
params.push(flag);
|
||||||
|
}
|
||||||
|
|
||||||
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' = ${}\
|
||||||
@ -890,6 +929,7 @@ impl DBTrans {
|
|||||||
ctes.push(format!("loc_contents AS (\
|
ctes.push(format!("loc_contents AS (\
|
||||||
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
|
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
|
||||||
)", param_no));
|
)", param_no));
|
||||||
|
#[allow(dropping_copy_types)]
|
||||||
drop(param_no); // or increment if this is a problem.
|
drop(param_no); // or increment if this is a problem.
|
||||||
params.push(&player_loc);
|
params.push(&player_loc);
|
||||||
include_tables.push("SELECT details, aliases FROM loc_contents");
|
include_tables.push("SELECT details, aliases FROM loc_contents");
|
||||||
|
@ -41,6 +41,7 @@ mod list;
|
|||||||
pub mod load;
|
pub mod load;
|
||||||
mod login;
|
mod login;
|
||||||
mod look;
|
mod look;
|
||||||
|
pub mod make;
|
||||||
mod map;
|
mod map;
|
||||||
pub mod movement;
|
pub mod movement;
|
||||||
pub mod open;
|
pub mod open;
|
||||||
@ -188,6 +189,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
"gm" => map::VERB,
|
"gm" => map::VERB,
|
||||||
"gmap" => map::VERB,
|
"gmap" => map::VERB,
|
||||||
|
|
||||||
|
"make" => make::VERB,
|
||||||
"open" => open::VERB,
|
"open" => open::VERB,
|
||||||
|
|
||||||
"p" => page::VERB,
|
"p" => page::VERB,
|
||||||
|
@ -114,14 +114,12 @@ impl UserVerb for Verb {
|
|||||||
let user = user_mut(ctx)?;
|
let user = user_mut(ctx)?;
|
||||||
match user.terms.last_presented_term.as_ref() {
|
match user.terms.last_presented_term.as_ref() {
|
||||||
None => {
|
None => {
|
||||||
drop(user);
|
|
||||||
user_error("There was nothing pending your agreement.".to_owned())?;
|
user_error("There was nothing pending your agreement.".to_owned())?;
|
||||||
}
|
}
|
||||||
Some(last_term) => {
|
Some(last_term) => {
|
||||||
user.terms
|
user.terms
|
||||||
.accepted_terms
|
.accepted_terms
|
||||||
.insert(last_term.to_owned(), Utc::now());
|
.insert(last_term.to_owned(), Utc::now());
|
||||||
drop(user);
|
|
||||||
if check_and_notify_accepts(ctx).await? {
|
if check_and_notify_accepts(ctx).await? {
|
||||||
ctx.trans
|
ctx.trans
|
||||||
.queue_for_session(
|
.queue_for_session(
|
||||||
|
@ -263,7 +263,7 @@ fn compute_new_consent_state(
|
|||||||
if Some(&new_consent) == their_target_consent.as_ref() {
|
if Some(&new_consent) == their_target_consent.as_ref() {
|
||||||
match new_consent.fight_consent.as_mut() {
|
match new_consent.fight_consent.as_mut() {
|
||||||
None => (),
|
None => (),
|
||||||
Some(mut m) => {
|
Some(m) => {
|
||||||
m.pending_change = None;
|
m.pending_change = None;
|
||||||
m.status = ConsentStatus::Active;
|
m.status = ConsentStatus::Active;
|
||||||
}
|
}
|
||||||
@ -285,7 +285,7 @@ fn compute_new_consent_state(
|
|||||||
None => {
|
None => {
|
||||||
match new_consent.fight_consent.as_mut() {
|
match new_consent.fight_consent.as_mut() {
|
||||||
None => (),
|
None => (),
|
||||||
Some(mut m) => {
|
Some(m) => {
|
||||||
m.status = ConsentStatus::PendingAdd;
|
m.status = ConsentStatus::PendingAdd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +67,7 @@ impl UserVerb for Verb {
|
|||||||
.any(|al| al.starts_with(&match_item))
|
.any(|al| al.starts_with(&match_item))
|
||||||
{
|
{
|
||||||
if offset_remaining <= 1 {
|
if offset_remaining <= 1 {
|
||||||
if let Some(mut user) = ctx.user_dat.as_mut() {
|
if let Some(user) = ctx.user_dat.as_mut() {
|
||||||
if user.credits < stock.list_price {
|
if user.credits < stock.list_price {
|
||||||
user_error(
|
user_error(
|
||||||
"You don't have enough credits to buy that!".to_owned(),
|
"You don't have enough credits to buy that!".to_owned(),
|
||||||
@ -96,7 +96,14 @@ impl UserVerb for Verb {
|
|||||||
here already"
|
here already"
|
||||||
.to_owned(),
|
.to_owned(),
|
||||||
)?,
|
)?,
|
||||||
_ => &player_item.location,
|
_ => {
|
||||||
|
ctx.trans.queue_for_session(
|
||||||
|
&ctx.session,
|
||||||
|
Some(
|
||||||
|
"It's too much for you to carry so you leave it on the ground.\n")
|
||||||
|
).await?;
|
||||||
|
&player_item.location
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => &player_item_str,
|
_ => &player_item_str,
|
||||||
|
@ -108,7 +108,7 @@ pub async fn update_follow_for_failed_movement(
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn suspend_follow_for_independent_move(player: &mut Item) {
|
pub fn suspend_follow_for_independent_move(player: &mut Item) {
|
||||||
if let Some(mut following) = player.following.as_mut() {
|
if let Some(following) = player.following.as_mut() {
|
||||||
following.state = FollowState::IfSameRoom;
|
following.state = FollowState::IfSameRoom;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ use crate::{
|
|||||||
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
||||||
},
|
},
|
||||||
services::{
|
services::{
|
||||||
capacity::{check_item_capacity, CapacityLevel},
|
capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel},
|
||||||
comms::broadcast_to_room,
|
comms::broadcast_to_room,
|
||||||
},
|
},
|
||||||
static_content::possession_type::possession_data,
|
static_content::possession_type::possession_data,
|
||||||
@ -123,7 +123,7 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
|
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
let item = match ctx.command {
|
let (item, container_opt) = match ctx.command {
|
||||||
QueueCommand::Get { possession_id } => {
|
QueueCommand::Get { possession_id } => {
|
||||||
let item = match ctx
|
let item = match ctx
|
||||||
.trans
|
.trans
|
||||||
@ -158,7 +158,7 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
Some(&msg_nonexp),
|
Some(&msg_nonexp),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
item
|
(item, None)
|
||||||
}
|
}
|
||||||
QueueCommand::GetFromContainer {
|
QueueCommand::GetFromContainer {
|
||||||
from_possession_id,
|
from_possession_id,
|
||||||
@ -210,7 +210,7 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
Some(&msg_nonexp),
|
Some(&msg_nonexp),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
item
|
(item, Some(container))
|
||||||
}
|
}
|
||||||
_ => user_error("Unexpected command".to_owned())?,
|
_ => user_error("Unexpected command".to_owned())?,
|
||||||
};
|
};
|
||||||
@ -235,7 +235,7 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
user_error(format!(
|
user_error(format!(
|
||||||
"{} You can't get {} because it is too heavy!",
|
"{} You can't get {} because it is too heavy!",
|
||||||
if explicit { "Fuck!" } else { "Rats!" },
|
if explicit { "Fuck!" } else { "Rats!" },
|
||||||
&ctx.item.display_for_sentence(explicit, 1, false)
|
&item.display_for_sentence(explicit, 1, false)
|
||||||
))?
|
))?
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
@ -245,6 +245,10 @@ impl QueueCommandHandler for QueueHandler {
|
|||||||
item_mut.location = ctx.item.refstr();
|
item_mut.location = ctx.item.refstr();
|
||||||
item_mut.action_type = LocationActionType::Normal;
|
item_mut.action_type = LocationActionType::Normal;
|
||||||
ctx.trans.save_item_model(&item_mut).await?;
|
ctx.trans.save_item_model(&item_mut).await?;
|
||||||
|
|
||||||
|
if let Some(container) = container_opt {
|
||||||
|
recalculate_container_weight(&ctx.trans, &container).await?;
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -200,7 +200,7 @@ impl UserVerb for Verb {
|
|||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut user_mut = get_user_or_fail_mut(ctx)?;
|
let user_mut = get_user_or_fail_mut(ctx)?;
|
||||||
user_mut.credits -= hire_dat.price;
|
user_mut.credits -= hire_dat.price;
|
||||||
|
|
||||||
ctx.trans
|
ctx.trans
|
||||||
|
@ -2,7 +2,6 @@ use super::{get_player_item_or_fail, UResult, UserVerb, UserVerbRef, VerbContext
|
|||||||
use crate::{
|
use crate::{
|
||||||
language::weight,
|
language::weight,
|
||||||
models::item::{Item, LocationActionType},
|
models::item::{Item, LocationActionType},
|
||||||
static_content::possession_type::{possession_data, PossessionType},
|
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -46,28 +45,18 @@ impl UserVerb for Verb {
|
|||||||
if item.item_type != "possession" {
|
if item.item_type != "possession" {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(posdat) = possession_data().get(
|
let it_total = items.iter().map(|it| it.weight).sum();
|
||||||
&item
|
total += it_total;
|
||||||
.possession_type
|
response.push_str(&format!(
|
||||||
.as_ref()
|
"{} [{}]{}\n",
|
||||||
.unwrap_or(&PossessionType::AntennaWhip),
|
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, items.len(), true),
|
||||||
) {
|
weight(it_total),
|
||||||
total += items.len() as u64 * posdat.weight;
|
match item.action_type {
|
||||||
response.push_str(&format!(
|
LocationActionType::Worn => " (worn)",
|
||||||
"{} [{}]{}\n",
|
LocationActionType::Wielded => " (wielded)",
|
||||||
item.display_for_sentence(
|
_ => "",
|
||||||
!ctx.session_dat.less_explicit_mode,
|
}
|
||||||
items.len(),
|
));
|
||||||
true
|
|
||||||
),
|
|
||||||
weight(items.len() as u64 * posdat.weight),
|
|
||||||
match item.action_type {
|
|
||||||
LocationActionType::Worn => " (worn)",
|
|
||||||
LocationActionType::Wielded => " (wielded)",
|
|
||||||
_ => "",
|
|
||||||
}
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
response.push_str(&format!(
|
response.push_str(&format!(
|
||||||
"Total weight: {} ({} max)\n",
|
"Total weight: {} ({} max)\n",
|
||||||
|
@ -10,10 +10,10 @@ use crate::{
|
|||||||
db::ItemSearchParams,
|
db::ItemSearchParams,
|
||||||
language,
|
language,
|
||||||
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack},
|
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack},
|
||||||
services::combat::max_health,
|
services::{combat::max_health, skills::calc_level_gap},
|
||||||
static_content::{
|
static_content::{
|
||||||
dynzone,
|
dynzone,
|
||||||
possession_type::possession_data,
|
possession_type::{possession_data, recipe_craft_by_recipe},
|
||||||
room::{self, Direction},
|
room::{self, Direction},
|
||||||
species::{species_info_map, SpeciesType},
|
species::{species_info_map, SpeciesType},
|
||||||
},
|
},
|
||||||
@ -25,7 +25,11 @@ use mockall_double::double;
|
|||||||
use std::collections::BTreeSet;
|
use std::collections::BTreeSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> {
|
pub async fn describe_normal_item(
|
||||||
|
player_item: &Item,
|
||||||
|
ctx: &VerbContext<'_>,
|
||||||
|
item: &Item,
|
||||||
|
) -> UResult<()> {
|
||||||
let mut contents_desc = String::new();
|
let mut contents_desc = String::new();
|
||||||
|
|
||||||
let mut items = ctx
|
let mut items = ctx
|
||||||
@ -238,6 +242,72 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
|
|||||||
};
|
};
|
||||||
contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit));
|
contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(recipe_craft_data) = item
|
||||||
|
.possession_type
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|pt| recipe_craft_by_recipe().get(pt))
|
||||||
|
{
|
||||||
|
contents_desc.push_str("You will need:\n");
|
||||||
|
for (input_pt, count) in &recipe_craft_data.craft_data.inputs.iter().counts() {
|
||||||
|
if let Some(pd) = possession_data().get(&input_pt) {
|
||||||
|
let thing = if ctx.session_dat.less_explicit_mode {
|
||||||
|
pd.display_less_explicit.unwrap_or(pd.display)
|
||||||
|
} else {
|
||||||
|
pd.display
|
||||||
|
};
|
||||||
|
contents_desc.push_str(&format!(
|
||||||
|
" {} {}\n",
|
||||||
|
count,
|
||||||
|
&(if count != &1 {
|
||||||
|
language::pluralise(thing)
|
||||||
|
} else {
|
||||||
|
thing.to_owned()
|
||||||
|
})
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match recipe_craft_data.bench.as_ref() {
|
||||||
|
None => contents_desc.push_str("You can make this without any special bench.\n"),
|
||||||
|
Some(bench) => {
|
||||||
|
if let Some(pd) = possession_data().get(bench) {
|
||||||
|
contents_desc.push_str(&format!(
|
||||||
|
"You'll need to make this on a {}.\n",
|
||||||
|
if ctx.session_dat.less_explicit_mode {
|
||||||
|
pd.display_less_explicit.unwrap_or(pd.display)
|
||||||
|
} else {
|
||||||
|
pd.display
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let diff = calc_level_gap(
|
||||||
|
&player_item,
|
||||||
|
&recipe_craft_data.craft_data.skill,
|
||||||
|
recipe_craft_data.craft_data.difficulty,
|
||||||
|
);
|
||||||
|
let challenge_level = if diff > 5.0 {
|
||||||
|
"You are rather unlikely to succeed in making this."
|
||||||
|
} else if diff >= 4.0 {
|
||||||
|
"You're not that likely to succeed in making this, and you're likely to be too confused to learn anything making it."
|
||||||
|
} else if diff >= 3.0 {
|
||||||
|
"You've got about a 1/4 chance to succeed at making this, and you might learn something making it."
|
||||||
|
} else if diff >= 2.0 {
|
||||||
|
"You've got about a 1/3 chance to succeed at making this, and you might learn something making it."
|
||||||
|
} else if diff >= 0.0 {
|
||||||
|
"You've got a less than 50/50 chance to succeed at making this, and you'll probably learn a lot."
|
||||||
|
} else if diff >= -2.0 {
|
||||||
|
"You've got a better than 50/50 chance to succeed at making this, and you'll probably learn a lot."
|
||||||
|
} else if diff >= -3.0 {
|
||||||
|
"Three out of four times, you'll succeed at making this, and you might still learn something."
|
||||||
|
} else if diff >= -4.0 {
|
||||||
|
"Most of the time, you'll succeed at making this, but you'll only rarely learn something new."
|
||||||
|
} else {
|
||||||
|
"You're highly likely to succeed at making this, but unlikely to learn anything new."
|
||||||
|
};
|
||||||
|
contents_desc.push_str(&format!("{}\n", challenge_level));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.trans
|
ctx.trans
|
||||||
@ -568,7 +638,11 @@ impl UserVerb for Verb {
|
|||||||
) -> UResult<()> {
|
) -> UResult<()> {
|
||||||
let player_item = get_player_item_or_fail(ctx).await?;
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
|
|
||||||
let rem_trim = remaining.trim().to_lowercase();
|
let mut rem_trim = remaining.trim().to_lowercase();
|
||||||
|
let rem_orig = rem_trim.clone();
|
||||||
|
if rem_trim.starts_with("in ") {
|
||||||
|
rem_trim = rem_trim[3..].trim_start().to_owned();
|
||||||
|
}
|
||||||
let use_location = if player_item.death_data.is_some() {
|
let use_location = if player_item.death_data.is_some() {
|
||||||
"room/repro_xv_respawn"
|
"room/repro_xv_respawn"
|
||||||
} else {
|
} else {
|
||||||
@ -582,37 +656,58 @@ impl UserVerb for Verb {
|
|||||||
.find_item_by_type_code(heretype, herecode)
|
.find_item_by_type_code(heretype, herecode)
|
||||||
.await?
|
.await?
|
||||||
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
||||||
} else if let Some(dir) = Direction::parse(&rem_trim) {
|
} else if let Some(dir) =
|
||||||
match is_door_in_direction(&ctx.trans, &dir, use_location).await? {
|
Direction::parse(&rem_trim).or_else(|| Direction::parse(&rem_orig))
|
||||||
DoorSituation::NoDoor
|
{
|
||||||
| DoorSituation::DoorOutOfRoom {
|
// This is complex because "in" is overloaded, and if this fails, we want
|
||||||
|
// to also consider if they are looking in a container.
|
||||||
|
match is_door_in_direction(&ctx.trans, &dir, use_location).await {
|
||||||
|
Ok(DoorSituation::NoDoor)
|
||||||
|
| Ok(DoorSituation::DoorOutOfRoom {
|
||||||
state: DoorState { open: true, .. },
|
state: DoorState { open: true, .. },
|
||||||
..
|
..
|
||||||
}
|
})
|
||||||
| DoorSituation::DoorIntoRoom {
|
| Ok(DoorSituation::DoorIntoRoom {
|
||||||
state: DoorState { open: true, .. },
|
state: DoorState { open: true, .. },
|
||||||
..
|
..
|
||||||
} => {}
|
})
|
||||||
DoorSituation::DoorIntoRoom {
|
| Err(UserError(_)) => {}
|
||||||
|
Ok(DoorSituation::DoorIntoRoom {
|
||||||
state,
|
state,
|
||||||
room_with_door,
|
room_with_door,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
if let Some(rev_dir) = dir.reverse() {
|
if let Some(rev_dir) = dir.reverse() {
|
||||||
return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
|
return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DoorSituation::DoorOutOfRoom {
|
Ok(DoorSituation::DoorOutOfRoom {
|
||||||
state,
|
state,
|
||||||
room_with_door,
|
room_with_door,
|
||||||
..
|
..
|
||||||
} => {
|
}) => {
|
||||||
return describe_door(ctx, &room_with_door, &state, &dir).await;
|
return describe_door(ctx, &room_with_door, &state, &dir).await;
|
||||||
}
|
}
|
||||||
|
Err(e) => Err(e)?,
|
||||||
|
}
|
||||||
|
match direction_to_item(&ctx.trans, use_location, &dir).await {
|
||||||
|
Ok(Some(item)) => item,
|
||||||
|
Ok(None) | Err(UserError(_)) => search_item_for_user(
|
||||||
|
&ctx,
|
||||||
|
&ItemSearchParams {
|
||||||
|
include_contents: true,
|
||||||
|
include_loc_contents: true,
|
||||||
|
limit: 1,
|
||||||
|
..ItemSearchParams::base(&player_item, &rem_trim)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| match e {
|
||||||
|
UserError(_) => UserError("There's nothing in that direction".to_owned()),
|
||||||
|
e => e,
|
||||||
|
})?,
|
||||||
|
Err(e) => Err(e)?,
|
||||||
}
|
}
|
||||||
direction_to_item(&ctx.trans, use_location, &dir)
|
|
||||||
.await?
|
|
||||||
.ok_or_else(|| UserError("There's nothing in that direction".to_owned()))?
|
|
||||||
} else if rem_trim == "me" || rem_trim == "self" {
|
} else if rem_trim == "me" || rem_trim == "self" {
|
||||||
player_item.clone()
|
player_item.clone()
|
||||||
} else {
|
} else {
|
||||||
@ -652,7 +747,7 @@ impl UserVerb for Verb {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
describe_normal_item(ctx, &item).await?;
|
describe_normal_item(&player_item, ctx, &item).await?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
452
blastmud_game/src/message_handler/user_commands/make.rs
Normal file
452
blastmud_game/src/message_handler/user_commands/make.rs
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
use super::{
|
||||||
|
get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult,
|
||||||
|
UserError, UserVerb, UserVerbRef, VerbContext,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
models::item::{Item, ItemFlag},
|
||||||
|
regular_tasks::queued_command::{
|
||||||
|
queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
||||||
|
},
|
||||||
|
services::{
|
||||||
|
comms::broadcast_to_room,
|
||||||
|
destroy_container,
|
||||||
|
skills::{crit_fail_penalty_for_skill, skill_check_and_grind},
|
||||||
|
},
|
||||||
|
static_content::possession_type::{
|
||||||
|
possession_data, recipe_craft_by_recipe, CraftData, PossessionType,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::time;
|
||||||
|
use std::{collections::BTreeSet, sync::Arc};
|
||||||
|
|
||||||
|
// This is written this way for future expansion to dynamic recipes.
|
||||||
|
async fn get_craft_data_for_instructions<'l>(instructions: &'l Item) -> UResult<Option<CraftData>> {
|
||||||
|
// For now, only static recipes, so we just fetch them...
|
||||||
|
Ok(instructions
|
||||||
|
.possession_type
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|pt| recipe_craft_by_recipe().get(pt))
|
||||||
|
.map(|rcd| rcd.craft_data.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct QueueHandler;
|
||||||
|
#[async_trait]
|
||||||
|
impl QueueCommandHandler for QueueHandler {
|
||||||
|
async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<time::Duration> {
|
||||||
|
if ctx.item.death_data.is_some() {
|
||||||
|
user_error("The dead aren't very good at making stuff.".to_owned())?;
|
||||||
|
}
|
||||||
|
let (bench_id_opt, instructions_id) = match ctx.command {
|
||||||
|
QueueCommand::Make {
|
||||||
|
ref bench_possession_id,
|
||||||
|
ref instructions_possession_id,
|
||||||
|
..
|
||||||
|
} => (
|
||||||
|
bench_possession_id.as_ref().map(|s| s.as_str()),
|
||||||
|
instructions_possession_id,
|
||||||
|
),
|
||||||
|
_ => user_error("Unexpected command".to_owned())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (expected_location, bench_opt) = match bench_id_opt {
|
||||||
|
None => (ctx.item.location.clone(), None),
|
||||||
|
Some(bench_id) => {
|
||||||
|
let bench = ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code("possession", bench_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
UserError(
|
||||||
|
"Hmm, you can't find the equipment you were planning to use!"
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if bench.location != ctx.item.location {
|
||||||
|
user_error(
|
||||||
|
"Hmm, you can't find the equipment you were planning to use!".to_owned(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
(bench.refstr(), Some(bench))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let instructions = ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code("possession", instructions_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
UserError(
|
||||||
|
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if instructions.location != expected_location {
|
||||||
|
user_error(
|
||||||
|
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut msg_exp = format!(
|
||||||
|
"{} starts fiddling around trying to make something",
|
||||||
|
&ctx.item.display_for_sentence(true, 1, true)
|
||||||
|
);
|
||||||
|
let mut msg_nonexp = format!(
|
||||||
|
"{} starts fiddling around trying to make something",
|
||||||
|
&ctx.item.display_for_sentence(false, 1, true)
|
||||||
|
);
|
||||||
|
|
||||||
|
match bench_opt {
|
||||||
|
None => {}
|
||||||
|
Some(bench) => {
|
||||||
|
msg_exp.push_str(&format!(
|
||||||
|
" on {}",
|
||||||
|
bench.display_for_sentence(true, 1, false)
|
||||||
|
));
|
||||||
|
msg_nonexp.push_str(&format!(
|
||||||
|
" on {}",
|
||||||
|
bench.display_for_sentence(false, 1, false)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
msg_exp.push_str(".\n");
|
||||||
|
msg_nonexp.push_str(".\n");
|
||||||
|
|
||||||
|
broadcast_to_room(
|
||||||
|
&ctx.trans,
|
||||||
|
&ctx.item.location,
|
||||||
|
None,
|
||||||
|
&msg_exp,
|
||||||
|
Some(&msg_nonexp),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(time::Duration::from_secs(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> {
|
||||||
|
let (bench_id_opt, instructions_id, already_used) = match ctx.command {
|
||||||
|
QueueCommand::Make {
|
||||||
|
ref bench_possession_id,
|
||||||
|
ref instructions_possession_id,
|
||||||
|
ref already_used,
|
||||||
|
} => (
|
||||||
|
bench_possession_id.as_ref().map(|s| s.as_str()),
|
||||||
|
instructions_possession_id,
|
||||||
|
already_used,
|
||||||
|
),
|
||||||
|
_ => user_error("Unexpected command".to_owned())?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (expected_location, bench_opt) = match bench_id_opt {
|
||||||
|
None => (ctx.item.location.clone(), None),
|
||||||
|
Some(bench_id) => {
|
||||||
|
let bench = ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code("possession", bench_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
UserError(
|
||||||
|
"Hmm, you can't find the equipment you were planning to use!"
|
||||||
|
.to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if bench.location != ctx.item.location {
|
||||||
|
user_error(
|
||||||
|
"Hmm, you can't find the equipment you were planning to use!".to_owned(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
(bench.refstr(), Some(bench))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let instructions = ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code("possession", instructions_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
UserError(
|
||||||
|
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if instructions.location != expected_location {
|
||||||
|
user_error(
|
||||||
|
"Hmm, you can't find the instructions you were planning to follow!".to_owned(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(bench) = bench_opt.as_ref() {
|
||||||
|
if let Some(bench_data) = bench
|
||||||
|
.possession_type
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|pt| possession_data().get(pt))
|
||||||
|
.and_then(|pd| pd.bench_data)
|
||||||
|
{
|
||||||
|
bench_data
|
||||||
|
.check_make(&ctx.trans, bench, &instructions)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (on_what_exp, on_what_nonexp) = match bench_opt {
|
||||||
|
None => ("".to_owned(), "".to_owned()),
|
||||||
|
Some(bench) => (
|
||||||
|
format!(" on {}", bench.display_for_sentence(true, 1, false)),
|
||||||
|
format!(" on {}", bench.display_for_sentence(false, 1, false)),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
let craft_data = get_craft_data_for_instructions(&instructions)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| UserError("Looks like you can't make that anymore.".to_owned()))?;
|
||||||
|
let mut ingredients_left: Vec<PossessionType> = craft_data.inputs.clone();
|
||||||
|
|
||||||
|
let mut to_destroy_if_success: Vec<Arc<Item>> = Vec::new();
|
||||||
|
for item_id in already_used.iter() {
|
||||||
|
let item = ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code("possession", &item_id)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| UserError("Item used in crafting not found.".to_owned()))?;
|
||||||
|
to_destroy_if_success.push(item.clone());
|
||||||
|
let possession_type = item
|
||||||
|
.possession_type
|
||||||
|
.as_ref()
|
||||||
|
.ok_or_else(|| UserError("Item used in crafting not a possession.".to_owned()))?;
|
||||||
|
if let Some(match_pos) = ingredients_left.iter().position(|pt| pt == possession_type) {
|
||||||
|
ingredients_left.remove(match_pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let session = if ctx.item.item_type == "player" {
|
||||||
|
ctx.trans
|
||||||
|
.find_session_for_player(&ctx.item.item_code)
|
||||||
|
.await?
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
let explicit = session
|
||||||
|
.as_ref()
|
||||||
|
.map(|s| !s.1.less_explicit_mode)
|
||||||
|
.unwrap_or(false);
|
||||||
|
|
||||||
|
match ingredients_left.iter().next() {
|
||||||
|
None => {
|
||||||
|
for item in to_destroy_if_success {
|
||||||
|
destroy_container(&ctx.trans, &item).await?;
|
||||||
|
}
|
||||||
|
let mut new_item: Item = craft_data.output.clone().into();
|
||||||
|
new_item.item_code = ctx.trans.alloc_item_code().await?.to_string();
|
||||||
|
new_item.location = expected_location.clone();
|
||||||
|
ctx.trans.create_item(&new_item).await?;
|
||||||
|
broadcast_to_room(
|
||||||
|
&ctx.trans,
|
||||||
|
&ctx.item.location,
|
||||||
|
None,
|
||||||
|
&format!(
|
||||||
|
"{} makes a {}{}.\n",
|
||||||
|
&ctx.item.display_for_sentence(true, 1, true),
|
||||||
|
&new_item.display_for_sentence(true, 1, false),
|
||||||
|
&on_what_exp
|
||||||
|
),
|
||||||
|
Some(&format!(
|
||||||
|
"{} makes a {}{}.\n",
|
||||||
|
&ctx.item.display_for_sentence(false, 1, true),
|
||||||
|
&new_item.display_for_sentence(false, 1, false),
|
||||||
|
&on_what_nonexp
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
Some(possession_type) => {
|
||||||
|
let addable = ctx
|
||||||
|
.trans
|
||||||
|
.find_items_by_location_possession_type_excluding(
|
||||||
|
expected_location.as_str(),
|
||||||
|
possession_type,
|
||||||
|
&already_used.iter().map(|v| v.as_str()).collect(),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let pd = possession_data().get(&possession_type).ok_or_else(|| {
|
||||||
|
UserError(
|
||||||
|
"Looks like something needed to make that is something I know nothing about!".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match addable.iter().next() {
|
||||||
|
None => user_error(format!(
|
||||||
|
"You realise you'd need {}.",
|
||||||
|
if explicit {
|
||||||
|
pd.display
|
||||||
|
} else {
|
||||||
|
pd.display_less_explicit.unwrap_or(pd.display)
|
||||||
|
}
|
||||||
|
))?,
|
||||||
|
Some(item) => {
|
||||||
|
let skill_result = skill_check_and_grind(
|
||||||
|
&ctx.trans,
|
||||||
|
ctx.item,
|
||||||
|
&craft_data.skill,
|
||||||
|
craft_data.difficulty,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
if skill_result <= -0.5 {
|
||||||
|
crit_fail_penalty_for_skill(&ctx.trans, ctx.item, &craft_data.skill)
|
||||||
|
.await?;
|
||||||
|
ctx.trans
|
||||||
|
.delete_item(&item.item_type, &item.item_code)
|
||||||
|
.await?;
|
||||||
|
if let Some((sess, _)) = session {
|
||||||
|
ctx.trans
|
||||||
|
.queue_for_session(
|
||||||
|
&sess,
|
||||||
|
Some(&format!(
|
||||||
|
"You try adding {}, but it goes badly and you waste it.\n",
|
||||||
|
&item.display_for_sentence(explicit, 1, false)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
} else if skill_result <= 0.0 {
|
||||||
|
if let Some((sess, _)) = session {
|
||||||
|
ctx.trans
|
||||||
|
.queue_for_session(
|
||||||
|
&sess,
|
||||||
|
Some(&format!(
|
||||||
|
"You try and fail at adding {}.\n",
|
||||||
|
&item.display_for_sentence(explicit, 1, false)
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if let Some((sess, _)) = session {
|
||||||
|
ctx.trans
|
||||||
|
.queue_for_session(
|
||||||
|
&sess,
|
||||||
|
Some(&format!(
|
||||||
|
"You try adding {}.\n",
|
||||||
|
&item.display_for_sentence(explicit, 1, false),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
let mut new_already_used = (*already_used).clone();
|
||||||
|
new_already_used.insert(item.item_code.clone());
|
||||||
|
|
||||||
|
ctx.item.queue.push_front(QueueCommand::Make {
|
||||||
|
bench_possession_id: bench_id_opt.map(|id| id.to_owned()),
|
||||||
|
instructions_possession_id: instructions_id.to_string(),
|
||||||
|
already_used: new_already_used,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Verb;
|
||||||
|
#[async_trait]
|
||||||
|
impl UserVerb for Verb {
|
||||||
|
async fn handle(
|
||||||
|
self: &Self,
|
||||||
|
ctx: &mut VerbContext,
|
||||||
|
_verb: &str,
|
||||||
|
remaining: &str,
|
||||||
|
) -> UResult<()> {
|
||||||
|
let rtrim = remaining.trim();
|
||||||
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
|
|
||||||
|
if player_item.death_data.is_some() {
|
||||||
|
user_error("The dead aren't very good at making stuff.".to_owned())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (bench, output) = match rtrim.split_once(" on ") {
|
||||||
|
None => (None, rtrim),
|
||||||
|
Some((output_str, bench_str)) => {
|
||||||
|
let bench = search_item_for_user(
|
||||||
|
ctx,
|
||||||
|
&ItemSearchParams {
|
||||||
|
item_type_only: Some("possession"),
|
||||||
|
include_loc_contents: true,
|
||||||
|
..ItemSearchParams::base(&player_item, bench_str.trim())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
(Some(bench), output_str.trim())
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let instructions = search_item_for_user(
|
||||||
|
ctx,
|
||||||
|
&ItemSearchParams {
|
||||||
|
item_type_only: Some("possession"),
|
||||||
|
include_contents: true,
|
||||||
|
flagged_only: Some(ItemFlag::Instructions),
|
||||||
|
..ItemSearchParams::base(bench.as_ref().unwrap_or(&player_item), output.trim())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
let recipe_craft = instructions
|
||||||
|
.possession_type
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|pt| recipe_craft_by_recipe().get(&pt))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
UserError(
|
||||||
|
"Sorry, those instructions no longer seem to form part of the game!".to_owned(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
match (recipe_craft.bench.as_ref(), bench.as_ref()) {
|
||||||
|
(Some(bench_type), None) => user_error(format!(
|
||||||
|
"The {} can only be made on the {}.",
|
||||||
|
&instructions.display_for_session(&ctx.session_dat),
|
||||||
|
possession_data()
|
||||||
|
.get(bench_type)
|
||||||
|
.map(|pd| if ctx.session_dat.less_explicit_mode {
|
||||||
|
pd.display_less_explicit.unwrap_or(pd.display)
|
||||||
|
} else {
|
||||||
|
pd.display
|
||||||
|
})
|
||||||
|
.unwrap_or("bench")
|
||||||
|
))?,
|
||||||
|
(Some(bench_type), Some(bench))
|
||||||
|
if bench.possession_type.as_ref() != Some(bench_type) =>
|
||||||
|
{
|
||||||
|
user_error(format!(
|
||||||
|
"The {} can only be made on the {}.",
|
||||||
|
&instructions.display_for_session(&ctx.session_dat),
|
||||||
|
possession_data()
|
||||||
|
.get(bench_type)
|
||||||
|
.map(|pd| {
|
||||||
|
if ctx.session_dat.less_explicit_mode {
|
||||||
|
pd.display_less_explicit.unwrap_or(pd.display)
|
||||||
|
} else {
|
||||||
|
pd.display
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap_or("bench")
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
queue_command_and_save(
|
||||||
|
ctx,
|
||||||
|
&player_item,
|
||||||
|
&QueueCommand::Make {
|
||||||
|
bench_possession_id: bench.as_ref().map(|b| b.item_code.clone()),
|
||||||
|
instructions_possession_id: instructions.item_code.clone(),
|
||||||
|
already_used: BTreeSet::<String>::new(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static VERB_INT: Verb = Verb;
|
||||||
|
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|
@ -306,7 +306,7 @@ pub async fn handle_fall(trans: &DBTrans, faller: &mut Item, fall_dist: u64) ->
|
|||||||
// Returns false if the move failed.
|
// Returns false if the move failed.
|
||||||
async fn attempt_move_immediate(
|
async fn attempt_move_immediate(
|
||||||
direction: &Direction,
|
direction: &Direction,
|
||||||
mut ctx: &mut QueuedCommandContext<'_>,
|
ctx: &mut QueuedCommandContext<'_>,
|
||||||
source: &MovementSource,
|
source: &MovementSource,
|
||||||
) -> UResult<bool> {
|
) -> UResult<bool> {
|
||||||
let use_location = if ctx.item.death_data.is_some() {
|
let use_location = if ctx.item.death_data.is_some() {
|
||||||
|
@ -59,7 +59,7 @@ impl TaskHandler for SwingShutHandler {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut room_item_mut = (*room_item).clone();
|
let mut room_item_mut = (*room_item).clone();
|
||||||
let mut door_state = match room_item_mut
|
let door_state = match room_item_mut
|
||||||
.door_states
|
.door_states
|
||||||
.as_mut()
|
.as_mut()
|
||||||
.and_then(|ds| ds.get_mut(&direction))
|
.and_then(|ds| ds.get_mut(&direction))
|
||||||
|
@ -3,7 +3,7 @@ use super::{
|
|||||||
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
|
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
|
||||||
};
|
};
|
||||||
use crate::{
|
use crate::{
|
||||||
models::item::LocationActionType,
|
models::item::{ItemFlag, LocationActionType},
|
||||||
regular_tasks::queued_command::{
|
regular_tasks::queued_command::{
|
||||||
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
||||||
},
|
},
|
||||||
@ -215,7 +215,7 @@ impl UserVerb for Verb {
|
|||||||
remaining = remaining2;
|
remaining = remaining2;
|
||||||
}
|
}
|
||||||
|
|
||||||
let (search_what, for_what) = match remaining.split_once(" in ") {
|
let (into_what, for_what) = match remaining.split_once(" in ") {
|
||||||
None => {
|
None => {
|
||||||
user_error(ansi!("Try <bold>put<reset> item <bold>in<reset> container").to_owned())?
|
user_error(ansi!("Try <bold>put<reset> item <bold>in<reset> container").to_owned())?
|
||||||
}
|
}
|
||||||
@ -256,15 +256,58 @@ impl UserVerb for Verb {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|t| t.action_type.is_visible_in_look())
|
.filter(|t| t.action_type.is_visible_in_look())
|
||||||
{
|
{
|
||||||
|
if target.item_type == into_what.item_type && target.item_code == into_what.item_code {
|
||||||
|
user_error(
|
||||||
|
"You briefly ponder whether something can contain itself, but it blows your mind and you give up.".to_owned()
|
||||||
|
)?;
|
||||||
|
}
|
||||||
if target.item_type != "possession" {
|
if target.item_type != "possession" {
|
||||||
user_error("You can't put that in something!".to_owned())?;
|
user_error("You can't put that in something!".to_owned())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
did_anything = true;
|
did_anything = true;
|
||||||
|
|
||||||
|
if into_what.flags.contains(&ItemFlag::Bench) && target.flags.contains(&ItemFlag::Book)
|
||||||
|
{
|
||||||
|
let pages = ctx.trans.find_items_by_location(&target.refstr()).await?;
|
||||||
|
if !pages.is_empty() {
|
||||||
|
ctx.trans
|
||||||
|
.queue_for_session(&ctx.session,
|
||||||
|
Some(
|
||||||
|
&format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n",
|
||||||
|
&target.display_for_session(&ctx.session_dat),
|
||||||
|
&into_what.display_for_session(&ctx.session_dat)),
|
||||||
|
)
|
||||||
|
).await?;
|
||||||
|
for page in pages {
|
||||||
|
queue_command(
|
||||||
|
ctx,
|
||||||
|
&mut player_item_mut,
|
||||||
|
&QueueCommand::GetFromContainer {
|
||||||
|
from_possession_id: target.item_code.clone(),
|
||||||
|
get_possession_id: page.item_code.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
queue_command(
|
||||||
|
ctx,
|
||||||
|
&mut player_item_mut,
|
||||||
|
&QueueCommand::Put {
|
||||||
|
container_possession_id: into_what.item_code.clone(),
|
||||||
|
target_possession_id: page.item_code.clone(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
queue_command(
|
queue_command(
|
||||||
ctx,
|
ctx,
|
||||||
&mut player_item_mut,
|
&mut player_item_mut,
|
||||||
&QueueCommand::Put {
|
&QueueCommand::Put {
|
||||||
container_possession_id: search_what.item_code.clone(),
|
container_possession_id: into_what.item_code.clone(),
|
||||||
target_possession_id: target.item_code.clone(),
|
target_possession_id: target.item_code.clone(),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -261,6 +261,9 @@ pub enum ItemFlag {
|
|||||||
Hireable,
|
Hireable,
|
||||||
NPCsDontAttack,
|
NPCsDontAttack,
|
||||||
CanLoad,
|
CanLoad,
|
||||||
|
Bench,
|
||||||
|
Book,
|
||||||
|
Instructions,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
@ -2,7 +2,7 @@ use super::{TaskHandler, TaskRunContext};
|
|||||||
#[double]
|
#[double]
|
||||||
use crate::db::DBTrans;
|
use crate::db::DBTrans;
|
||||||
use crate::message_handler::user_commands::{
|
use crate::message_handler::user_commands::{
|
||||||
close, cut, drop, get, improvise, movement, open, put, remove, use_cmd, user_error, wear,
|
close, cut, drop, get, improvise, make, movement, open, put, remove, use_cmd, user_error, wear,
|
||||||
wield, CommandHandlingError, UResult, VerbContext,
|
wield, CommandHandlingError, UResult, VerbContext,
|
||||||
};
|
};
|
||||||
use crate::message_handler::ListenerSession;
|
use crate::message_handler::ListenerSession;
|
||||||
@ -67,6 +67,11 @@ pub enum QueueCommand {
|
|||||||
from_possession_id: String,
|
from_possession_id: String,
|
||||||
get_possession_id: String,
|
get_possession_id: String,
|
||||||
},
|
},
|
||||||
|
Make {
|
||||||
|
bench_possession_id: Option<String>,
|
||||||
|
instructions_possession_id: String,
|
||||||
|
already_used: BTreeSet<String>,
|
||||||
|
},
|
||||||
Movement {
|
Movement {
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
source: MovementSource,
|
source: MovementSource,
|
||||||
@ -109,6 +114,7 @@ impl QueueCommand {
|
|||||||
Drop { .. } => "Drop",
|
Drop { .. } => "Drop",
|
||||||
Get { .. } => "Get",
|
Get { .. } => "Get",
|
||||||
GetFromContainer { .. } => "GetFromContainer",
|
GetFromContainer { .. } => "GetFromContainer",
|
||||||
|
Make { .. } => "Make",
|
||||||
Movement { .. } => "Movement",
|
Movement { .. } => "Movement",
|
||||||
OpenDoor { .. } => "OpenDoor",
|
OpenDoor { .. } => "OpenDoor",
|
||||||
Put { .. } => "Put",
|
Put { .. } => "Put",
|
||||||
@ -181,6 +187,10 @@ fn queue_command_registry(
|
|||||||
"GetFromContainer",
|
"GetFromContainer",
|
||||||
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
"Make",
|
||||||
|
&make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||||
|
),
|
||||||
(
|
(
|
||||||
"Movement",
|
"Movement",
|
||||||
&movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
&movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
#[double]
|
#[double]
|
||||||
use crate::db::DBTrans;
|
use crate::db::DBTrans;
|
||||||
use crate::{
|
use crate::{
|
||||||
message_handler::user_commands::drop::consider_expire_job_for_item,
|
message_handler::user_commands::{drop::consider_expire_job_for_item, user_error, UResult},
|
||||||
models::consent::{Consent, ConsentStatus, ConsentType},
|
|
||||||
models::item::Item,
|
models::item::Item,
|
||||||
static_content::npc::npc_by_code,
|
models::{
|
||||||
|
consent::{Consent, ConsentStatus, ConsentType},
|
||||||
|
item::ItemSpecialData,
|
||||||
|
},
|
||||||
|
static_content::{
|
||||||
|
dynzone::{dynzone_by_type, DynzoneType},
|
||||||
|
npc::npc_by_code,
|
||||||
|
room::room_map_by_code,
|
||||||
|
},
|
||||||
DResult,
|
DResult,
|
||||||
};
|
};
|
||||||
use mockall_double::double;
|
use mockall_double::double;
|
||||||
@ -117,3 +124,28 @@ pub async fn destroy_container(trans: &DBTrans, container: &Item) -> DResult<()>
|
|||||||
.await?;
|
.await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn require_power(item: &Item) -> UResult<()> {
|
||||||
|
let result = match item.item_type.as_str() {
|
||||||
|
"room" => room_map_by_code()
|
||||||
|
.get(item.item_code.as_str())
|
||||||
|
.map(|r| r.has_power)
|
||||||
|
.unwrap_or(false),
|
||||||
|
"dynroom" => match &item.special_data {
|
||||||
|
Some(ItemSpecialData::DynroomData {
|
||||||
|
dynzone_code,
|
||||||
|
dynroom_code,
|
||||||
|
}) => DynzoneType::from_str(dynzone_code.as_str())
|
||||||
|
.and_then(|dzt| dynzone_by_type().get(&dzt))
|
||||||
|
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()))
|
||||||
|
.map(|dr| dr.has_power)
|
||||||
|
.unwrap_or(false),
|
||||||
|
_ => false,
|
||||||
|
},
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
if !result {
|
||||||
|
user_error("That would require power, and it looks like wireless power distribution is not available here.".to_owned())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
@ -54,7 +54,7 @@ pub async fn soak_damage<DamageDist: DamageDistribution>(
|
|||||||
let mut total_damage = 0.0;
|
let mut total_damage = 0.0;
|
||||||
|
|
||||||
for (damage_type, mut damage_amount) in &damage_by_type {
|
for (damage_type, mut damage_amount) in &damage_by_type {
|
||||||
for mut clothing in &mut clothes {
|
for clothing in &mut clothes {
|
||||||
if let Some(soak) = clothing
|
if let Some(soak) = clothing
|
||||||
.possession_type
|
.possession_type
|
||||||
.as_ref()
|
.as_ref()
|
||||||
|
@ -1,24 +1,32 @@
|
|||||||
use crate::{
|
#[double]
|
||||||
DResult,
|
use crate::db::DBTrans;
|
||||||
models::item::Item,
|
use crate::{models::item::Item, DResult};
|
||||||
};
|
|
||||||
use mockall_double::double;
|
use mockall_double::double;
|
||||||
#[double] use crate::db::DBTrans;
|
|
||||||
|
|
||||||
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
pub async fn broadcast_to_room(
|
||||||
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
trans: &DBTrans,
|
||||||
|
location: &str,
|
||||||
|
from_item: Option<&Item>,
|
||||||
|
message_explicit_ok: &str,
|
||||||
|
message_nonexplicit: Option<&str>,
|
||||||
|
) -> DResult<()> {
|
||||||
for item in trans.find_items_by_location(location).await? {
|
for item in trans.find_items_by_location(location).await? {
|
||||||
if item.item_type != "player" || item.death_data.is_some() {
|
if item.item_type != "player" || item.death_data.is_some() {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? {
|
if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await?
|
||||||
if session_dat.less_explicit_mode && Some(&item.item_code) != from_item.map(|i| &i.item_code) {
|
{
|
||||||
|
if session_dat.less_explicit_mode
|
||||||
|
&& Some(&item.item_code) != from_item.map(|i| &i.item_code)
|
||||||
|
{
|
||||||
if let Some(msg) = message_nonexplicit {
|
if let Some(msg) = message_nonexplicit {
|
||||||
trans.queue_for_session(&session, Some(msg)).await?;
|
trans.queue_for_session(&session, Some(msg)).await?;
|
||||||
}
|
}
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
trans.queue_for_session(&session, Some(message_explicit_ok)).await?;
|
trans
|
||||||
|
.queue_for_session(&session, Some(message_explicit_ok))
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1,41 +1,29 @@
|
|||||||
|
use super::{combat::change_health, comms::broadcast_to_room};
|
||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{
|
models::{
|
||||||
item::Item,
|
item::Item,
|
||||||
task::{
|
task::{Task, TaskDetails, TaskMeta},
|
||||||
Task,
|
|
||||||
TaskMeta,
|
|
||||||
TaskDetails,
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
regular_tasks::{TaskHandler, TaskRunContext},
|
||||||
|
static_content::possession_type::UseEffect,
|
||||||
DResult,
|
DResult,
|
||||||
static_content::{
|
|
||||||
possession_type::UseEffect,
|
|
||||||
},
|
|
||||||
regular_tasks::{
|
|
||||||
TaskHandler,
|
|
||||||
TaskRunContext,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
use super::{
|
|
||||||
comms::broadcast_to_room,
|
|
||||||
combat::change_health,
|
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use std::time;
|
|
||||||
use serde::{Serialize, Deserialize};
|
|
||||||
use std::collections::{BTreeMap, VecDeque};
|
|
||||||
use chrono::Utc;
|
use chrono::Utc;
|
||||||
use log::info;
|
use log::info;
|
||||||
use mockall_double::double;
|
use mockall_double::double;
|
||||||
#[double] use crate::db::DBTrans;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
|
use std::time;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub struct DelayedHealthEffect {
|
pub struct DelayedHealthEffect {
|
||||||
magnitude: i64,
|
magnitude: i64,
|
||||||
delay: u64,
|
delay: u64,
|
||||||
message: String,
|
message: String,
|
||||||
message_nonexp: String
|
message_nonexp: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DelayedHealthTaskHandler;
|
pub struct DelayedHealthTaskHandler;
|
||||||
@ -43,70 +31,109 @@ pub struct DelayedHealthTaskHandler;
|
|||||||
impl TaskHandler for DelayedHealthTaskHandler {
|
impl TaskHandler for DelayedHealthTaskHandler {
|
||||||
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
||||||
let ref mut item_effect_series = match &mut ctx.task.details {
|
let ref mut item_effect_series = match &mut ctx.task.details {
|
||||||
TaskDetails::DelayedHealth { item, ref mut effect_series } => (item, effect_series),
|
TaskDetails::DelayedHealth {
|
||||||
_ => Err("Expected DelayedHealth type")?
|
item,
|
||||||
|
ref mut effect_series,
|
||||||
|
} => (item, effect_series),
|
||||||
|
_ => Err("Expected DelayedHealth type")?,
|
||||||
};
|
};
|
||||||
let (item_type, item_code) = match item_effect_series.0.split_once("/") {
|
let (item_type, item_code) = match item_effect_series.0.split_once("/") {
|
||||||
None => {
|
None => {
|
||||||
info!("Invalid item {} to DelayedHealthTaskHandler", item_effect_series.0);
|
info!(
|
||||||
|
"Invalid item {} to DelayedHealthTaskHandler",
|
||||||
|
item_effect_series.0
|
||||||
|
);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Some((item_type, item_code)) => (item_type, item_code)
|
Some((item_type, item_code)) => (item_type, item_code),
|
||||||
};
|
};
|
||||||
let item = match ctx.trans.find_item_by_type_code(item_type, item_code).await? {
|
let item = match ctx
|
||||||
|
.trans
|
||||||
|
.find_item_by_type_code(item_type, item_code)
|
||||||
|
.await?
|
||||||
|
{
|
||||||
None => {
|
None => {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
Some(it) => it
|
Some(it) => it,
|
||||||
};
|
};
|
||||||
if item.death_data.is_some() {
|
if item.death_data.is_some() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
match item_effect_series.1.pop_front() {
|
match item_effect_series.1.pop_front() {
|
||||||
None => Ok(None),
|
None => Ok(None),
|
||||||
Some(DelayedHealthEffect { magnitude, message, message_nonexp, .. }) => {
|
Some(DelayedHealthEffect {
|
||||||
|
magnitude,
|
||||||
|
message,
|
||||||
|
message_nonexp,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
let mut item_mut = (*item).clone();
|
let mut item_mut = (*item).clone();
|
||||||
change_health(ctx.trans, magnitude, &mut item_mut, &message, &message_nonexp).await?;
|
change_health(
|
||||||
|
ctx.trans,
|
||||||
|
magnitude,
|
||||||
|
&mut item_mut,
|
||||||
|
&message,
|
||||||
|
&message_nonexp,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
ctx.trans.save_item_model(&item_mut).await?;
|
ctx.trans.save_item_model(&item_mut).await?;
|
||||||
Ok(item_effect_series.1.front().map(|it| time::Duration::from_secs(it.delay)))
|
Ok(item_effect_series
|
||||||
|
.1
|
||||||
|
.front()
|
||||||
|
.map(|it| time::Duration::from_secs(it.delay)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub static DELAYED_HEALTH_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DelayedHealthTaskHandler;
|
pub static DELAYED_HEALTH_HANDLER: &'static (dyn TaskHandler + Sync + Send) =
|
||||||
|
&DelayedHealthTaskHandler;
|
||||||
|
|
||||||
pub async fn run_effects(
|
pub async fn run_effects(
|
||||||
trans: &DBTrans, effects: &Vec<UseEffect>,
|
trans: &DBTrans,
|
||||||
|
effects: &Vec<UseEffect>,
|
||||||
player: &mut Item,
|
player: &mut Item,
|
||||||
item: &Item,
|
item: &Item,
|
||||||
// None if target is player
|
// None if target is player
|
||||||
target: &mut Option<Item>,
|
target: &mut Option<Item>,
|
||||||
level: f64,
|
level: f64,
|
||||||
task_ref: &str
|
task_ref: &str,
|
||||||
) -> DResult<()> {
|
) -> DResult<()> {
|
||||||
let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::new();
|
let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::new();
|
||||||
for effect in effects {
|
for effect in effects {
|
||||||
match effect {
|
match effect {
|
||||||
UseEffect::BroadcastMessage { messagef } => {
|
UseEffect::BroadcastMessage { messagef } => {
|
||||||
let (msg_exp, msg_nonexp) = messagef(player, item, target.as_ref().unwrap_or(player));
|
let (msg_exp, msg_nonexp) =
|
||||||
broadcast_to_room(trans, &player.location, None, &msg_exp,
|
messagef(player, item, target.as_ref().unwrap_or(player));
|
||||||
Some(&msg_nonexp)).await?;
|
broadcast_to_room(trans, &player.location, None, &msg_exp, Some(&msg_nonexp))
|
||||||
},
|
.await?;
|
||||||
UseEffect::ChangeTargetHealth { delay_secs, base_effect, skill_multiplier, max_effect,
|
}
|
||||||
message } => {
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs,
|
||||||
|
base_effect,
|
||||||
|
skill_multiplier,
|
||||||
|
max_effect,
|
||||||
|
message,
|
||||||
|
} => {
|
||||||
let health_impact =
|
let health_impact =
|
||||||
(*base_effect + ((skill_multiplier * level) as i64).min(*max_effect)) as i64;
|
(*base_effect + ((skill_multiplier * level) as i64).min(*max_effect)) as i64;
|
||||||
let (msg, msg_nonexp) = message(target.as_ref().unwrap_or(player));
|
let (msg, msg_nonexp) = message(target.as_ref().unwrap_or(player));
|
||||||
if *delay_secs == 0 {
|
if *delay_secs == 0 {
|
||||||
change_health(trans, health_impact, target.as_mut().unwrap_or(player), &msg,
|
change_health(
|
||||||
&msg_nonexp).await?;
|
trans,
|
||||||
|
health_impact,
|
||||||
|
target.as_mut().unwrap_or(player),
|
||||||
|
&msg,
|
||||||
|
&msg_nonexp,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
} else {
|
} else {
|
||||||
let target_it = target.as_ref().unwrap_or(player);
|
let target_it = target.as_ref().unwrap_or(player);
|
||||||
let fx = DelayedHealthEffect {
|
let fx = DelayedHealthEffect {
|
||||||
magnitude: health_impact,
|
magnitude: health_impact,
|
||||||
delay: *delay_secs,
|
delay: *delay_secs,
|
||||||
message: msg,
|
message: msg,
|
||||||
message_nonexp: msg_nonexp
|
message_nonexp: msg_nonexp,
|
||||||
};
|
};
|
||||||
target_health_series
|
target_health_series
|
||||||
.entry(format!("{}/{}", target_it.item_type, target_it.item_code))
|
.entry(format!("{}/{}", target_it.item_type, target_it.item_code))
|
||||||
@ -118,17 +145,19 @@ pub async fn run_effects(
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (eff_item, l) in target_health_series.into_iter() {
|
for (eff_item, l) in target_health_series.into_iter() {
|
||||||
trans.upsert_task(&Task {
|
trans
|
||||||
meta: TaskMeta {
|
.upsert_task(&Task {
|
||||||
task_code: format!("{}/{}", eff_item, task_ref),
|
meta: TaskMeta {
|
||||||
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
|
task_code: format!("{}/{}", eff_item, task_ref),
|
||||||
..Default::default()
|
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
|
||||||
},
|
..Default::default()
|
||||||
details: TaskDetails::DelayedHealth {
|
},
|
||||||
effect_series: l,
|
details: TaskDetails::DelayedHealth {
|
||||||
item: eff_item,
|
effect_series: l,
|
||||||
}
|
item: eff_item,
|
||||||
}).await?;
|
},
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -388,7 +388,7 @@ pub async fn skill_check_and_grind(
|
|||||||
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
|
// If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
|
||||||
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
||||||
if who.item_type == "player"
|
if who.item_type == "player"
|
||||||
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp()
|
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap.abs() as f64)).exp()
|
||||||
{
|
{
|
||||||
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
|
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
|
||||||
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
|
if let Some(mut user) = trans.find_by_username(&who.item_code).await? {
|
||||||
|
@ -3,32 +3,33 @@
|
|||||||
// dynamically. They can dynamically connect to the grid.
|
// dynamically. They can dynamically connect to the grid.
|
||||||
// Apartments, planes, and boats are all expected to be specific instances of dynzones.
|
// Apartments, planes, and boats are all expected to be specific instances of dynzones.
|
||||||
use super::room::{Direction, GridCoords};
|
use super::room::{Direction, GridCoords};
|
||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
use crate::{
|
use crate::{
|
||||||
message_handler::user_commands::{user_error, UResult},
|
message_handler::user_commands::{user_error, UResult},
|
||||||
models::item::{Item, ItemFlag, ItemSpecialData, DynamicEntrance, DoorState}
|
models::item::{DoorState, DynamicEntrance, Item, ItemFlag, ItemSpecialData},
|
||||||
};
|
};
|
||||||
|
use mockall_double::double;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use mockall_double::double;
|
|
||||||
#[double] use crate::db::DBTrans;
|
|
||||||
|
|
||||||
mod cokmurl_apartment;
|
mod cokmurl_apartment;
|
||||||
|
|
||||||
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
|
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
|
||||||
pub enum DynzoneType {
|
pub enum DynzoneType {
|
||||||
CokMurlApartment
|
CokMurlApartment,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DynzoneType {
|
impl DynzoneType {
|
||||||
pub fn from_str(i: &str) -> Option<Self> {
|
pub fn from_str(i: &str) -> Option<Self> {
|
||||||
match i {
|
match i {
|
||||||
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
|
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
|
||||||
_ => None
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn to_str(&self) -> &'static str {
|
pub fn to_str(&self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
DynzoneType::CokMurlApartment => "CokMurlApartment"
|
DynzoneType::CokMurlApartment => "CokMurlApartment",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,43 +44,51 @@ pub struct Dynzone {
|
|||||||
|
|
||||||
impl Dynzone {
|
impl Dynzone {
|
||||||
// Returns None if there is already an instance in the same exit direction.
|
// Returns None if there is already an instance in the same exit direction.
|
||||||
pub async fn create_instance(&self, trans: &DBTrans, connect_where: &str, dup_message: &str,
|
pub async fn create_instance(
|
||||||
new_owner: &Item, new_exit_direction: &Direction) -> UResult<String> {
|
&self,
|
||||||
|
trans: &DBTrans,
|
||||||
|
connect_where: &str,
|
||||||
|
dup_message: &str,
|
||||||
|
new_owner: &Item,
|
||||||
|
new_exit_direction: &Direction,
|
||||||
|
) -> UResult<String> {
|
||||||
// Check exit not taken...
|
// Check exit not taken...
|
||||||
if trans.find_exact_dyn_exit(connect_where, new_exit_direction).await?.is_some() {
|
if trans
|
||||||
|
.find_exact_dyn_exit(connect_where, new_exit_direction)
|
||||||
|
.await?
|
||||||
|
.is_some()
|
||||||
|
{
|
||||||
user_error(dup_message.to_string())?;
|
user_error(dup_message.to_string())?;
|
||||||
}
|
}
|
||||||
let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code);
|
let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code);
|
||||||
|
|
||||||
let code = format!("{}", &trans.alloc_item_code().await?);
|
let code = format!("{}", &trans.alloc_item_code().await?);
|
||||||
trans.create_item(
|
trans
|
||||||
&Item {
|
.create_item(&Item {
|
||||||
item_type: "dynzone".to_owned(),
|
item_type: "dynzone".to_owned(),
|
||||||
item_code: code.clone(),
|
item_code: code.clone(),
|
||||||
display: self.zonename.to_owned(),
|
display: self.zonename.to_owned(),
|
||||||
special_data: Some(
|
special_data: Some(ItemSpecialData::DynzoneData {
|
||||||
ItemSpecialData::DynzoneData {
|
zone_exit: Some(connect_where.to_owned()),
|
||||||
zone_exit: Some(connect_where.to_owned()),
|
vacate_after: None,
|
||||||
vacate_after: None
|
}),
|
||||||
}
|
|
||||||
),
|
|
||||||
owner: Some(owner.clone()),
|
owner: Some(owner.clone()),
|
||||||
location: format!("dynzone/{}", &code),
|
location: format!("dynzone/{}", &code),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
})
|
||||||
).await?;
|
.await?;
|
||||||
|
|
||||||
let mut should_connect = true;
|
let mut should_connect = true;
|
||||||
for (_, room) in &self.dyn_rooms {
|
for (_, room) in &self.dyn_rooms {
|
||||||
let roomcode = format!("{}/{}", &code, room.subcode);
|
let roomcode = format!("{}/{}", &code, room.subcode);
|
||||||
let will_connect = should_connect &&
|
let will_connect = should_connect
|
||||||
room.exits.iter().any(|r| match r.target {
|
&& room.exits.iter().any(|r| match r.target {
|
||||||
ExitTarget::ExitZone => true,
|
ExitTarget::ExitZone => true,
|
||||||
_ => false
|
_ => false,
|
||||||
});
|
});
|
||||||
should_connect &= !will_connect;
|
should_connect &= !will_connect;
|
||||||
trans.create_item(
|
trans
|
||||||
&Item {
|
.create_item(&Item {
|
||||||
item_type: "dynroom".to_owned(),
|
item_type: "dynroom".to_owned(),
|
||||||
item_code: roomcode,
|
item_code: roomcode,
|
||||||
display: room.name.to_owned(),
|
display: room.name.to_owned(),
|
||||||
@ -88,29 +97,39 @@ impl Dynzone {
|
|||||||
location: format!("dynzone/{}", &code),
|
location: format!("dynzone/{}", &code),
|
||||||
special_data: Some(ItemSpecialData::DynroomData {
|
special_data: Some(ItemSpecialData::DynroomData {
|
||||||
dynzone_code: self.zonetype.to_str().to_owned(),
|
dynzone_code: self.zonetype.to_str().to_owned(),
|
||||||
dynroom_code: room.subcode.to_owned()
|
dynroom_code: room.subcode.to_owned(),
|
||||||
}),
|
}),
|
||||||
dynamic_entrance: if will_connect {
|
dynamic_entrance: if will_connect {
|
||||||
Some(DynamicEntrance {
|
Some(DynamicEntrance {
|
||||||
direction: new_exit_direction.clone(),
|
direction: new_exit_direction.clone(),
|
||||||
source_item: connect_where.to_owned()
|
source_item: connect_where.to_owned(),
|
||||||
})
|
})
|
||||||
} else { None },
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
flags: room.item_flags.clone(),
|
flags: room.item_flags.clone(),
|
||||||
owner: Some(owner.clone()),
|
owner: Some(owner.clone()),
|
||||||
door_states: Some(room.exits.iter()
|
door_states: Some(
|
||||||
.filter_map(|ex|
|
room.exits
|
||||||
if let ExitType::Doored { description } = ex.exit_type {
|
.iter()
|
||||||
Some((ex.direction.clone(), DoorState {
|
.filter_map(|ex| {
|
||||||
open: false,
|
if let ExitType::Doored { description } = ex.exit_type {
|
||||||
description: description.to_owned()
|
Some((
|
||||||
}))
|
ex.direction.clone(),
|
||||||
} else {
|
DoorState {
|
||||||
None
|
open: false,
|
||||||
}).collect()),
|
description: description.to_owned(),
|
||||||
|
},
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
})
|
||||||
).await?;
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(format!("dynzone/{}", &code))
|
Ok(format!("dynzone/{}", &code))
|
||||||
@ -161,6 +180,7 @@ pub struct Dynroom {
|
|||||||
pub should_caption: bool,
|
pub should_caption: bool,
|
||||||
pub item_flags: Vec<ItemFlag>,
|
pub item_flags: Vec<ItemFlag>,
|
||||||
pub grid_coords: GridCoords,
|
pub grid_coords: GridCoords,
|
||||||
|
pub has_power: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Dynroom {
|
impl Default for Dynroom {
|
||||||
@ -171,55 +191,61 @@ impl Default for Dynroom {
|
|||||||
short: "XX",
|
short: "XX",
|
||||||
description: "A generic room",
|
description: "A generic room",
|
||||||
description_less_explicit: None,
|
description_less_explicit: None,
|
||||||
exits: vec!(),
|
exits: vec![],
|
||||||
should_caption: false,
|
should_caption: false,
|
||||||
item_flags: vec!(),
|
item_flags: vec![],
|
||||||
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
|
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
|
||||||
|
has_power: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dynzone_list() -> &'static Vec<Dynzone> {
|
pub fn dynzone_list() -> &'static Vec<Dynzone> {
|
||||||
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
|
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
|
||||||
CELL.get_or_init(
|
CELL.get_or_init(|| vec![cokmurl_apartment::zone()])
|
||||||
|| vec!(
|
|
||||||
cokmurl_apartment::zone()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {
|
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {
|
||||||
static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new();
|
static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new();
|
||||||
CELL.get_or_init(
|
CELL.get_or_init(|| {
|
||||||
|| dynzone_list().iter().map(|z| (&z.zonetype, (*z).clone())).collect()
|
dynzone_list()
|
||||||
)
|
.iter()
|
||||||
|
.map(|z| (&z.zonetype, (*z).clone()))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test {
|
||||||
|
use super::super::room::Direction;
|
||||||
|
use super::{dynzone_list, DynzoneType, ExitTarget};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use super::super::room::{Direction};
|
|
||||||
use super::{dynzone_list, ExitTarget, DynzoneType};
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dynzone_types_unique() {
|
fn dynzone_types_unique() {
|
||||||
let mut sorted_list = dynzone_list().clone();
|
let mut sorted_list = dynzone_list().clone();
|
||||||
sorted_list.sort();
|
sorted_list.sort();
|
||||||
assert_eq!(Vec::<(&DynzoneType, usize)>::new(),
|
assert_eq!(
|
||||||
sorted_list.iter()
|
Vec::<(&DynzoneType, usize)>::new(),
|
||||||
.group_by(|v| &v.zonetype)
|
sorted_list
|
||||||
.into_iter()
|
.iter()
|
||||||
.map(|v| (v.0, v.1.count()))
|
.group_by(|v| &v.zonetype)
|
||||||
.filter(|v| v.1 > 1)
|
.into_iter()
|
||||||
.collect::<Vec<(&DynzoneType, usize)>>()
|
.map(|v| (v.0, v.1.count()))
|
||||||
);
|
.filter(|v| v.1 > 1)
|
||||||
|
.collect::<Vec<(&DynzoneType, usize)>>()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dynroom_codes_match_struct() {
|
fn dynroom_codes_match_struct() {
|
||||||
for dynzone in dynzone_list() {
|
for dynzone in dynzone_list() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
dynzone.dyn_rooms.iter().filter(|v| *v.0 != v.1.subcode)
|
dynzone
|
||||||
.map(|v| *v.0).collect::<Vec<&str>>(),
|
.dyn_rooms
|
||||||
|
.iter()
|
||||||
|
.filter(|v| *v.0 != v.1.subcode)
|
||||||
|
.map(|v| *v.0)
|
||||||
|
.collect::<Vec<&str>>(),
|
||||||
Vec::<&str>::new()
|
Vec::<&str>::new()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -228,8 +254,12 @@ mod test {
|
|||||||
#[test]
|
#[test]
|
||||||
fn dynzone_has_dynroom() {
|
fn dynzone_has_dynroom() {
|
||||||
for dynzone in dynzone_list() {
|
for dynzone in dynzone_list() {
|
||||||
assert_ne!(0, dynzone.dyn_rooms.len(), "# rooms in zone {}",
|
assert_ne!(
|
||||||
dynzone.zonetype.to_str())
|
0,
|
||||||
|
dynzone.dyn_rooms.len(),
|
||||||
|
"# rooms in zone {}",
|
||||||
|
dynzone.zonetype.to_str()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -237,16 +267,26 @@ mod test {
|
|||||||
fn dynroom_exits_subcodes_exists() {
|
fn dynroom_exits_subcodes_exists() {
|
||||||
for dynzone in dynzone_list() {
|
for dynzone in dynzone_list() {
|
||||||
for dynroom in dynzone.dyn_rooms.iter() {
|
for dynroom in dynzone.dyn_rooms.iter() {
|
||||||
let exits = dynroom.1.exits.iter().filter(
|
let exits = dynroom
|
||||||
|ex|
|
.1
|
||||||
if let ExitTarget::Intrazone { subcode } = ex.target {
|
.exits
|
||||||
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
|
.iter()
|
||||||
} else {
|
.filter(|ex| {
|
||||||
false
|
if let ExitTarget::Intrazone { subcode } = ex.target {
|
||||||
}).map(|ex| &ex.direction).collect::<Vec<&Direction>>();
|
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
|
||||||
assert_eq!(Vec::<&Direction>::new(), exits,
|
} else {
|
||||||
"exits to invalid subcode in room {} in zone {}", dynroom.0,
|
false
|
||||||
dynzone.zonetype.to_str());
|
}
|
||||||
|
})
|
||||||
|
.map(|ex| &ex.direction)
|
||||||
|
.collect::<Vec<&Direction>>();
|
||||||
|
assert_eq!(
|
||||||
|
Vec::<&Direction>::new(),
|
||||||
|
exits,
|
||||||
|
"exits to invalid subcode in room {} in zone {}",
|
||||||
|
dynroom.0,
|
||||||
|
dynzone.zonetype.to_str()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,14 +1,6 @@
|
|||||||
use super::{
|
use super::{super::room::GridCoords, Dynroom, Dynzone, DynzoneType, Exit, ExitTarget, ExitType};
|
||||||
Dynzone,
|
|
||||||
DynzoneType,
|
|
||||||
Dynroom,
|
|
||||||
Exit,
|
|
||||||
ExitTarget,
|
|
||||||
ExitType,
|
|
||||||
super::room::GridCoords
|
|
||||||
};
|
|
||||||
use crate::static_content::room::Direction;
|
|
||||||
use crate::models::item::ItemFlag;
|
use crate::models::item::ItemFlag;
|
||||||
|
use crate::static_content::room::Direction;
|
||||||
|
|
||||||
pub fn zone() -> Dynzone {
|
pub fn zone() -> Dynzone {
|
||||||
Dynzone {
|
Dynzone {
|
||||||
@ -54,6 +46,7 @@ pub fn zone() -> Dynzone {
|
|||||||
),
|
),
|
||||||
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
|
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
|
||||||
should_caption: true,
|
should_caption: true,
|
||||||
|
has_power: true,
|
||||||
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
|
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
|
||||||
ItemFlag::PrivatePlace),
|
ItemFlag::PrivatePlace),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
|
@ -1,19 +1,16 @@
|
|||||||
use crate::{
|
|
||||||
DResult,
|
|
||||||
models::{
|
|
||||||
user::User,
|
|
||||||
item::Item,
|
|
||||||
journal::JournalType,
|
|
||||||
}
|
|
||||||
};
|
|
||||||
use std::collections::{BTreeMap};
|
|
||||||
use once_cell::sync::OnceCell;
|
|
||||||
use mockall_double::double;
|
|
||||||
#[double] use crate::db::DBTrans;
|
|
||||||
use log::warn;
|
|
||||||
use async_trait::async_trait;
|
|
||||||
use super::species::SpeciesType;
|
use super::species::SpeciesType;
|
||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
|
use crate::{
|
||||||
|
models::{item::Item, journal::JournalType, user::User},
|
||||||
|
DResult,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
use log::warn;
|
||||||
|
use mockall_double::double;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
mod first_dog;
|
mod first_dog;
|
||||||
|
|
||||||
@ -31,7 +28,7 @@ pub trait JournalChecker {
|
|||||||
trans: &DBTrans,
|
trans: &DBTrans,
|
||||||
user: &mut User,
|
user: &mut User,
|
||||||
player: &mut Item,
|
player: &mut Item,
|
||||||
victim: &Item
|
victim: &Item,
|
||||||
) -> DResult<bool>;
|
) -> DResult<bool>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +36,6 @@ pub struct JournalData {
|
|||||||
name: &'static str,
|
name: &'static str,
|
||||||
details: &'static str,
|
details: &'static str,
|
||||||
xp: u64,
|
xp: u64,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
|
pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
|
||||||
@ -55,34 +51,32 @@ pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
|
|||||||
details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.",
|
details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.",
|
||||||
xp: 150
|
xp: 150
|
||||||
})
|
})
|
||||||
).into_iter().collect())
|
).into_iter().collect());
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn journal_checkers() -> &'static Vec<&'static (dyn JournalChecker + Sync + Send)> {
|
pub fn journal_checkers() -> &'static Vec<&'static (dyn JournalChecker + Sync + Send)> {
|
||||||
static CHECKERS: OnceCell<Vec<&'static (dyn JournalChecker + Sync + Send)>> = OnceCell::new();
|
static CHECKERS: OnceCell<Vec<&'static (dyn JournalChecker + Sync + Send)>> = OnceCell::new();
|
||||||
CHECKERS.get_or_init(|| vec!(
|
CHECKERS.get_or_init(|| vec![&first_dog::CHECKER])
|
||||||
&first_dog::CHECKER
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkers_by_species() ->
|
pub fn checkers_by_species(
|
||||||
&'static BTreeMap<SpeciesType,
|
) -> &'static BTreeMap<SpeciesType, Vec<&'static (dyn JournalChecker + Sync + Send)>> {
|
||||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>
|
static MAP: OnceCell<BTreeMap<SpeciesType, Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||||
{
|
|
||||||
static MAP: OnceCell<BTreeMap<SpeciesType,
|
|
||||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
|
||||||
OnceCell::new();
|
OnceCell::new();
|
||||||
MAP.get_or_init(|| {
|
MAP.get_or_init(|| {
|
||||||
let species_groups = journal_checkers().iter().flat_map(
|
let species_groups = journal_checkers()
|
||||||
|jc|
|
.iter()
|
||||||
jc.kill_subscriptions().into_iter()
|
.flat_map(|jc| {
|
||||||
.filter_map(|sub|
|
jc.kill_subscriptions()
|
||||||
match sub {
|
.into_iter()
|
||||||
KillSubscriptionType::SpecificNPCSpecies { species } =>
|
.filter_map(|sub| match sub {
|
||||||
Some((species.clone(), jc.clone())),
|
KillSubscriptionType::SpecificNPCSpecies { species } => {
|
||||||
_ => None
|
Some((species.clone(), *jc))
|
||||||
})
|
}
|
||||||
).group_by(|v| v.0.clone());
|
_ => None,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.group_by(|v| v.0.clone());
|
||||||
species_groups
|
species_groups
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
||||||
@ -90,24 +84,22 @@ pub fn checkers_by_species() ->
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn checkers_by_npc() ->
|
pub fn checkers_by_npc(
|
||||||
&'static BTreeMap<&'static str,
|
) -> &'static BTreeMap<&'static str, Vec<&'static (dyn JournalChecker + Sync + Send)>> {
|
||||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>
|
static MAP: OnceCell<BTreeMap<&'static str, Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||||
{
|
|
||||||
static MAP: OnceCell<BTreeMap<&'static str,
|
|
||||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
|
||||||
OnceCell::new();
|
OnceCell::new();
|
||||||
MAP.get_or_init(|| {
|
MAP.get_or_init(|| {
|
||||||
let npc_groups = journal_checkers().iter().flat_map(
|
let npc_groups = journal_checkers()
|
||||||
|jc|
|
.iter()
|
||||||
jc.kill_subscriptions().into_iter()
|
.flat_map(|jc| {
|
||||||
.filter_map(|sub|
|
jc.kill_subscriptions()
|
||||||
match sub {
|
.into_iter()
|
||||||
KillSubscriptionType::SpecificNPC { code } =>
|
.filter_map(|sub| match sub {
|
||||||
Some((code.clone(), jc.clone())),
|
KillSubscriptionType::SpecificNPC { code } => Some((code.clone(), *jc)),
|
||||||
_ => None
|
_ => None,
|
||||||
})
|
})
|
||||||
).group_by(|v| v.0.clone());
|
})
|
||||||
|
.group_by(|v| v.0.clone());
|
||||||
npc_groups
|
npc_groups
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
||||||
@ -115,58 +107,78 @@ pub fn checkers_by_npc() ->
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn award_journal_if_needed(trans: &DBTrans,
|
pub async fn award_journal_if_needed(
|
||||||
user: &mut User,
|
trans: &DBTrans,
|
||||||
player: &mut Item,
|
user: &mut User,
|
||||||
journal: JournalType) -> DResult<bool> {
|
player: &mut Item,
|
||||||
if user.experience.journals.completed_journals.contains(&journal) {
|
journal: JournalType,
|
||||||
|
) -> DResult<bool> {
|
||||||
|
if user
|
||||||
|
.experience
|
||||||
|
.journals
|
||||||
|
.completed_journals
|
||||||
|
.contains(&journal)
|
||||||
|
{
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let journal_data = match journal_types().get(&journal) {
|
let journal_data = match journal_types().get(&journal) {
|
||||||
None => {
|
None => {
|
||||||
warn!("Tried to award journal type {:#?} that doesn't exist.", &journal);
|
warn!(
|
||||||
|
"Tried to award journal type {:#?} that doesn't exist.",
|
||||||
|
&journal
|
||||||
|
);
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
},
|
}
|
||||||
Some(v) => v
|
Some(v) => v,
|
||||||
};
|
};
|
||||||
user.experience.journals.completed_journals.insert(journal);
|
user.experience.journals.completed_journals.insert(journal);
|
||||||
// Note: Not counted as 'change for this reroll' since it is permanent.
|
// Note: Not counted as 'change for this reroll' since it is permanent.
|
||||||
player.total_xp += journal_data.xp;
|
player.total_xp += journal_data.xp;
|
||||||
if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? {
|
if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? {
|
||||||
trans.queue_for_session(
|
trans
|
||||||
&sess,
|
.queue_for_session(
|
||||||
Some(&format!("Journal earned: {} - You earned {} XP for {}\n",
|
&sess,
|
||||||
journal_data.name, journal_data.xp, journal_data.details)
|
Some(&format!(
|
||||||
)).await?;
|
"Journal earned: {} - You earned {} XP for {}\n",
|
||||||
|
journal_data.name, journal_data.xp, journal_data.details
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(true)
|
Ok(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn check_journal_for_kill(trans: &DBTrans,
|
pub async fn check_journal_for_kill(
|
||||||
player: &mut Item,
|
trans: &DBTrans,
|
||||||
victim: &Item) -> DResult<bool> {
|
player: &mut Item,
|
||||||
|
victim: &Item,
|
||||||
|
) -> DResult<bool> {
|
||||||
if player.item_type != "player" {
|
if player.item_type != "player" {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
let mut user = match trans.find_by_username(&player.item_code).await? {
|
let mut user = match trans.find_by_username(&player.item_code).await? {
|
||||||
None => return Ok(false),
|
None => return Ok(false),
|
||||||
Some(u) => u
|
Some(u) => u,
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut did_work = false;
|
let mut did_work = false;
|
||||||
|
|
||||||
if let Some(checkers) = checkers_by_species().get(&victim.species) {
|
if let Some(checkers) = checkers_by_species().get(&victim.species) {
|
||||||
for checker in checkers {
|
for checker in checkers {
|
||||||
did_work = did_work ||
|
did_work = did_work
|
||||||
checker.handle_kill(trans, &mut user, player, victim).await?;
|
|| checker
|
||||||
|
.handle_kill(trans, &mut user, player, victim)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(checkers) = checkers_by_npc().get(victim.item_code.as_str()) {
|
if let Some(checkers) = checkers_by_npc().get(victim.item_code.as_str()) {
|
||||||
for checker in checkers {
|
for checker in checkers {
|
||||||
did_work = did_work ||
|
did_work = did_work
|
||||||
checker.handle_kill(trans, &mut user, player, victim).await?;
|
|| checker
|
||||||
|
.handle_kill(trans, &mut user, player, victim)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,26 +1,21 @@
|
|||||||
use super::{JournalChecker, KillSubscriptionType, award_journal_if_needed};
|
use super::{award_journal_if_needed, JournalChecker, KillSubscriptionType};
|
||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
use crate::{
|
use crate::{
|
||||||
DResult,
|
models::{item::Item, journal::JournalType, user::User},
|
||||||
static_content::species::SpeciesType,
|
static_content::species::SpeciesType,
|
||||||
models::{
|
DResult,
|
||||||
user::User,
|
|
||||||
item::Item,
|
|
||||||
journal::JournalType,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use mockall_double::double;
|
use mockall_double::double;
|
||||||
#[double] use crate::db::DBTrans;
|
|
||||||
|
|
||||||
pub struct FirstDogChecker;
|
pub struct FirstDogChecker;
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
impl JournalChecker for FirstDogChecker {
|
impl JournalChecker for FirstDogChecker {
|
||||||
fn kill_subscriptions(&self) -> Vec<KillSubscriptionType> {
|
fn kill_subscriptions(&self) -> Vec<KillSubscriptionType> {
|
||||||
vec!(
|
vec![KillSubscriptionType::SpecificNPCSpecies {
|
||||||
KillSubscriptionType::SpecificNPCSpecies {
|
species: SpeciesType::Dog,
|
||||||
species: SpeciesType::Dog
|
}]
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_kill(
|
async fn handle_kill(
|
||||||
@ -28,7 +23,7 @@ impl JournalChecker for FirstDogChecker {
|
|||||||
trans: &DBTrans,
|
trans: &DBTrans,
|
||||||
user: &mut User,
|
user: &mut User,
|
||||||
player: &mut Item,
|
player: &mut Item,
|
||||||
_victim: &Item
|
_victim: &Item,
|
||||||
) -> DResult<bool> {
|
) -> DResult<bool> {
|
||||||
award_journal_if_needed(trans, user, player, JournalType::SlayedMeanDog).await
|
award_journal_if_needed(trans, user, player, JournalType::SlayedMeanDog).await
|
||||||
}
|
}
|
||||||
|
@ -1,16 +1,21 @@
|
|||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
use crate::{
|
use crate::{
|
||||||
message_handler::user_commands::{UResult, VerbContext},
|
message_handler::user_commands::{UResult, VerbContext},
|
||||||
models::consent::ConsentType,
|
models::consent::ConsentType,
|
||||||
models::item::{Item, Pronouns, SkillType},
|
models::item::{Item, ItemFlag, Pronouns, SkillType},
|
||||||
regular_tasks::queued_command::QueuedCommandContext,
|
regular_tasks::queued_command::QueuedCommandContext,
|
||||||
static_content::{room::Direction, species::BodyPart},
|
static_content::{room::Direction, species::BodyPart},
|
||||||
};
|
};
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
|
use mockall_double::double;
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
use rand::seq::SliceRandom;
|
use rand::seq::SliceRandom;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
mod bags;
|
||||||
|
mod benches;
|
||||||
mod blade;
|
mod blade;
|
||||||
mod books;
|
mod books;
|
||||||
mod corp_licence;
|
mod corp_licence;
|
||||||
@ -256,6 +261,11 @@ impl ContainerCheck for PermissiveContainerCheck {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
pub trait BenchData {
|
||||||
|
async fn check_make(&self, trans: &DBTrans, bench: &Item, recipe: &Item) -> UResult<()>;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ContainerData {
|
pub struct ContainerData {
|
||||||
pub max_weight: u64,
|
pub max_weight: u64,
|
||||||
pub base_weight: u64,
|
pub base_weight: u64,
|
||||||
@ -293,8 +303,10 @@ 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 bench_data: Option<&'static (dyn BenchData + Sync + Send)>,
|
||||||
pub wear_data: Option<WearData>,
|
pub wear_data: Option<WearData>,
|
||||||
pub container_data: Option<ContainerData>,
|
pub container_data: Option<ContainerData>,
|
||||||
|
pub default_flags: Vec<ItemFlag>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for PossessionData {
|
impl Default for PossessionData {
|
||||||
@ -316,8 +328,10 @@ impl Default for PossessionData {
|
|||||||
sign_handler: None,
|
sign_handler: None,
|
||||||
write_handler: None,
|
write_handler: None,
|
||||||
can_butcher: false,
|
can_butcher: false,
|
||||||
|
bench_data: None,
|
||||||
wear_data: None,
|
wear_data: None,
|
||||||
container_data: None,
|
container_data: None,
|
||||||
|
default_flags: vec![],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -346,7 +360,7 @@ impl WeaponAttackData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub enum PossessionType {
|
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
|
||||||
@ -367,12 +381,18 @@ pub enum PossessionType {
|
|||||||
// Corporate
|
// Corporate
|
||||||
NewCorpLicence,
|
NewCorpLicence,
|
||||||
CertificateOfIncorporation,
|
CertificateOfIncorporation,
|
||||||
|
// Storage
|
||||||
|
DuffelBag,
|
||||||
// Security
|
// Security
|
||||||
Scanlock,
|
Scanlock,
|
||||||
// Crafting
|
// Food
|
||||||
|
GrilledSteak,
|
||||||
|
// Crafting inputs
|
||||||
Steak,
|
Steak,
|
||||||
AnimalSkin,
|
AnimalSkin,
|
||||||
SeveredHead,
|
SeveredHead,
|
||||||
|
// Craft benches
|
||||||
|
KitchenStove,
|
||||||
// Recipes
|
// Recipes
|
||||||
CulinaryEssentials,
|
CulinaryEssentials,
|
||||||
GrilledSteakRecipe,
|
GrilledSteakRecipe,
|
||||||
@ -395,6 +415,7 @@ impl Into<Item> for PossessionType {
|
|||||||
.collect(),
|
.collect(),
|
||||||
health: possession_dat.max_health,
|
health: possession_dat.max_health,
|
||||||
weight: possession_dat.weight,
|
weight: possession_dat.weight,
|
||||||
|
flags: possession_dat.default_flags.clone(),
|
||||||
pronouns: Pronouns {
|
pronouns: Pronouns {
|
||||||
is_proper: false,
|
is_proper: false,
|
||||||
..Pronouns::default_inanimate()
|
..Pronouns::default_inanimate()
|
||||||
@ -446,6 +467,8 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
|
|||||||
vec![(Fangs, fangs::data())]
|
vec![(Fangs, fangs::data())]
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
.chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
|
.chain(bags::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
|
.chain(benches::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(blade::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(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||||
.chain(
|
.chain(
|
||||||
@ -509,6 +532,7 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Debug, Clone)]
|
||||||
pub struct CraftData {
|
pub struct CraftData {
|
||||||
pub skill: SkillType,
|
pub skill: SkillType,
|
||||||
pub difficulty: f64,
|
pub difficulty: f64,
|
||||||
@ -516,6 +540,12 @@ pub struct CraftData {
|
|||||||
pub output: PossessionType,
|
pub output: PossessionType,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct RecipeCraftData {
|
||||||
|
pub craft_data: CraftData,
|
||||||
|
pub recipe: PossessionType,
|
||||||
|
pub bench: Option<PossessionType>,
|
||||||
|
}
|
||||||
|
|
||||||
pub fn improv_table() -> &'static Vec<CraftData> {
|
pub fn improv_table() -> &'static Vec<CraftData> {
|
||||||
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
|
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
|
||||||
IMPROV_CELL.get_or_init(|| {
|
IMPROV_CELL.get_or_init(|| {
|
||||||
@ -563,6 +593,32 @@ pub fn improv_by_output() -> &'static BTreeMap<PossessionType, &'static CraftDat
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn recipe_craft_table() -> &'static Vec<RecipeCraftData> {
|
||||||
|
static RECIPE_CELL: OnceCell<Vec<RecipeCraftData>> = OnceCell::new();
|
||||||
|
RECIPE_CELL.get_or_init(|| {
|
||||||
|
vec![RecipeCraftData {
|
||||||
|
craft_data: CraftData {
|
||||||
|
skill: SkillType::Craft,
|
||||||
|
difficulty: 5.0,
|
||||||
|
inputs: vec![PossessionType::Steak],
|
||||||
|
output: PossessionType::GrilledSteak,
|
||||||
|
},
|
||||||
|
recipe: PossessionType::GrilledSteakRecipe,
|
||||||
|
bench: Some(PossessionType::KitchenStove),
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recipe_craft_by_recipe() -> &'static BTreeMap<PossessionType, &'static RecipeCraftData> {
|
||||||
|
static MAP_CELL: OnceCell<BTreeMap<PossessionType, &'static RecipeCraftData>> = OnceCell::new();
|
||||||
|
MAP_CELL.get_or_init(|| {
|
||||||
|
recipe_craft_table()
|
||||||
|
.iter()
|
||||||
|
.map(|rcd| (rcd.recipe.clone(), rcd))
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
@ -624,6 +680,55 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_improv_item_has_possession_data_for_inputs() {
|
||||||
|
assert_eq!(
|
||||||
|
improv_table()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|cd| cd.inputs.iter())
|
||||||
|
.filter_map(|inp| if possession_data().get(inp).is_none() {
|
||||||
|
Some(inp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect::<Vec<&'static PossessionType>>(),
|
||||||
|
Vec::<&'static PossessionType>::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_recipe_has_possession_data_for_output() {
|
||||||
|
assert_eq!(
|
||||||
|
recipe_craft_table()
|
||||||
|
.iter()
|
||||||
|
.filter_map(
|
||||||
|
|rcd| if possession_data().get(&rcd.craft_data.output).is_none() {
|
||||||
|
Some(&rcd.craft_data.output)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.collect::<Vec<&'static PossessionType>>(),
|
||||||
|
Vec::<&'static PossessionType>::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_recipe_has_possession_data_for_inputs() {
|
||||||
|
assert_eq!(
|
||||||
|
recipe_craft_table()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|rcd| rcd.craft_data.inputs.iter())
|
||||||
|
.filter_map(|inp| if possession_data().get(inp).is_none() {
|
||||||
|
Some(inp)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
.collect::<Vec<&'static PossessionType>>(),
|
||||||
|
Vec::<&'static PossessionType>::new()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn container_weight_should_match_calculated_weight() {
|
fn container_weight_should_match_calculated_weight() {
|
||||||
for (_pt, pd) in possession_data().iter() {
|
for (_pt, pd) in possession_data().iter() {
|
||||||
|
27
blastmud_game/src/static_content/possession_type/bags.rs
Normal file
27
blastmud_game/src/static_content/possession_type/bags.rs
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
use super::{PossessionData, PossessionType};
|
||||||
|
use crate::static_content::possession_type::ContainerData;
|
||||||
|
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::DuffelBag,
|
||||||
|
PossessionData {
|
||||||
|
display: "duffel bag",
|
||||||
|
aliases: vec!["bag"],
|
||||||
|
details: "A sturdy bag made from some kind of black synthetic-fibre fabric.\n\n\
|
||||||
|
A single broad strap from one side to another looks like it is intended \
|
||||||
|
to be carried across the shoulder. It looks like it can carry about 20 kgs \
|
||||||
|
of stuff as easily as you could carry 14.4 kgs loose",
|
||||||
|
weight: 400,
|
||||||
|
container_data: Some(ContainerData {
|
||||||
|
base_weight: 400,
|
||||||
|
compression_ratio: 0.7,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
})
|
||||||
|
}
|
53
blastmud_game/src/static_content/possession_type/benches.rs
Normal file
53
blastmud_game/src/static_content/possession_type/benches.rs
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
use super::{BenchData, PossessionData, PossessionType};
|
||||||
|
#[double]
|
||||||
|
use crate::db::DBTrans;
|
||||||
|
use crate::{
|
||||||
|
message_handler::user_commands::{UResult, UserError},
|
||||||
|
models::item::{Item, ItemFlag},
|
||||||
|
services::require_power,
|
||||||
|
static_content::possession_type::ContainerData,
|
||||||
|
};
|
||||||
|
use ansi::ansi;
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use mockall_double::double;
|
||||||
|
use once_cell::sync::OnceCell;
|
||||||
|
|
||||||
|
struct NeedPowerBench;
|
||||||
|
static NEED_POWER_BENCH: NeedPowerBench = NeedPowerBench;
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl BenchData for NeedPowerBench {
|
||||||
|
async fn check_make(&self, trans: &DBTrans, bench: &Item, _recipe: &Item) -> UResult<()> {
|
||||||
|
let (room_type, room_code) = bench
|
||||||
|
.location
|
||||||
|
.split_once("/")
|
||||||
|
.ok_or_else(|| UserError("You're in an invalid location!".to_owned()))?;
|
||||||
|
let room = trans
|
||||||
|
.find_item_by_type_code(room_type, room_code)
|
||||||
|
.await?
|
||||||
|
.ok_or_else(|| UserError("Your current room doesn't exist.".to_owned()))?;
|
||||||
|
Ok(require_power(&room)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||||
|
static D: OnceCell<Vec<(PossessionType, PossessionData)>> = OnceCell::new();
|
||||||
|
&D.get_or_init(|| {
|
||||||
|
vec![(
|
||||||
|
PossessionType::KitchenStove,
|
||||||
|
PossessionData {
|
||||||
|
display: "kitchen stove",
|
||||||
|
aliases: vec!["stove"],
|
||||||
|
details: ansi!("A four-element electric stove, with an oven underneath, capable of cooking nutritious meals - or burning a careless amateur cook! [To use, try putting a recipe and the ingredients in it with the <bold>put<reset> command, and turning it on with the <bold>make<reset> command]"),
|
||||||
|
weight: 40000,
|
||||||
|
container_data: Some(ContainerData {
|
||||||
|
base_weight: 40000,
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
bench_data: Some(&NEED_POWER_BENCH),
|
||||||
|
default_flags: vec![ItemFlag::Bench],
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
)]
|
||||||
|
})
|
||||||
|
}
|
@ -1,7 +1,7 @@
|
|||||||
use super::{PossessionData, PossessionType};
|
use super::{PossessionData, PossessionType};
|
||||||
use crate::{
|
use crate::{
|
||||||
message_handler::user_commands::{UResult, UserError},
|
message_handler::user_commands::{UResult, UserError},
|
||||||
models::item::Item,
|
models::item::{Item, ItemFlag},
|
||||||
static_content::possession_type::{ContainerCheck, ContainerData},
|
static_content::possession_type::{ContainerCheck, ContainerData},
|
||||||
};
|
};
|
||||||
use once_cell::sync::OnceCell;
|
use once_cell::sync::OnceCell;
|
||||||
@ -48,6 +48,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
|||||||
checker: &RECIPES_ONLY_CHECKER,
|
checker: &RECIPES_ONLY_CHECKER,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
default_flags: vec![ItemFlag::Book],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
(PossessionType::GrilledSteakRecipe,
|
(PossessionType::GrilledSteakRecipe,
|
||||||
@ -56,6 +57,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
|||||||
aliases: vec!["grilled steak"],
|
aliases: vec!["grilled steak"],
|
||||||
details: "Instructions for how to make a basic but mouthwatering steak",
|
details: "Instructions for how to make a basic but mouthwatering steak",
|
||||||
weight: 10,
|
weight: 10,
|
||||||
|
default_flags: vec![ItemFlag::Instructions],
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
))
|
))
|
||||||
|
@ -32,5 +32,14 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
PossessionType::GrilledSteak,
|
||||||
|
PossessionData {
|
||||||
|
display: "grilled steak",
|
||||||
|
details: "A mouth-wateringly grilled steak, its outer brown surface a perfect demonstration of the Maillard reaction, with a thin bit of fat adjoining the lean protein",
|
||||||
|
weight: 250,
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
@ -321,6 +321,7 @@ pub struct Room {
|
|||||||
// What can be rented here...
|
// What can be rented here...
|
||||||
pub rentable_dynzone: Vec<RentInfo>,
|
pub rentable_dynzone: Vec<RentInfo>,
|
||||||
pub material_type: MaterialType,
|
pub material_type: MaterialType,
|
||||||
|
pub has_power: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Room {
|
impl Default for Room {
|
||||||
@ -341,6 +342,7 @@ impl Default for Room {
|
|||||||
stock_list: vec![],
|
stock_list: vec![],
|
||||||
rentable_dynzone: vec![],
|
rentable_dynzone: vec![],
|
||||||
material_type: MaterialType::Normal,
|
material_type: MaterialType::Normal,
|
||||||
|
has_power: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user