Implement craft on benches

Initially just a stove
Also update Rust.
This commit is contained in:
Condorra 2023-07-24 22:46:50 +10:00
parent bfc1d4d4b5
commit 590d4640dd
35 changed files with 5653 additions and 4544 deletions

1234
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -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;
}
}

View File

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

View File

@ -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;
}
}

View File

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

View File

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

View File

@ -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,21 +45,12 @@ 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;
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(items.len() as u64 * posdat.weight),
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)",
@ -68,7 +58,6 @@ impl UserVerb for Verb {
}
));
}
}
response.push_str(&format!(
"Total weight: {} ({} max)\n",
weight(total),

View File

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

View 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;

View File

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

View File

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

View File

@ -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: search_what.item_code.clone(),
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: into_what.item_code.clone(),
target_possession_id: target.item_code.clone(),
},
)

View File

@ -261,6 +261,9 @@ pub enum ItemFlag {
Hireable,
NPCsDontAttack,
CanLoad,
Bench,
Book,
Instructions,
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]

View File

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

View File

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

View File

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

View File

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

View File

@ -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,7 +145,8 @@ pub async fn run_effects(
}
for (eff_item, l) in target_health_series.into_iter() {
trans.upsert_task(&Task {
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),
@ -127,8 +155,9 @@ pub async fn run_effects(
details: TaskDetails::DelayedHealth {
effect_series: l,
item: eff_item,
}
}).await?;
},
})
.await?;
}
Ok(())

View File

@ -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? {

View File

@ -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 {
special_data: Some(ItemSpecialData::DynzoneData {
zone_exit: Some(connect_where.to_owned()),
vacate_after: None
}
),
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,29 +97,39 @@ 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 },
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()),
..Default::default()
},
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
}
).await?;
})
.collect(),
),
..Default::default()
})
.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,41 +191,43 @@ 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()
assert_eq!(
Vec::<(&DynzoneType, usize)>::new(),
sorted_list
.iter()
.group_by(|v| &v.zonetype)
.into_iter()
.map(|v| (v.0, v.1.count()))
@ -218,8 +240,12 @@ mod 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,8 +254,12 @@ 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()
)
}
}
@ -237,16 +267,26 @@ mod 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|
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());
}
})
.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()
);
}
}
}

View File

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

View File

@ -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
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());
})
.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
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());
})
.group_by(|v| v.0.clone());
npc_groups
.into_iter()
.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(
trans: &DBTrans,
user: &mut User,
player: &mut Item,
journal: JournalType) -> DResult<bool> {
if user.experience.journals.completed_journals.contains(&journal) {
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(
trans
.queue_for_session(
&sess,
Some(&format!("Journal earned: {} - You earned {} XP for {}\n",
journal_data.name, journal_data.xp, journal_data.details)
)).await?;
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,
pub async fn check_journal_for_kill(
trans: &DBTrans,
player: &mut Item,
victim: &Item) -> DResult<bool> {
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?;
}
}

View File

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

View File

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

View 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()
},
)]
})
}

View 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()
},
)]
})
}

View File

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

View File

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

View File

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

View File

@ -405,7 +405,7 @@ pub fn room_list() -> Vec<Room> {
secondary_zones: vec!(),
code: "melbs_kingst_110",
name: "King Street - 110 block",
short: ansi!("<yellow>||<reset>"),
short: ansi!("<yellow>##<reset>"),
description: "A wide road (5 lanes each way) that has been rather poorly maintained. Potholes dot the ashphalt road, while cracks line the footpaths on either side",
description_less_explicit: None,
grid_coords: GridCoords { x: 1, y: 9, z: 0 },
@ -418,10 +418,61 @@ pub fn room_list() -> Vec<Room> {
direction: Direction::SOUTH,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),
code: "melbs_kings_110_laneway",
name: "110 Kings Laneway",
short: ansi!("<yellow>=<reset>"),
description: "A narrow grotty and dingy laneway between concrete walls, both covered with layer upon layer of graffiti. The concrete path has long cracks on its surface",
description_less_explicit: None,
grid_coords: GridCoords { x: 2, y: 9, z: 0 },
exits: vec!(
Exit {
direction: Direction::WEST,
..Default::default()
},
Exit {
direction: Direction::EAST,
..Default::default()
},
),
should_caption: false,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec![],
code: "melbs_rustic_furnishings",
name: "Rustic Furnishings",
short: ansi!("<cyan>RF<reset>"),
description: "The Rustic Furnishings unveils a captivating sight as you enter. Bathed in soft, natural light that filters through large windows, the furniture store reveals a world of comfort and nostalgia.\n\nA warm and inviting atmosphere surrounds you as you explore the meticulously arranged space. Rich, reclaimed hardwood floors stretch out beneath your feet, bearing the echoes of countless stories from the past. Worn yet sturdy, the wooden furniture exudes an enduring charm that captivates the eye, while the kitchen and bathroom sections of the store harbour more modern furniture.\n\nA seasoned storekeeper stands ready to assist shoppers",
description_less_explicit: None,
grid_coords: GridCoords { x: 3, y: 9, z: 0 },
exits: vec![
Exit {
direction: Direction::WEST,
..Default::default()
}
],
stock_list: vec![
RoomStock {
possession_type: PossessionType::KitchenStove,
list_price: 1000,
..Default::default()
}
],
should_caption: true,
..Default::default()
},
Room {
zone: "melbs",
secondary_zones: vec!(),
@ -2499,6 +2550,11 @@ pub fn room_list() -> Vec<Room> {
list_price: 400,
..Default::default()
},
RoomStock {
possession_type: PossessionType::DuffelBag,
list_price: 250,
..Default::default()
},
),
should_caption: true,
..Default::default()

View File