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,
|
||||
}
|
||||
drop(st);
|
||||
return Some(AnsiEvent::<'l>(
|
||||
AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]),
|
||||
self.state.clone(),
|
||||
|
@ -3,7 +3,7 @@ use crate::message_handler::ListenerSession;
|
||||
use crate::models::{
|
||||
consent::{Consent, ConsentType},
|
||||
corp::{Corp, CorpCommType, CorpId, CorpMembership},
|
||||
item::{Item, LocationActionType},
|
||||
item::{Item, ItemFlag, LocationActionType},
|
||||
session::Session,
|
||||
task::{Task, TaskParse},
|
||||
user::User,
|
||||
@ -297,6 +297,7 @@ pub struct ItemSearchParams<'l> {
|
||||
pub include_all_players: bool,
|
||||
pub item_type_only: Option<&'l str>,
|
||||
pub item_action_type_only: Option<&'l LocationActionType>,
|
||||
pub flagged_only: Option<ItemFlag>,
|
||||
pub limit: u8,
|
||||
pub dead_first: bool,
|
||||
}
|
||||
@ -314,6 +315,7 @@ impl ItemSearchParams<'_> {
|
||||
limit: 100,
|
||||
item_type_only: None,
|
||||
item_action_type_only: None,
|
||||
flagged_only: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -717,6 +719,33 @@ impl DBTrans {
|
||||
.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>(
|
||||
self: &'a Self,
|
||||
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 {
|
||||
ctes.push(format!("contents AS (\
|
||||
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
|
||||
@ -890,6 +929,7 @@ impl DBTrans {
|
||||
ctes.push(format!("loc_contents AS (\
|
||||
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
|
||||
)", param_no));
|
||||
#[allow(dropping_copy_types)]
|
||||
drop(param_no); // or increment if this is a problem.
|
||||
params.push(&player_loc);
|
||||
include_tables.push("SELECT details, aliases FROM loc_contents");
|
||||
|
@ -41,6 +41,7 @@ mod list;
|
||||
pub mod load;
|
||||
mod login;
|
||||
mod look;
|
||||
pub mod make;
|
||||
mod map;
|
||||
pub mod movement;
|
||||
pub mod open;
|
||||
@ -188,6 +189,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
"gm" => map::VERB,
|
||||
"gmap" => map::VERB,
|
||||
|
||||
"make" => make::VERB,
|
||||
"open" => open::VERB,
|
||||
|
||||
"p" => page::VERB,
|
||||
|
@ -114,14 +114,12 @@ impl UserVerb for Verb {
|
||||
let user = user_mut(ctx)?;
|
||||
match user.terms.last_presented_term.as_ref() {
|
||||
None => {
|
||||
drop(user);
|
||||
user_error("There was nothing pending your agreement.".to_owned())?;
|
||||
}
|
||||
Some(last_term) => {
|
||||
user.terms
|
||||
.accepted_terms
|
||||
.insert(last_term.to_owned(), Utc::now());
|
||||
drop(user);
|
||||
if check_and_notify_accepts(ctx).await? {
|
||||
ctx.trans
|
||||
.queue_for_session(
|
||||
|
@ -263,7 +263,7 @@ fn compute_new_consent_state(
|
||||
if Some(&new_consent) == their_target_consent.as_ref() {
|
||||
match new_consent.fight_consent.as_mut() {
|
||||
None => (),
|
||||
Some(mut m) => {
|
||||
Some(m) => {
|
||||
m.pending_change = None;
|
||||
m.status = ConsentStatus::Active;
|
||||
}
|
||||
@ -285,7 +285,7 @@ fn compute_new_consent_state(
|
||||
None => {
|
||||
match new_consent.fight_consent.as_mut() {
|
||||
None => (),
|
||||
Some(mut m) => {
|
||||
Some(m) => {
|
||||
m.status = ConsentStatus::PendingAdd;
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ impl UserVerb for Verb {
|
||||
.any(|al| al.starts_with(&match_item))
|
||||
{
|
||||
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 {
|
||||
user_error(
|
||||
"You don't have enough credits to buy that!".to_owned(),
|
||||
@ -96,7 +96,14 @@ impl UserVerb for Verb {
|
||||
here already"
|
||||
.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,
|
||||
|
@ -108,7 +108,7 @@ pub async fn update_follow_for_failed_movement(
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -8,7 +8,7 @@ use crate::{
|
||||
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
||||
},
|
||||
services::{
|
||||
capacity::{check_item_capacity, CapacityLevel},
|
||||
capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel},
|
||||
comms::broadcast_to_room,
|
||||
},
|
||||
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(),
|
||||
)?;
|
||||
}
|
||||
let item = match ctx.command {
|
||||
let (item, container_opt) = match ctx.command {
|
||||
QueueCommand::Get { possession_id } => {
|
||||
let item = match ctx
|
||||
.trans
|
||||
@ -158,7 +158,7 @@ impl QueueCommandHandler for QueueHandler {
|
||||
Some(&msg_nonexp),
|
||||
)
|
||||
.await?;
|
||||
item
|
||||
(item, None)
|
||||
}
|
||||
QueueCommand::GetFromContainer {
|
||||
from_possession_id,
|
||||
@ -210,7 +210,7 @@ impl QueueCommandHandler for QueueHandler {
|
||||
Some(&msg_nonexp),
|
||||
)
|
||||
.await?;
|
||||
item
|
||||
(item, Some(container))
|
||||
}
|
||||
_ => user_error("Unexpected command".to_owned())?,
|
||||
};
|
||||
@ -235,7 +235,7 @@ impl QueueCommandHandler for QueueHandler {
|
||||
user_error(format!(
|
||||
"{} You can't get {} because it is too heavy!",
|
||||
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.action_type = LocationActionType::Normal;
|
||||
ctx.trans.save_item_model(&item_mut).await?;
|
||||
|
||||
if let Some(container) = container_opt {
|
||||
recalculate_container_weight(&ctx.trans, &container).await?;
|
||||
}
|
||||
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;
|
||||
|
||||
ctx.trans
|
||||
|
@ -2,7 +2,6 @@ use super::{get_player_item_or_fail, UResult, UserVerb, UserVerbRef, VerbContext
|
||||
use crate::{
|
||||
language::weight,
|
||||
models::item::{Item, LocationActionType},
|
||||
static_content::possession_type::{possession_data, PossessionType},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use itertools::Itertools;
|
||||
@ -46,28 +45,18 @@ impl UserVerb for Verb {
|
||||
if item.item_type != "possession" {
|
||||
continue;
|
||||
}
|
||||
if let Some(posdat) = possession_data().get(
|
||||
&item
|
||||
.possession_type
|
||||
.as_ref()
|
||||
.unwrap_or(&PossessionType::AntennaWhip),
|
||||
) {
|
||||
total += items.len() as u64 * posdat.weight;
|
||||
response.push_str(&format!(
|
||||
"{} [{}]{}\n",
|
||||
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)",
|
||||
_ => "",
|
||||
}
|
||||
));
|
||||
}
|
||||
let it_total = items.iter().map(|it| it.weight).sum();
|
||||
total += it_total;
|
||||
response.push_str(&format!(
|
||||
"{} [{}]{}\n",
|
||||
item.display_for_sentence(!ctx.session_dat.less_explicit_mode, items.len(), true),
|
||||
weight(it_total),
|
||||
match item.action_type {
|
||||
LocationActionType::Worn => " (worn)",
|
||||
LocationActionType::Wielded => " (wielded)",
|
||||
_ => "",
|
||||
}
|
||||
));
|
||||
}
|
||||
response.push_str(&format!(
|
||||
"Total weight: {} ({} max)\n",
|
||||
|
@ -10,10 +10,10 @@ use crate::{
|
||||
db::ItemSearchParams,
|
||||
language,
|
||||
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack},
|
||||
services::combat::max_health,
|
||||
services::{combat::max_health, skills::calc_level_gap},
|
||||
static_content::{
|
||||
dynzone,
|
||||
possession_type::possession_data,
|
||||
possession_type::{possession_data, recipe_craft_by_recipe},
|
||||
room::{self, Direction},
|
||||
species::{species_info_map, SpeciesType},
|
||||
},
|
||||
@ -25,7 +25,11 @@ use mockall_double::double;
|
||||
use std::collections::BTreeSet;
|
||||
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 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));
|
||||
}
|
||||
|
||||
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
|
||||
@ -568,7 +638,11 @@ impl UserVerb for Verb {
|
||||
) -> UResult<()> {
|
||||
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() {
|
||||
"room/repro_xv_respawn"
|
||||
} else {
|
||||
@ -582,37 +656,58 @@ impl UserVerb for Verb {
|
||||
.find_item_by_type_code(heretype, herecode)
|
||||
.await?
|
||||
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
|
||||
} else if let Some(dir) = Direction::parse(&rem_trim) {
|
||||
match is_door_in_direction(&ctx.trans, &dir, use_location).await? {
|
||||
DoorSituation::NoDoor
|
||||
| DoorSituation::DoorOutOfRoom {
|
||||
} else if let Some(dir) =
|
||||
Direction::parse(&rem_trim).or_else(|| Direction::parse(&rem_orig))
|
||||
{
|
||||
// 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, .. },
|
||||
..
|
||||
}
|
||||
| DoorSituation::DoorIntoRoom {
|
||||
})
|
||||
| Ok(DoorSituation::DoorIntoRoom {
|
||||
state: DoorState { open: true, .. },
|
||||
..
|
||||
} => {}
|
||||
DoorSituation::DoorIntoRoom {
|
||||
})
|
||||
| Err(UserError(_)) => {}
|
||||
Ok(DoorSituation::DoorIntoRoom {
|
||||
state,
|
||||
room_with_door,
|
||||
..
|
||||
} => {
|
||||
}) => {
|
||||
if let Some(rev_dir) = dir.reverse() {
|
||||
return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
|
||||
}
|
||||
}
|
||||
DoorSituation::DoorOutOfRoom {
|
||||
Ok(DoorSituation::DoorOutOfRoom {
|
||||
state,
|
||||
room_with_door,
|
||||
..
|
||||
} => {
|
||||
}) => {
|
||||
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" {
|
||||
player_item.clone()
|
||||
} else {
|
||||
@ -652,7 +747,7 @@ impl UserVerb for Verb {
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
describe_normal_item(ctx, &item).await?;
|
||||
describe_normal_item(&player_item, ctx, &item).await?;
|
||||
}
|
||||
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.
|
||||
async fn attempt_move_immediate(
|
||||
direction: &Direction,
|
||||
mut ctx: &mut QueuedCommandContext<'_>,
|
||||
ctx: &mut QueuedCommandContext<'_>,
|
||||
source: &MovementSource,
|
||||
) -> UResult<bool> {
|
||||
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 door_state = match room_item_mut
|
||||
let door_state = match room_item_mut
|
||||
.door_states
|
||||
.as_mut()
|
||||
.and_then(|ds| ds.get_mut(&direction))
|
||||
|
@ -3,7 +3,7 @@ use super::{
|
||||
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
|
||||
};
|
||||
use crate::{
|
||||
models::item::LocationActionType,
|
||||
models::item::{ItemFlag, LocationActionType},
|
||||
regular_tasks::queued_command::{
|
||||
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
|
||||
},
|
||||
@ -215,7 +215,7 @@ impl UserVerb for Verb {
|
||||
remaining = remaining2;
|
||||
}
|
||||
|
||||
let (search_what, for_what) = match remaining.split_once(" in ") {
|
||||
let (into_what, for_what) = match remaining.split_once(" in ") {
|
||||
None => {
|
||||
user_error(ansi!("Try <bold>put<reset> item <bold>in<reset> container").to_owned())?
|
||||
}
|
||||
@ -256,15 +256,58 @@ impl UserVerb for Verb {
|
||||
.iter()
|
||||
.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" {
|
||||
user_error("You can't put that in something!".to_owned())?;
|
||||
}
|
||||
|
||||
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(
|
||||
ctx,
|
||||
&mut player_item_mut,
|
||||
&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(),
|
||||
},
|
||||
)
|
||||
|
@ -261,6 +261,9 @@ pub enum ItemFlag {
|
||||
Hireable,
|
||||
NPCsDontAttack,
|
||||
CanLoad,
|
||||
Bench,
|
||||
Book,
|
||||
Instructions,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||
|
@ -2,7 +2,7 @@ use super::{TaskHandler, TaskRunContext};
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
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,
|
||||
};
|
||||
use crate::message_handler::ListenerSession;
|
||||
@ -67,6 +67,11 @@ pub enum QueueCommand {
|
||||
from_possession_id: String,
|
||||
get_possession_id: String,
|
||||
},
|
||||
Make {
|
||||
bench_possession_id: Option<String>,
|
||||
instructions_possession_id: String,
|
||||
already_used: BTreeSet<String>,
|
||||
},
|
||||
Movement {
|
||||
direction: Direction,
|
||||
source: MovementSource,
|
||||
@ -109,6 +114,7 @@ impl QueueCommand {
|
||||
Drop { .. } => "Drop",
|
||||
Get { .. } => "Get",
|
||||
GetFromContainer { .. } => "GetFromContainer",
|
||||
Make { .. } => "Make",
|
||||
Movement { .. } => "Movement",
|
||||
OpenDoor { .. } => "OpenDoor",
|
||||
Put { .. } => "Put",
|
||||
@ -181,6 +187,10 @@ fn queue_command_registry(
|
||||
"GetFromContainer",
|
||||
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"Make",
|
||||
&make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
),
|
||||
(
|
||||
"Movement",
|
||||
&movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
|
||||
|
@ -1,10 +1,17 @@
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
message_handler::user_commands::drop::consider_expire_job_for_item,
|
||||
models::consent::{Consent, ConsentStatus, ConsentType},
|
||||
message_handler::user_commands::{drop::consider_expire_job_for_item, user_error, UResult},
|
||||
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,
|
||||
};
|
||||
use mockall_double::double;
|
||||
@ -117,3 +124,28 @@ pub async fn destroy_container(trans: &DBTrans, container: &Item) -> DResult<()>
|
||||
.await?;
|
||||
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;
|
||||
|
||||
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
|
||||
.possession_type
|
||||
.as_ref()
|
||||
|
@ -1,24 +1,32 @@
|
||||
use crate::{
|
||||
DResult,
|
||||
models::item::Item,
|
||||
};
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{models::item::Item, DResult};
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
|
||||
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
||||
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
||||
pub async fn broadcast_to_room(
|
||||
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? {
|
||||
if item.item_type != "player" || item.death_data.is_some() {
|
||||
continue;
|
||||
}
|
||||
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 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 let Some(msg) = message_nonexplicit {
|
||||
trans.queue_for_session(&session, Some(msg)).await?;
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
trans.queue_for_session(&session, Some(message_explicit_ok)).await?;
|
||||
trans
|
||||
.queue_for_session(&session, Some(message_explicit_ok))
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
@ -1,41 +1,29 @@
|
||||
use super::{combat::change_health, comms::broadcast_to_room};
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
models::{
|
||||
item::Item,
|
||||
task::{
|
||||
Task,
|
||||
TaskMeta,
|
||||
TaskDetails,
|
||||
}
|
||||
task::{Task, TaskDetails, TaskMeta},
|
||||
},
|
||||
|
||||
regular_tasks::{TaskHandler, TaskRunContext},
|
||||
static_content::possession_type::UseEffect,
|
||||
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 std::time;
|
||||
use serde::{Serialize, Deserialize};
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use chrono::Utc;
|
||||
use log::info;
|
||||
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)]
|
||||
pub struct DelayedHealthEffect {
|
||||
magnitude: i64,
|
||||
delay: u64,
|
||||
message: String,
|
||||
message_nonexp: String
|
||||
message_nonexp: String,
|
||||
}
|
||||
|
||||
pub struct DelayedHealthTaskHandler;
|
||||
@ -43,70 +31,109 @@ pub struct DelayedHealthTaskHandler;
|
||||
impl TaskHandler for DelayedHealthTaskHandler {
|
||||
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
||||
let ref mut item_effect_series = match &mut ctx.task.details {
|
||||
TaskDetails::DelayedHealth { item, ref mut effect_series } => (item, effect_series),
|
||||
_ => Err("Expected DelayedHealth type")?
|
||||
TaskDetails::DelayedHealth {
|
||||
item,
|
||||
ref mut effect_series,
|
||||
} => (item, effect_series),
|
||||
_ => Err("Expected DelayedHealth type")?,
|
||||
};
|
||||
let (item_type, item_code) = match item_effect_series.0.split_once("/") {
|
||||
None => {
|
||||
info!("Invalid item {} to DelayedHealthTaskHandler", item_effect_series.0);
|
||||
info!(
|
||||
"Invalid item {} to DelayedHealthTaskHandler",
|
||||
item_effect_series.0
|
||||
);
|
||||
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 => {
|
||||
return Ok(None);
|
||||
}
|
||||
Some(it) => it
|
||||
Some(it) => it,
|
||||
};
|
||||
if item.death_data.is_some() {
|
||||
return Ok(None);
|
||||
}
|
||||
match item_effect_series.1.pop_front() {
|
||||
None => Ok(None),
|
||||
Some(DelayedHealthEffect { magnitude, message, message_nonexp, .. }) => {
|
||||
Some(DelayedHealthEffect {
|
||||
magnitude,
|
||||
message,
|
||||
message_nonexp,
|
||||
..
|
||||
}) => {
|
||||
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?;
|
||||
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(
|
||||
trans: &DBTrans, effects: &Vec<UseEffect>,
|
||||
trans: &DBTrans,
|
||||
effects: &Vec<UseEffect>,
|
||||
player: &mut Item,
|
||||
item: &Item,
|
||||
// None if target is player
|
||||
target: &mut Option<Item>,
|
||||
level: f64,
|
||||
task_ref: &str
|
||||
task_ref: &str,
|
||||
) -> DResult<()> {
|
||||
let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::new();
|
||||
for effect in effects {
|
||||
match effect {
|
||||
UseEffect::BroadcastMessage { messagef } => {
|
||||
let (msg_exp, msg_nonexp) = messagef(player, item, target.as_ref().unwrap_or(player));
|
||||
broadcast_to_room(trans, &player.location, None, &msg_exp,
|
||||
Some(&msg_nonexp)).await?;
|
||||
},
|
||||
UseEffect::ChangeTargetHealth { delay_secs, base_effect, skill_multiplier, max_effect,
|
||||
message } => {
|
||||
let (msg_exp, msg_nonexp) =
|
||||
messagef(player, item, target.as_ref().unwrap_or(player));
|
||||
broadcast_to_room(trans, &player.location, None, &msg_exp, Some(&msg_nonexp))
|
||||
.await?;
|
||||
}
|
||||
UseEffect::ChangeTargetHealth {
|
||||
delay_secs,
|
||||
base_effect,
|
||||
skill_multiplier,
|
||||
max_effect,
|
||||
message,
|
||||
} => {
|
||||
let health_impact =
|
||||
(*base_effect + ((skill_multiplier * level) as i64).min(*max_effect)) as i64;
|
||||
let (msg, msg_nonexp) = message(target.as_ref().unwrap_or(player));
|
||||
if *delay_secs == 0 {
|
||||
change_health(trans, health_impact, target.as_mut().unwrap_or(player), &msg,
|
||||
&msg_nonexp).await?;
|
||||
change_health(
|
||||
trans,
|
||||
health_impact,
|
||||
target.as_mut().unwrap_or(player),
|
||||
&msg,
|
||||
&msg_nonexp,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
let target_it = target.as_ref().unwrap_or(player);
|
||||
let fx = DelayedHealthEffect {
|
||||
magnitude: health_impact,
|
||||
delay: *delay_secs,
|
||||
message: msg,
|
||||
message_nonexp: msg_nonexp
|
||||
message_nonexp: msg_nonexp,
|
||||
};
|
||||
target_health_series
|
||||
.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() {
|
||||
trans.upsert_task(&Task {
|
||||
meta: TaskMeta {
|
||||
task_code: format!("{}/{}", eff_item, task_ref),
|
||||
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
|
||||
..Default::default()
|
||||
},
|
||||
details: TaskDetails::DelayedHealth {
|
||||
effect_series: l,
|
||||
item: eff_item,
|
||||
}
|
||||
}).await?;
|
||||
trans
|
||||
.upsert_task(&Task {
|
||||
meta: TaskMeta {
|
||||
task_code: format!("{}/{}", eff_item, task_ref),
|
||||
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
|
||||
..Default::default()
|
||||
},
|
||||
details: TaskDetails::DelayedHealth {
|
||||
effect_series: l,
|
||||
item: eff_item,
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
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).
|
||||
const LAMBDA: f64 = -0.2231435513142097; // log 0.8
|
||||
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(mut user) = trans.find_by_username(&who.item_code).await? {
|
||||
|
@ -3,32 +3,33 @@
|
||||
// dynamically. They can dynamically connect to the grid.
|
||||
// Apartments, planes, and boats are all expected to be specific instances of dynzones.
|
||||
use super::room::{Direction, GridCoords};
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
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 std::collections::BTreeMap;
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
|
||||
mod cokmurl_apartment;
|
||||
|
||||
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
|
||||
pub enum DynzoneType {
|
||||
CokMurlApartment
|
||||
CokMurlApartment,
|
||||
}
|
||||
|
||||
impl DynzoneType {
|
||||
pub fn from_str(i: &str) -> Option<Self> {
|
||||
match i {
|
||||
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
|
||||
_ => None
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
pub fn to_str(&self) -> &'static str {
|
||||
match self {
|
||||
DynzoneType::CokMurlApartment => "CokMurlApartment"
|
||||
DynzoneType::CokMurlApartment => "CokMurlApartment",
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -43,43 +44,51 @@ pub struct Dynzone {
|
||||
|
||||
impl Dynzone {
|
||||
// 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,
|
||||
new_owner: &Item, new_exit_direction: &Direction) -> UResult<String> {
|
||||
pub async fn create_instance(
|
||||
&self,
|
||||
trans: &DBTrans,
|
||||
connect_where: &str,
|
||||
dup_message: &str,
|
||||
new_owner: &Item,
|
||||
new_exit_direction: &Direction,
|
||||
) -> UResult<String> {
|
||||
// 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())?;
|
||||
}
|
||||
let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code);
|
||||
|
||||
|
||||
let code = format!("{}", &trans.alloc_item_code().await?);
|
||||
trans.create_item(
|
||||
&Item {
|
||||
trans
|
||||
.create_item(&Item {
|
||||
item_type: "dynzone".to_owned(),
|
||||
item_code: code.clone(),
|
||||
display: self.zonename.to_owned(),
|
||||
special_data: Some(
|
||||
ItemSpecialData::DynzoneData {
|
||||
zone_exit: Some(connect_where.to_owned()),
|
||||
vacate_after: None
|
||||
}
|
||||
),
|
||||
special_data: Some(ItemSpecialData::DynzoneData {
|
||||
zone_exit: Some(connect_where.to_owned()),
|
||||
vacate_after: None,
|
||||
}),
|
||||
owner: Some(owner.clone()),
|
||||
location: format!("dynzone/{}", &code),
|
||||
..Default::default()
|
||||
}
|
||||
).await?;
|
||||
})
|
||||
.await?;
|
||||
|
||||
let mut should_connect = true;
|
||||
for (_, room) in &self.dyn_rooms {
|
||||
let roomcode = format!("{}/{}", &code, room.subcode);
|
||||
let will_connect = should_connect &&
|
||||
room.exits.iter().any(|r| match r.target {
|
||||
let will_connect = should_connect
|
||||
&& room.exits.iter().any(|r| match r.target {
|
||||
ExitTarget::ExitZone => true,
|
||||
_ => false
|
||||
_ => false,
|
||||
});
|
||||
should_connect &= !will_connect;
|
||||
trans.create_item(
|
||||
&Item {
|
||||
trans
|
||||
.create_item(&Item {
|
||||
item_type: "dynroom".to_owned(),
|
||||
item_code: roomcode,
|
||||
display: room.name.to_owned(),
|
||||
@ -88,31 +97,41 @@ impl Dynzone {
|
||||
location: format!("dynzone/{}", &code),
|
||||
special_data: Some(ItemSpecialData::DynroomData {
|
||||
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 {
|
||||
Some(DynamicEntrance {
|
||||
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(),
|
||||
owner: Some(owner.clone()),
|
||||
door_states: Some(room.exits.iter()
|
||||
.filter_map(|ex|
|
||||
if let ExitType::Doored { description } = ex.exit_type {
|
||||
Some((ex.direction.clone(), DoorState {
|
||||
open: false,
|
||||
description: description.to_owned()
|
||||
}))
|
||||
} else {
|
||||
None
|
||||
}).collect()),
|
||||
door_states: Some(
|
||||
room.exits
|
||||
.iter()
|
||||
.filter_map(|ex| {
|
||||
if let ExitType::Doored { description } = ex.exit_type {
|
||||
Some((
|
||||
ex.direction.clone(),
|
||||
DoorState {
|
||||
open: false,
|
||||
description: description.to_owned(),
|
||||
},
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
..Default::default()
|
||||
}
|
||||
).await?;
|
||||
})
|
||||
.await?;
|
||||
}
|
||||
|
||||
|
||||
Ok(format!("dynzone/{}", &code))
|
||||
}
|
||||
}
|
||||
@ -161,6 +180,7 @@ pub struct Dynroom {
|
||||
pub should_caption: bool,
|
||||
pub item_flags: Vec<ItemFlag>,
|
||||
pub grid_coords: GridCoords,
|
||||
pub has_power: bool,
|
||||
}
|
||||
|
||||
impl Default for Dynroom {
|
||||
@ -171,55 +191,61 @@ impl Default for Dynroom {
|
||||
short: "XX",
|
||||
description: "A generic room",
|
||||
description_less_explicit: None,
|
||||
exits: vec!(),
|
||||
exits: vec![],
|
||||
should_caption: false,
|
||||
item_flags: vec!(),
|
||||
item_flags: vec![],
|
||||
grid_coords: GridCoords { x: 0, y: 0, z: 0 },
|
||||
has_power: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dynzone_list() -> &'static Vec<Dynzone> {
|
||||
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
|
||||
CELL.get_or_init(
|
||||
|| vec!(
|
||||
cokmurl_apartment::zone()
|
||||
)
|
||||
)
|
||||
CELL.get_or_init(|| vec![cokmurl_apartment::zone()])
|
||||
}
|
||||
|
||||
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {
|
||||
static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new();
|
||||
CELL.get_or_init(
|
||||
|| dynzone_list().iter().map(|z| (&z.zonetype, (*z).clone())).collect()
|
||||
)
|
||||
CELL.get_or_init(|| {
|
||||
dynzone_list()
|
||||
.iter()
|
||||
.map(|z| (&z.zonetype, (*z).clone()))
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::super::room::Direction;
|
||||
use super::{dynzone_list, DynzoneType, ExitTarget};
|
||||
use itertools::Itertools;
|
||||
use super::super::room::{Direction};
|
||||
use super::{dynzone_list, ExitTarget, DynzoneType};
|
||||
#[test]
|
||||
fn dynzone_types_unique() {
|
||||
let mut sorted_list = dynzone_list().clone();
|
||||
sorted_list.sort();
|
||||
assert_eq!(Vec::<(&DynzoneType, usize)>::new(),
|
||||
sorted_list.iter()
|
||||
.group_by(|v| &v.zonetype)
|
||||
.into_iter()
|
||||
.map(|v| (v.0, v.1.count()))
|
||||
.filter(|v| v.1 > 1)
|
||||
.collect::<Vec<(&DynzoneType, usize)>>()
|
||||
);
|
||||
assert_eq!(
|
||||
Vec::<(&DynzoneType, usize)>::new(),
|
||||
sorted_list
|
||||
.iter()
|
||||
.group_by(|v| &v.zonetype)
|
||||
.into_iter()
|
||||
.map(|v| (v.0, v.1.count()))
|
||||
.filter(|v| v.1 > 1)
|
||||
.collect::<Vec<(&DynzoneType, usize)>>()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dynroom_codes_match_struct() {
|
||||
for dynzone in dynzone_list() {
|
||||
assert_eq!(
|
||||
dynzone.dyn_rooms.iter().filter(|v| *v.0 != v.1.subcode)
|
||||
.map(|v| *v.0).collect::<Vec<&str>>(),
|
||||
dynzone
|
||||
.dyn_rooms
|
||||
.iter()
|
||||
.filter(|v| *v.0 != v.1.subcode)
|
||||
.map(|v| *v.0)
|
||||
.collect::<Vec<&str>>(),
|
||||
Vec::<&str>::new()
|
||||
);
|
||||
}
|
||||
@ -228,25 +254,39 @@ mod test {
|
||||
#[test]
|
||||
fn dynzone_has_dynroom() {
|
||||
for dynzone in dynzone_list() {
|
||||
assert_ne!(0, dynzone.dyn_rooms.len(), "# rooms in zone {}",
|
||||
dynzone.zonetype.to_str())
|
||||
assert_ne!(
|
||||
0,
|
||||
dynzone.dyn_rooms.len(),
|
||||
"# rooms in zone {}",
|
||||
dynzone.zonetype.to_str()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn dynroom_exits_subcodes_exists() {
|
||||
for dynzone in dynzone_list() {
|
||||
for dynroom in dynzone.dyn_rooms.iter() {
|
||||
let exits = dynroom.1.exits.iter().filter(
|
||||
|ex|
|
||||
if let ExitTarget::Intrazone { subcode } = ex.target {
|
||||
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
|
||||
} else {
|
||||
false
|
||||
}).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());
|
||||
let exits = dynroom
|
||||
.1
|
||||
.exits
|
||||
.iter()
|
||||
.filter(|ex| {
|
||||
if let ExitTarget::Intrazone { subcode } = ex.target {
|
||||
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.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::{
|
||||
Dynzone,
|
||||
DynzoneType,
|
||||
Dynroom,
|
||||
Exit,
|
||||
ExitTarget,
|
||||
ExitType,
|
||||
super::room::GridCoords
|
||||
};
|
||||
use crate::static_content::room::Direction;
|
||||
use super::{super::room::GridCoords, Dynroom, Dynzone, DynzoneType, Exit, ExitTarget, ExitType};
|
||||
use crate::models::item::ItemFlag;
|
||||
use crate::static_content::room::Direction;
|
||||
|
||||
pub fn zone() -> Dynzone {
|
||||
Dynzone {
|
||||
@ -54,6 +46,7 @@ pub fn zone() -> Dynzone {
|
||||
),
|
||||
grid_coords: GridCoords { x: 1, y: 0, z: 0 },
|
||||
should_caption: true,
|
||||
has_power: true,
|
||||
item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
|
||||
ItemFlag::PrivatePlace),
|
||||
..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;
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
models::{item::Item, journal::JournalType, user::User},
|
||||
DResult,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use itertools::Itertools;
|
||||
use log::warn;
|
||||
use mockall_double::double;
|
||||
use once_cell::sync::OnceCell;
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
mod first_dog;
|
||||
|
||||
@ -31,7 +28,7 @@ pub trait JournalChecker {
|
||||
trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
victim: &Item
|
||||
victim: &Item,
|
||||
) -> DResult<bool>;
|
||||
}
|
||||
|
||||
@ -39,7 +36,6 @@ pub struct JournalData {
|
||||
name: &'static str,
|
||||
details: &'static str,
|
||||
xp: u64,
|
||||
|
||||
}
|
||||
|
||||
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.",
|
||||
xp: 150
|
||||
})
|
||||
).into_iter().collect())
|
||||
).into_iter().collect());
|
||||
}
|
||||
|
||||
pub fn journal_checkers() -> &'static Vec<&'static (dyn JournalChecker + Sync + Send)> {
|
||||
static CHECKERS: OnceCell<Vec<&'static (dyn JournalChecker + Sync + Send)>> = OnceCell::new();
|
||||
CHECKERS.get_or_init(|| vec!(
|
||||
&first_dog::CHECKER
|
||||
))
|
||||
CHECKERS.get_or_init(|| vec![&first_dog::CHECKER])
|
||||
}
|
||||
|
||||
pub fn checkers_by_species() ->
|
||||
&'static BTreeMap<SpeciesType,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>
|
||||
{
|
||||
static MAP: OnceCell<BTreeMap<SpeciesType,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||
pub fn checkers_by_species(
|
||||
) -> &'static BTreeMap<SpeciesType, Vec<&'static (dyn JournalChecker + Sync + Send)>> {
|
||||
static MAP: OnceCell<BTreeMap<SpeciesType, Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||
OnceCell::new();
|
||||
MAP.get_or_init(|| {
|
||||
let species_groups = journal_checkers().iter().flat_map(
|
||||
|jc|
|
||||
jc.kill_subscriptions().into_iter()
|
||||
.filter_map(|sub|
|
||||
match sub {
|
||||
KillSubscriptionType::SpecificNPCSpecies { species } =>
|
||||
Some((species.clone(), jc.clone())),
|
||||
_ => None
|
||||
})
|
||||
).group_by(|v| v.0.clone());
|
||||
let species_groups = journal_checkers()
|
||||
.iter()
|
||||
.flat_map(|jc| {
|
||||
jc.kill_subscriptions()
|
||||
.into_iter()
|
||||
.filter_map(|sub| match sub {
|
||||
KillSubscriptionType::SpecificNPCSpecies { species } => {
|
||||
Some((species.clone(), *jc))
|
||||
}
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.group_by(|v| v.0.clone());
|
||||
species_groups
|
||||
.into_iter()
|
||||
.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() ->
|
||||
&'static BTreeMap<&'static str,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>
|
||||
{
|
||||
static MAP: OnceCell<BTreeMap<&'static str,
|
||||
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||
pub fn checkers_by_npc(
|
||||
) -> &'static BTreeMap<&'static str, Vec<&'static (dyn JournalChecker + Sync + Send)>> {
|
||||
static MAP: OnceCell<BTreeMap<&'static str, Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
|
||||
OnceCell::new();
|
||||
MAP.get_or_init(|| {
|
||||
let npc_groups = journal_checkers().iter().flat_map(
|
||||
|jc|
|
||||
jc.kill_subscriptions().into_iter()
|
||||
.filter_map(|sub|
|
||||
match sub {
|
||||
KillSubscriptionType::SpecificNPC { code } =>
|
||||
Some((code.clone(), jc.clone())),
|
||||
_ => None
|
||||
})
|
||||
).group_by(|v| v.0.clone());
|
||||
let npc_groups = journal_checkers()
|
||||
.iter()
|
||||
.flat_map(|jc| {
|
||||
jc.kill_subscriptions()
|
||||
.into_iter()
|
||||
.filter_map(|sub| match sub {
|
||||
KillSubscriptionType::SpecificNPC { code } => Some((code.clone(), *jc)),
|
||||
_ => None,
|
||||
})
|
||||
})
|
||||
.group_by(|v| v.0.clone());
|
||||
npc_groups
|
||||
.into_iter()
|
||||
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
|
||||
@ -115,61 +107,81 @@ pub fn checkers_by_npc() ->
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn award_journal_if_needed(trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
journal: JournalType) -> DResult<bool> {
|
||||
if user.experience.journals.completed_journals.contains(&journal) {
|
||||
pub async fn award_journal_if_needed(
|
||||
trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
journal: JournalType,
|
||||
) -> DResult<bool> {
|
||||
if user
|
||||
.experience
|
||||
.journals
|
||||
.completed_journals
|
||||
.contains(&journal)
|
||||
{
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
let journal_data = match journal_types().get(&journal) {
|
||||
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);
|
||||
},
|
||||
Some(v) => v
|
||||
}
|
||||
Some(v) => v,
|
||||
};
|
||||
user.experience.journals.completed_journals.insert(journal);
|
||||
// Note: Not counted as 'change for this reroll' since it is permanent.
|
||||
player.total_xp += journal_data.xp;
|
||||
if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? {
|
||||
trans.queue_for_session(
|
||||
&sess,
|
||||
Some(&format!("Journal earned: {} - You earned {} XP for {}\n",
|
||||
journal_data.name, journal_data.xp, journal_data.details)
|
||||
)).await?;
|
||||
trans
|
||||
.queue_for_session(
|
||||
&sess,
|
||||
Some(&format!(
|
||||
"Journal earned: {} - You earned {} XP for {}\n",
|
||||
journal_data.name, journal_data.xp, journal_data.details
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub async fn check_journal_for_kill(trans: &DBTrans,
|
||||
player: &mut Item,
|
||||
victim: &Item) -> DResult<bool> {
|
||||
pub async fn check_journal_for_kill(
|
||||
trans: &DBTrans,
|
||||
player: &mut Item,
|
||||
victim: &Item,
|
||||
) -> DResult<bool> {
|
||||
if player.item_type != "player" {
|
||||
return Ok(false);
|
||||
}
|
||||
let mut user = match trans.find_by_username(&player.item_code).await? {
|
||||
None => return Ok(false),
|
||||
Some(u) => u
|
||||
Some(u) => u,
|
||||
};
|
||||
|
||||
let mut did_work = false;
|
||||
|
||||
if let Some(checkers) = checkers_by_species().get(&victim.species) {
|
||||
for checker in checkers {
|
||||
did_work = did_work ||
|
||||
checker.handle_kill(trans, &mut user, player, victim).await?;
|
||||
did_work = did_work
|
||||
|| checker
|
||||
.handle_kill(trans, &mut user, player, victim)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
if let Some(checkers) = checkers_by_npc().get(victim.item_code.as_str()) {
|
||||
for checker in checkers {
|
||||
did_work = did_work ||
|
||||
checker.handle_kill(trans, &mut user, player, victim).await?;
|
||||
did_work = did_work
|
||||
|| checker
|
||||
.handle_kill(trans, &mut user, player, victim)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if did_work {
|
||||
trans.save_user_model(&user).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::{
|
||||
DResult,
|
||||
models::{item::Item, journal::JournalType, user::User},
|
||||
static_content::species::SpeciesType,
|
||||
models::{
|
||||
user::User,
|
||||
item::Item,
|
||||
journal::JournalType,
|
||||
}
|
||||
DResult,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use mockall_double::double;
|
||||
#[double] use crate::db::DBTrans;
|
||||
|
||||
pub struct FirstDogChecker;
|
||||
#[async_trait]
|
||||
impl JournalChecker for FirstDogChecker {
|
||||
fn kill_subscriptions(&self) -> Vec<KillSubscriptionType> {
|
||||
vec!(
|
||||
KillSubscriptionType::SpecificNPCSpecies {
|
||||
species: SpeciesType::Dog
|
||||
}
|
||||
)
|
||||
vec![KillSubscriptionType::SpecificNPCSpecies {
|
||||
species: SpeciesType::Dog,
|
||||
}]
|
||||
}
|
||||
|
||||
async fn handle_kill(
|
||||
@ -28,7 +23,7 @@ impl JournalChecker for FirstDogChecker {
|
||||
trans: &DBTrans,
|
||||
user: &mut User,
|
||||
player: &mut Item,
|
||||
_victim: &Item
|
||||
_victim: &Item,
|
||||
) -> DResult<bool> {
|
||||
award_journal_if_needed(trans, user, player, JournalType::SlayedMeanDog).await
|
||||
}
|
||||
|
@ -1,16 +1,21 @@
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
message_handler::user_commands::{UResult, VerbContext},
|
||||
models::consent::ConsentType,
|
||||
models::item::{Item, Pronouns, SkillType},
|
||||
models::item::{Item, ItemFlag, Pronouns, SkillType},
|
||||
regular_tasks::queued_command::QueuedCommandContext,
|
||||
static_content::{room::Direction, species::BodyPart},
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use mockall_double::double;
|
||||
use once_cell::sync::OnceCell;
|
||||
use rand::seq::SliceRandom;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
mod bags;
|
||||
mod benches;
|
||||
mod blade;
|
||||
mod books;
|
||||
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 max_weight: u64,
|
||||
pub base_weight: u64,
|
||||
@ -293,8 +303,10 @@ pub struct PossessionData {
|
||||
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
|
||||
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
|
||||
pub can_butcher: bool,
|
||||
pub bench_data: Option<&'static (dyn BenchData + Sync + Send)>,
|
||||
pub wear_data: Option<WearData>,
|
||||
pub container_data: Option<ContainerData>,
|
||||
pub default_flags: Vec<ItemFlag>,
|
||||
}
|
||||
|
||||
impl Default for PossessionData {
|
||||
@ -316,8 +328,10 @@ impl Default for PossessionData {
|
||||
sign_handler: None,
|
||||
write_handler: None,
|
||||
can_butcher: false,
|
||||
bench_data: None,
|
||||
wear_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 {
|
||||
// Special values that substitute for possessions.
|
||||
Fangs, // Default weapon for certain animals
|
||||
@ -367,12 +381,18 @@ pub enum PossessionType {
|
||||
// Corporate
|
||||
NewCorpLicence,
|
||||
CertificateOfIncorporation,
|
||||
// Storage
|
||||
DuffelBag,
|
||||
// Security
|
||||
Scanlock,
|
||||
// Crafting
|
||||
// Food
|
||||
GrilledSteak,
|
||||
// Crafting inputs
|
||||
Steak,
|
||||
AnimalSkin,
|
||||
SeveredHead,
|
||||
// Craft benches
|
||||
KitchenStove,
|
||||
// Recipes
|
||||
CulinaryEssentials,
|
||||
GrilledSteakRecipe,
|
||||
@ -395,6 +415,7 @@ impl Into<Item> for PossessionType {
|
||||
.collect(),
|
||||
health: possession_dat.max_health,
|
||||
weight: possession_dat.weight,
|
||||
flags: possession_dat.default_flags.clone(),
|
||||
pronouns: Pronouns {
|
||||
is_proper: false,
|
||||
..Pronouns::default_inanimate()
|
||||
@ -446,6 +467,8 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
|
||||
vec![(Fangs, fangs::data())]
|
||||
.into_iter()
|
||||
.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(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
|
||||
.chain(
|
||||
@ -509,6 +532,7 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Clone)]
|
||||
pub struct CraftData {
|
||||
pub skill: SkillType,
|
||||
pub difficulty: f64,
|
||||
@ -516,6 +540,12 @@ pub struct CraftData {
|
||||
pub output: PossessionType,
|
||||
}
|
||||
|
||||
pub struct RecipeCraftData {
|
||||
pub craft_data: CraftData,
|
||||
pub recipe: PossessionType,
|
||||
pub bench: Option<PossessionType>,
|
||||
}
|
||||
|
||||
pub fn improv_table() -> &'static Vec<CraftData> {
|
||||
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
|
||||
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)]
|
||||
mod tests {
|
||||
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]
|
||||
fn container_weight_should_match_calculated_weight() {
|
||||
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 crate::{
|
||||
message_handler::user_commands::{UResult, UserError},
|
||||
models::item::Item,
|
||||
models::item::{Item, ItemFlag},
|
||||
static_content::possession_type::{ContainerCheck, ContainerData},
|
||||
};
|
||||
use once_cell::sync::OnceCell;
|
||||
@ -48,6 +48,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
checker: &RECIPES_ONLY_CHECKER,
|
||||
..Default::default()
|
||||
}),
|
||||
default_flags: vec![ItemFlag::Book],
|
||||
..Default::default()
|
||||
}),
|
||||
(PossessionType::GrilledSteakRecipe,
|
||||
@ -56,6 +57,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
aliases: vec!["grilled steak"],
|
||||
details: "Instructions for how to make a basic but mouthwatering steak",
|
||||
weight: 10,
|
||||
default_flags: vec![ItemFlag::Instructions],
|
||||
..Default::default()
|
||||
}),
|
||||
))
|
||||
|
@ -32,5 +32,14 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
..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...
|
||||
pub rentable_dynzone: Vec<RentInfo>,
|
||||
pub material_type: MaterialType,
|
||||
pub has_power: bool,
|
||||
}
|
||||
|
||||
impl Default for Room {
|
||||
@ -341,6 +342,7 @@ impl Default for Room {
|
||||
stock_list: vec![],
|
||||
rentable_dynzone: vec![],
|
||||
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