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, _ => continue,
} }
drop(st);
return Some(AnsiEvent::<'l>( return Some(AnsiEvent::<'l>(
AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]), AnsiParseToken::ControlSeq(&self.input[i0..(imax + 1)]),
self.state.clone(), self.state.clone(),

View File

@ -3,7 +3,7 @@ use crate::message_handler::ListenerSession;
use crate::models::{ use crate::models::{
consent::{Consent, ConsentType}, consent::{Consent, ConsentType},
corp::{Corp, CorpCommType, CorpId, CorpMembership}, corp::{Corp, CorpCommType, CorpId, CorpMembership},
item::{Item, LocationActionType}, item::{Item, ItemFlag, LocationActionType},
session::Session, session::Session,
task::{Task, TaskParse}, task::{Task, TaskParse},
user::User, user::User,
@ -297,6 +297,7 @@ pub struct ItemSearchParams<'l> {
pub include_all_players: bool, pub include_all_players: bool,
pub item_type_only: Option<&'l str>, pub item_type_only: Option<&'l str>,
pub item_action_type_only: Option<&'l LocationActionType>, pub item_action_type_only: Option<&'l LocationActionType>,
pub flagged_only: Option<ItemFlag>,
pub limit: u8, pub limit: u8,
pub dead_first: bool, pub dead_first: bool,
} }
@ -314,6 +315,7 @@ impl ItemSearchParams<'_> {
limit: 100, limit: 100,
item_type_only: None, item_type_only: None,
item_action_type_only: None, item_action_type_only: None,
flagged_only: None,
} }
} }
} }
@ -717,6 +719,33 @@ impl DBTrans {
.collect()) .collect())
} }
pub async fn find_items_by_location_possession_type_excluding<'a>(
self: &'a Self,
location: &'a str,
possession_type: &'a PossessionType,
exclude_codes: &'a Vec<&'a str>,
) -> DResult<Vec<Arc<Item>>> {
Ok(self
.pg_trans()?
.query(
"SELECT details FROM items WHERE details->>'location' = $1 AND \
details->'possession_type' = $2 AND NOT \
($3::JSONB @> (details->'item_code')) \
ORDER BY details->>'display' \
LIMIT 100",
&[
&location,
&serde_json::to_value(possession_type)?,
&serde_json::to_value(exclude_codes)?,
],
)
.await?
.into_iter()
.filter_map(|i| serde_json::from_value(i.get("details")).ok())
.map(Arc::new)
.collect())
}
pub async fn find_item_by_location_dynroom_code<'a>( pub async fn find_item_by_location_dynroom_code<'a>(
self: &'a Self, self: &'a Self,
location: &'a str, location: &'a str,
@ -878,6 +907,16 @@ impl DBTrans {
} }
} }
let flagged_only_value: Option<serde_json::Value> = match search.flagged_only.as_ref() {
None => None,
Some(v) => Some(serde_json::to_value(v)?),
};
if let Some(flag) = flagged_only_value.as_ref() {
extra_where.push_str(&format!(" AND details->'flags' @> (${}::JSONB)", param_no));
param_no += 1;
params.push(flag);
}
if search.include_contents { if search.include_contents {
ctes.push(format!("contents AS (\ ctes.push(format!("contents AS (\
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\ SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
@ -890,6 +929,7 @@ impl DBTrans {
ctes.push(format!("loc_contents AS (\ ctes.push(format!("loc_contents AS (\
SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\ SELECT details, details->'aliases' AS aliases FROM items WHERE details->>'location' = ${}\
)", param_no)); )", param_no));
#[allow(dropping_copy_types)]
drop(param_no); // or increment if this is a problem. drop(param_no); // or increment if this is a problem.
params.push(&player_loc); params.push(&player_loc);
include_tables.push("SELECT details, aliases FROM loc_contents"); include_tables.push("SELECT details, aliases FROM loc_contents");

View File

@ -41,6 +41,7 @@ mod list;
pub mod load; pub mod load;
mod login; mod login;
mod look; mod look;
pub mod make;
mod map; mod map;
pub mod movement; pub mod movement;
pub mod open; pub mod open;
@ -188,6 +189,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"gm" => map::VERB, "gm" => map::VERB,
"gmap" => map::VERB, "gmap" => map::VERB,
"make" => make::VERB,
"open" => open::VERB, "open" => open::VERB,
"p" => page::VERB, "p" => page::VERB,

View File

@ -114,14 +114,12 @@ impl UserVerb for Verb {
let user = user_mut(ctx)?; let user = user_mut(ctx)?;
match user.terms.last_presented_term.as_ref() { match user.terms.last_presented_term.as_ref() {
None => { None => {
drop(user);
user_error("There was nothing pending your agreement.".to_owned())?; user_error("There was nothing pending your agreement.".to_owned())?;
} }
Some(last_term) => { Some(last_term) => {
user.terms user.terms
.accepted_terms .accepted_terms
.insert(last_term.to_owned(), Utc::now()); .insert(last_term.to_owned(), Utc::now());
drop(user);
if check_and_notify_accepts(ctx).await? { if check_and_notify_accepts(ctx).await? {
ctx.trans ctx.trans
.queue_for_session( .queue_for_session(

View File

@ -263,7 +263,7 @@ fn compute_new_consent_state(
if Some(&new_consent) == their_target_consent.as_ref() { if Some(&new_consent) == their_target_consent.as_ref() {
match new_consent.fight_consent.as_mut() { match new_consent.fight_consent.as_mut() {
None => (), None => (),
Some(mut m) => { Some(m) => {
m.pending_change = None; m.pending_change = None;
m.status = ConsentStatus::Active; m.status = ConsentStatus::Active;
} }
@ -285,7 +285,7 @@ fn compute_new_consent_state(
None => { None => {
match new_consent.fight_consent.as_mut() { match new_consent.fight_consent.as_mut() {
None => (), None => (),
Some(mut m) => { Some(m) => {
m.status = ConsentStatus::PendingAdd; m.status = ConsentStatus::PendingAdd;
} }
} }

View File

@ -67,7 +67,7 @@ impl UserVerb for Verb {
.any(|al| al.starts_with(&match_item)) .any(|al| al.starts_with(&match_item))
{ {
if offset_remaining <= 1 { if offset_remaining <= 1 {
if let Some(mut user) = ctx.user_dat.as_mut() { if let Some(user) = ctx.user_dat.as_mut() {
if user.credits < stock.list_price { if user.credits < stock.list_price {
user_error( user_error(
"You don't have enough credits to buy that!".to_owned(), "You don't have enough credits to buy that!".to_owned(),
@ -96,7 +96,14 @@ impl UserVerb for Verb {
here already" here already"
.to_owned(), .to_owned(),
)?, )?,
_ => &player_item.location, _ => {
ctx.trans.queue_for_session(
&ctx.session,
Some(
"It's too much for you to carry so you leave it on the ground.\n")
).await?;
&player_item.location
}
} }
} }
_ => &player_item_str, _ => &player_item_str,

View File

@ -108,7 +108,7 @@ pub async fn update_follow_for_failed_movement(
} }
pub fn suspend_follow_for_independent_move(player: &mut Item) { pub fn suspend_follow_for_independent_move(player: &mut Item) {
if let Some(mut following) = player.following.as_mut() { if let Some(following) = player.following.as_mut() {
following.state = FollowState::IfSameRoom; following.state = FollowState::IfSameRoom;
} }
} }

View File

@ -8,7 +8,7 @@ use crate::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
}, },
services::{ services::{
capacity::{check_item_capacity, CapacityLevel}, capacity::{check_item_capacity, recalculate_container_weight, CapacityLevel},
comms::broadcast_to_room, comms::broadcast_to_room,
}, },
static_content::possession_type::possession_data, static_content::possession_type::possession_data,
@ -123,7 +123,7 @@ impl QueueCommandHandler for QueueHandler {
"You try to get it, but your ghostly hands slip through it uselessly".to_owned(), "You try to get it, but your ghostly hands slip through it uselessly".to_owned(),
)?; )?;
} }
let item = match ctx.command { let (item, container_opt) = match ctx.command {
QueueCommand::Get { possession_id } => { QueueCommand::Get { possession_id } => {
let item = match ctx let item = match ctx
.trans .trans
@ -158,7 +158,7 @@ impl QueueCommandHandler for QueueHandler {
Some(&msg_nonexp), Some(&msg_nonexp),
) )
.await?; .await?;
item (item, None)
} }
QueueCommand::GetFromContainer { QueueCommand::GetFromContainer {
from_possession_id, from_possession_id,
@ -210,7 +210,7 @@ impl QueueCommandHandler for QueueHandler {
Some(&msg_nonexp), Some(&msg_nonexp),
) )
.await?; .await?;
item (item, Some(container))
} }
_ => user_error("Unexpected command".to_owned())?, _ => user_error("Unexpected command".to_owned())?,
}; };
@ -235,7 +235,7 @@ impl QueueCommandHandler for QueueHandler {
user_error(format!( user_error(format!(
"{} You can't get {} because it is too heavy!", "{} You can't get {} because it is too heavy!",
if explicit { "Fuck!" } else { "Rats!" }, if explicit { "Fuck!" } else { "Rats!" },
&ctx.item.display_for_sentence(explicit, 1, false) &item.display_for_sentence(explicit, 1, false)
))? ))?
} }
_ => (), _ => (),
@ -245,6 +245,10 @@ impl QueueCommandHandler for QueueHandler {
item_mut.location = ctx.item.refstr(); item_mut.location = ctx.item.refstr();
item_mut.action_type = LocationActionType::Normal; item_mut.action_type = LocationActionType::Normal;
ctx.trans.save_item_model(&item_mut).await?; ctx.trans.save_item_model(&item_mut).await?;
if let Some(container) = container_opt {
recalculate_container_weight(&ctx.trans, &container).await?;
}
Ok(()) Ok(())
} }
} }

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; user_mut.credits -= hire_dat.price;
ctx.trans ctx.trans

View File

@ -2,7 +2,6 @@ use super::{get_player_item_or_fail, UResult, UserVerb, UserVerbRef, VerbContext
use crate::{ use crate::{
language::weight, language::weight,
models::item::{Item, LocationActionType}, models::item::{Item, LocationActionType},
static_content::possession_type::{possession_data, PossessionType},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
@ -46,28 +45,18 @@ impl UserVerb for Verb {
if item.item_type != "possession" { if item.item_type != "possession" {
continue; continue;
} }
if let Some(posdat) = possession_data().get( let it_total = items.iter().map(|it| it.weight).sum();
&item total += it_total;
.possession_type response.push_str(&format!(
.as_ref() "{} [{}]{}\n",
.unwrap_or(&PossessionType::AntennaWhip), item.display_for_sentence(!ctx.session_dat.less_explicit_mode, items.len(), true),
) { weight(it_total),
total += items.len() as u64 * posdat.weight; match item.action_type {
response.push_str(&format!( LocationActionType::Worn => " (worn)",
"{} [{}]{}\n", LocationActionType::Wielded => " (wielded)",
item.display_for_sentence( _ => "",
!ctx.session_dat.less_explicit_mode, }
items.len(), ));
true
),
weight(items.len() as u64 * posdat.weight),
match item.action_type {
LocationActionType::Worn => " (worn)",
LocationActionType::Wielded => " (wielded)",
_ => "",
}
));
}
} }
response.push_str(&format!( response.push_str(&format!(
"Total weight: {} ({} max)\n", "Total weight: {} ({} max)\n",

View File

@ -10,10 +10,10 @@ use crate::{
db::ItemSearchParams, db::ItemSearchParams,
language, language,
models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack}, models::item::{DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, Subattack},
services::combat::max_health, services::{combat::max_health, skills::calc_level_gap},
static_content::{ static_content::{
dynzone, dynzone,
possession_type::possession_data, possession_type::{possession_data, recipe_craft_by_recipe},
room::{self, Direction}, room::{self, Direction},
species::{species_info_map, SpeciesType}, species::{species_info_map, SpeciesType},
}, },
@ -25,7 +25,11 @@ use mockall_double::double;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::sync::Arc; use std::sync::Arc;
pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult<()> { pub async fn describe_normal_item(
player_item: &Item,
ctx: &VerbContext<'_>,
item: &Item,
) -> UResult<()> {
let mut contents_desc = String::new(); let mut contents_desc = String::new();
let mut items = ctx let mut items = ctx
@ -238,6 +242,72 @@ pub async fn describe_normal_item(ctx: &VerbContext<'_>, item: &Item) -> UResult
}; };
contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit)); contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit));
} }
if let Some(recipe_craft_data) = item
.possession_type
.as_ref()
.and_then(|pt| recipe_craft_by_recipe().get(pt))
{
contents_desc.push_str("You will need:\n");
for (input_pt, count) in &recipe_craft_data.craft_data.inputs.iter().counts() {
if let Some(pd) = possession_data().get(&input_pt) {
let thing = if ctx.session_dat.less_explicit_mode {
pd.display_less_explicit.unwrap_or(pd.display)
} else {
pd.display
};
contents_desc.push_str(&format!(
" {} {}\n",
count,
&(if count != &1 {
language::pluralise(thing)
} else {
thing.to_owned()
})
));
}
}
match recipe_craft_data.bench.as_ref() {
None => contents_desc.push_str("You can make this without any special bench.\n"),
Some(bench) => {
if let Some(pd) = possession_data().get(bench) {
contents_desc.push_str(&format!(
"You'll need to make this on a {}.\n",
if ctx.session_dat.less_explicit_mode {
pd.display_less_explicit.unwrap_or(pd.display)
} else {
pd.display
}
))
}
}
}
let diff = calc_level_gap(
&player_item,
&recipe_craft_data.craft_data.skill,
recipe_craft_data.craft_data.difficulty,
);
let challenge_level = if diff > 5.0 {
"You are rather unlikely to succeed in making this."
} else if diff >= 4.0 {
"You're not that likely to succeed in making this, and you're likely to be too confused to learn anything making it."
} else if diff >= 3.0 {
"You've got about a 1/4 chance to succeed at making this, and you might learn something making it."
} else if diff >= 2.0 {
"You've got about a 1/3 chance to succeed at making this, and you might learn something making it."
} else if diff >= 0.0 {
"You've got a less than 50/50 chance to succeed at making this, and you'll probably learn a lot."
} else if diff >= -2.0 {
"You've got a better than 50/50 chance to succeed at making this, and you'll probably learn a lot."
} else if diff >= -3.0 {
"Three out of four times, you'll succeed at making this, and you might still learn something."
} else if diff >= -4.0 {
"Most of the time, you'll succeed at making this, but you'll only rarely learn something new."
} else {
"You're highly likely to succeed at making this, but unlikely to learn anything new."
};
contents_desc.push_str(&format!("{}\n", challenge_level));
}
} }
ctx.trans ctx.trans
@ -568,7 +638,11 @@ impl UserVerb for Verb {
) -> UResult<()> { ) -> UResult<()> {
let player_item = get_player_item_or_fail(ctx).await?; let player_item = get_player_item_or_fail(ctx).await?;
let rem_trim = remaining.trim().to_lowercase(); let mut rem_trim = remaining.trim().to_lowercase();
let rem_orig = rem_trim.clone();
if rem_trim.starts_with("in ") {
rem_trim = rem_trim[3..].trim_start().to_owned();
}
let use_location = if player_item.death_data.is_some() { let use_location = if player_item.death_data.is_some() {
"room/repro_xv_respawn" "room/repro_xv_respawn"
} else { } else {
@ -582,37 +656,58 @@ impl UserVerb for Verb {
.find_item_by_type_code(heretype, herecode) .find_item_by_type_code(heretype, herecode)
.await? .await?
.ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))? .ok_or_else(|| UserError("Sorry, that no longer exists".to_owned()))?
} else if let Some(dir) = Direction::parse(&rem_trim) { } else if let Some(dir) =
match is_door_in_direction(&ctx.trans, &dir, use_location).await? { Direction::parse(&rem_trim).or_else(|| Direction::parse(&rem_orig))
DoorSituation::NoDoor {
| DoorSituation::DoorOutOfRoom { // This is complex because "in" is overloaded, and if this fails, we want
// to also consider if they are looking in a container.
match is_door_in_direction(&ctx.trans, &dir, use_location).await {
Ok(DoorSituation::NoDoor)
| Ok(DoorSituation::DoorOutOfRoom {
state: DoorState { open: true, .. }, state: DoorState { open: true, .. },
.. ..
} })
| DoorSituation::DoorIntoRoom { | Ok(DoorSituation::DoorIntoRoom {
state: DoorState { open: true, .. }, state: DoorState { open: true, .. },
.. ..
} => {} })
DoorSituation::DoorIntoRoom { | Err(UserError(_)) => {}
Ok(DoorSituation::DoorIntoRoom {
state, state,
room_with_door, room_with_door,
.. ..
} => { }) => {
if let Some(rev_dir) = dir.reverse() { if let Some(rev_dir) = dir.reverse() {
return describe_door(ctx, &room_with_door, &state, &rev_dir).await; return describe_door(ctx, &room_with_door, &state, &rev_dir).await;
} }
} }
DoorSituation::DoorOutOfRoom { Ok(DoorSituation::DoorOutOfRoom {
state, state,
room_with_door, room_with_door,
.. ..
} => { }) => {
return describe_door(ctx, &room_with_door, &state, &dir).await; return describe_door(ctx, &room_with_door, &state, &dir).await;
} }
Err(e) => Err(e)?,
}
match direction_to_item(&ctx.trans, use_location, &dir).await {
Ok(Some(item)) => item,
Ok(None) | Err(UserError(_)) => search_item_for_user(
&ctx,
&ItemSearchParams {
include_contents: true,
include_loc_contents: true,
limit: 1,
..ItemSearchParams::base(&player_item, &rem_trim)
},
)
.await
.map_err(|e| match e {
UserError(_) => UserError("There's nothing in that direction".to_owned()),
e => e,
})?,
Err(e) => Err(e)?,
} }
direction_to_item(&ctx.trans, use_location, &dir)
.await?
.ok_or_else(|| UserError("There's nothing in that direction".to_owned()))?
} else if rem_trim == "me" || rem_trim == "self" { } else if rem_trim == "me" || rem_trim == "self" {
player_item.clone() player_item.clone()
} else { } else {
@ -652,7 +747,7 @@ impl UserVerb for Verb {
) )
.await?; .await?;
} else { } else {
describe_normal_item(ctx, &item).await?; describe_normal_item(&player_item, ctx, &item).await?;
} }
Ok(()) Ok(())
} }

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. // Returns false if the move failed.
async fn attempt_move_immediate( async fn attempt_move_immediate(
direction: &Direction, direction: &Direction,
mut ctx: &mut QueuedCommandContext<'_>, ctx: &mut QueuedCommandContext<'_>,
source: &MovementSource, source: &MovementSource,
) -> UResult<bool> { ) -> UResult<bool> {
let use_location = if ctx.item.death_data.is_some() { let use_location = if ctx.item.death_data.is_some() {

View File

@ -59,7 +59,7 @@ impl TaskHandler for SwingShutHandler {
}; };
let mut room_item_mut = (*room_item).clone(); let mut room_item_mut = (*room_item).clone();
let mut door_state = match room_item_mut let door_state = match room_item_mut
.door_states .door_states
.as_mut() .as_mut()
.and_then(|ds| ds.get_mut(&direction)) .and_then(|ds| ds.get_mut(&direction))

View File

@ -3,7 +3,7 @@ use super::{
user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext, user_error, ItemSearchParams, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
}; };
use crate::{ use crate::{
models::item::LocationActionType, models::item::{ItemFlag, LocationActionType},
regular_tasks::queued_command::{ regular_tasks::queued_command::{
queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext,
}, },
@ -215,7 +215,7 @@ impl UserVerb for Verb {
remaining = remaining2; remaining = remaining2;
} }
let (search_what, for_what) = match remaining.split_once(" in ") { let (into_what, for_what) = match remaining.split_once(" in ") {
None => { None => {
user_error(ansi!("Try <bold>put<reset> item <bold>in<reset> container").to_owned())? user_error(ansi!("Try <bold>put<reset> item <bold>in<reset> container").to_owned())?
} }
@ -256,15 +256,58 @@ impl UserVerb for Verb {
.iter() .iter()
.filter(|t| t.action_type.is_visible_in_look()) .filter(|t| t.action_type.is_visible_in_look())
{ {
if target.item_type == into_what.item_type && target.item_code == into_what.item_code {
user_error(
"You briefly ponder whether something can contain itself, but it blows your mind and you give up.".to_owned()
)?;
}
if target.item_type != "possession" { if target.item_type != "possession" {
user_error("You can't put that in something!".to_owned())?; user_error("You can't put that in something!".to_owned())?;
} }
did_anything = true; did_anything = true;
if into_what.flags.contains(&ItemFlag::Bench) && target.flags.contains(&ItemFlag::Book)
{
let pages = ctx.trans.find_items_by_location(&target.refstr()).await?;
if !pages.is_empty() {
ctx.trans
.queue_for_session(&ctx.session,
Some(
&format!("For ease of later use, you decide to rip the pages out of {} before placing them in {}.\n",
&target.display_for_session(&ctx.session_dat),
&into_what.display_for_session(&ctx.session_dat)),
)
).await?;
for page in pages {
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::GetFromContainer {
from_possession_id: target.item_code.clone(),
get_possession_id: page.item_code.clone(),
},
)
.await?;
queue_command(
ctx,
&mut player_item_mut,
&QueueCommand::Put {
container_possession_id: into_what.item_code.clone(),
target_possession_id: page.item_code.clone(),
},
)
.await?;
}
continue;
}
}
queue_command( queue_command(
ctx, ctx,
&mut player_item_mut, &mut player_item_mut,
&QueueCommand::Put { &QueueCommand::Put {
container_possession_id: search_what.item_code.clone(), container_possession_id: into_what.item_code.clone(),
target_possession_id: target.item_code.clone(), target_possession_id: target.item_code.clone(),
}, },
) )

View File

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

View File

@ -2,7 +2,7 @@ use super::{TaskHandler, TaskRunContext};
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::message_handler::user_commands::{ use crate::message_handler::user_commands::{
close, cut, drop, get, improvise, movement, open, put, remove, use_cmd, user_error, wear, close, cut, drop, get, improvise, make, movement, open, put, remove, use_cmd, user_error, wear,
wield, CommandHandlingError, UResult, VerbContext, wield, CommandHandlingError, UResult, VerbContext,
}; };
use crate::message_handler::ListenerSession; use crate::message_handler::ListenerSession;
@ -67,6 +67,11 @@ pub enum QueueCommand {
from_possession_id: String, from_possession_id: String,
get_possession_id: String, get_possession_id: String,
}, },
Make {
bench_possession_id: Option<String>,
instructions_possession_id: String,
already_used: BTreeSet<String>,
},
Movement { Movement {
direction: Direction, direction: Direction,
source: MovementSource, source: MovementSource,
@ -109,6 +114,7 @@ impl QueueCommand {
Drop { .. } => "Drop", Drop { .. } => "Drop",
Get { .. } => "Get", Get { .. } => "Get",
GetFromContainer { .. } => "GetFromContainer", GetFromContainer { .. } => "GetFromContainer",
Make { .. } => "Make",
Movement { .. } => "Movement", Movement { .. } => "Movement",
OpenDoor { .. } => "OpenDoor", OpenDoor { .. } => "OpenDoor",
Put { .. } => "Put", Put { .. } => "Put",
@ -181,6 +187,10 @@ fn queue_command_registry(
"GetFromContainer", "GetFromContainer",
&get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
), ),
(
"Make",
&make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),
),
( (
"Movement", "Movement",
&movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send),

View File

@ -1,10 +1,17 @@
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::drop::consider_expire_job_for_item, message_handler::user_commands::{drop::consider_expire_job_for_item, user_error, UResult},
models::consent::{Consent, ConsentStatus, ConsentType},
models::item::Item, models::item::Item,
static_content::npc::npc_by_code, models::{
consent::{Consent, ConsentStatus, ConsentType},
item::ItemSpecialData,
},
static_content::{
dynzone::{dynzone_by_type, DynzoneType},
npc::npc_by_code,
room::room_map_by_code,
},
DResult, DResult,
}; };
use mockall_double::double; use mockall_double::double;
@ -117,3 +124,28 @@ pub async fn destroy_container(trans: &DBTrans, container: &Item) -> DResult<()>
.await?; .await?;
Ok(()) Ok(())
} }
pub fn require_power(item: &Item) -> UResult<()> {
let result = match item.item_type.as_str() {
"room" => room_map_by_code()
.get(item.item_code.as_str())
.map(|r| r.has_power)
.unwrap_or(false),
"dynroom" => match &item.special_data {
Some(ItemSpecialData::DynroomData {
dynzone_code,
dynroom_code,
}) => DynzoneType::from_str(dynzone_code.as_str())
.and_then(|dzt| dynzone_by_type().get(&dzt))
.and_then(|dz| dz.dyn_rooms.get(dynroom_code.as_str()))
.map(|dr| dr.has_power)
.unwrap_or(false),
_ => false,
},
_ => false,
};
if !result {
user_error("That would require power, and it looks like wireless power distribution is not available here.".to_owned())?;
}
Ok(())
}

View File

@ -54,7 +54,7 @@ pub async fn soak_damage<DamageDist: DamageDistribution>(
let mut total_damage = 0.0; let mut total_damage = 0.0;
for (damage_type, mut damage_amount) in &damage_by_type { for (damage_type, mut damage_amount) in &damage_by_type {
for mut clothing in &mut clothes { for clothing in &mut clothes {
if let Some(soak) = clothing if let Some(soak) = clothing
.possession_type .possession_type
.as_ref() .as_ref()

View File

@ -1,24 +1,32 @@
use crate::{ #[double]
DResult, use crate::db::DBTrans;
models::item::Item, use crate::{models::item::Item, DResult};
};
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans;
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>, pub async fn broadcast_to_room(
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> { trans: &DBTrans,
location: &str,
from_item: Option<&Item>,
message_explicit_ok: &str,
message_nonexplicit: Option<&str>,
) -> DResult<()> {
for item in trans.find_items_by_location(location).await? { for item in trans.find_items_by_location(location).await? {
if item.item_type != "player" || item.death_data.is_some() { if item.item_type != "player" || item.death_data.is_some() {
continue; continue;
} }
if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await? { if let Some((session, session_dat)) = trans.find_session_for_player(&item.item_code).await?
if session_dat.less_explicit_mode && Some(&item.item_code) != from_item.map(|i| &i.item_code) { {
if session_dat.less_explicit_mode
&& Some(&item.item_code) != from_item.map(|i| &i.item_code)
{
if let Some(msg) = message_nonexplicit { if let Some(msg) = message_nonexplicit {
trans.queue_for_session(&session, Some(msg)).await?; trans.queue_for_session(&session, Some(msg)).await?;
} }
return Ok(()); return Ok(());
} }
trans.queue_for_session(&session, Some(message_explicit_ok)).await?; trans
.queue_for_session(&session, Some(message_explicit_ok))
.await?;
} }
} }
Ok(()) Ok(())

View File

@ -1,41 +1,29 @@
use super::{combat::change_health, comms::broadcast_to_room};
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
models::{ models::{
item::Item, item::Item,
task::{ task::{Task, TaskDetails, TaskMeta},
Task,
TaskMeta,
TaskDetails,
}
}, },
regular_tasks::{TaskHandler, TaskRunContext},
static_content::possession_type::UseEffect,
DResult, DResult,
static_content::{
possession_type::UseEffect,
},
regular_tasks::{
TaskHandler,
TaskRunContext,
}
};
use super::{
comms::broadcast_to_room,
combat::change_health,
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::time;
use serde::{Serialize, Deserialize};
use std::collections::{BTreeMap, VecDeque};
use chrono::Utc; use chrono::Utc;
use log::info; use log::info;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans; use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, VecDeque};
use std::time;
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
pub struct DelayedHealthEffect { pub struct DelayedHealthEffect {
magnitude: i64, magnitude: i64,
delay: u64, delay: u64,
message: String, message: String,
message_nonexp: String message_nonexp: String,
} }
pub struct DelayedHealthTaskHandler; pub struct DelayedHealthTaskHandler;
@ -43,70 +31,109 @@ pub struct DelayedHealthTaskHandler;
impl TaskHandler for DelayedHealthTaskHandler { impl TaskHandler for DelayedHealthTaskHandler {
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
let ref mut item_effect_series = match &mut ctx.task.details { let ref mut item_effect_series = match &mut ctx.task.details {
TaskDetails::DelayedHealth { item, ref mut effect_series } => (item, effect_series), TaskDetails::DelayedHealth {
_ => Err("Expected DelayedHealth type")? item,
ref mut effect_series,
} => (item, effect_series),
_ => Err("Expected DelayedHealth type")?,
}; };
let (item_type, item_code) = match item_effect_series.0.split_once("/") { let (item_type, item_code) = match item_effect_series.0.split_once("/") {
None => { None => {
info!("Invalid item {} to DelayedHealthTaskHandler", item_effect_series.0); info!(
"Invalid item {} to DelayedHealthTaskHandler",
item_effect_series.0
);
return Ok(None); return Ok(None);
} }
Some((item_type, item_code)) => (item_type, item_code) Some((item_type, item_code)) => (item_type, item_code),
}; };
let item = match ctx.trans.find_item_by_type_code(item_type, item_code).await? { let item = match ctx
.trans
.find_item_by_type_code(item_type, item_code)
.await?
{
None => { None => {
return Ok(None); return Ok(None);
} }
Some(it) => it Some(it) => it,
}; };
if item.death_data.is_some() { if item.death_data.is_some() {
return Ok(None); return Ok(None);
} }
match item_effect_series.1.pop_front() { match item_effect_series.1.pop_front() {
None => Ok(None), None => Ok(None),
Some(DelayedHealthEffect { magnitude, message, message_nonexp, .. }) => { Some(DelayedHealthEffect {
magnitude,
message,
message_nonexp,
..
}) => {
let mut item_mut = (*item).clone(); let mut item_mut = (*item).clone();
change_health(ctx.trans, magnitude, &mut item_mut, &message, &message_nonexp).await?; change_health(
ctx.trans,
magnitude,
&mut item_mut,
&message,
&message_nonexp,
)
.await?;
ctx.trans.save_item_model(&item_mut).await?; ctx.trans.save_item_model(&item_mut).await?;
Ok(item_effect_series.1.front().map(|it| time::Duration::from_secs(it.delay))) Ok(item_effect_series
.1
.front()
.map(|it| time::Duration::from_secs(it.delay)))
} }
} }
} }
} }
pub static DELAYED_HEALTH_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DelayedHealthTaskHandler; pub static DELAYED_HEALTH_HANDLER: &'static (dyn TaskHandler + Sync + Send) =
&DelayedHealthTaskHandler;
pub async fn run_effects( pub async fn run_effects(
trans: &DBTrans, effects: &Vec<UseEffect>, trans: &DBTrans,
effects: &Vec<UseEffect>,
player: &mut Item, player: &mut Item,
item: &Item, item: &Item,
// None if target is player // None if target is player
target: &mut Option<Item>, target: &mut Option<Item>,
level: f64, level: f64,
task_ref: &str task_ref: &str,
) -> DResult<()> { ) -> DResult<()> {
let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::new(); let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::new();
for effect in effects { for effect in effects {
match effect { match effect {
UseEffect::BroadcastMessage { messagef } => { UseEffect::BroadcastMessage { messagef } => {
let (msg_exp, msg_nonexp) = messagef(player, item, target.as_ref().unwrap_or(player)); let (msg_exp, msg_nonexp) =
broadcast_to_room(trans, &player.location, None, &msg_exp, messagef(player, item, target.as_ref().unwrap_or(player));
Some(&msg_nonexp)).await?; broadcast_to_room(trans, &player.location, None, &msg_exp, Some(&msg_nonexp))
}, .await?;
UseEffect::ChangeTargetHealth { delay_secs, base_effect, skill_multiplier, max_effect, }
message } => { UseEffect::ChangeTargetHealth {
delay_secs,
base_effect,
skill_multiplier,
max_effect,
message,
} => {
let health_impact = let health_impact =
(*base_effect + ((skill_multiplier * level) as i64).min(*max_effect)) as i64; (*base_effect + ((skill_multiplier * level) as i64).min(*max_effect)) as i64;
let (msg, msg_nonexp) = message(target.as_ref().unwrap_or(player)); let (msg, msg_nonexp) = message(target.as_ref().unwrap_or(player));
if *delay_secs == 0 { if *delay_secs == 0 {
change_health(trans, health_impact, target.as_mut().unwrap_or(player), &msg, change_health(
&msg_nonexp).await?; trans,
health_impact,
target.as_mut().unwrap_or(player),
&msg,
&msg_nonexp,
)
.await?;
} else { } else {
let target_it = target.as_ref().unwrap_or(player); let target_it = target.as_ref().unwrap_or(player);
let fx = DelayedHealthEffect { let fx = DelayedHealthEffect {
magnitude: health_impact, magnitude: health_impact,
delay: *delay_secs, delay: *delay_secs,
message: msg, message: msg,
message_nonexp: msg_nonexp message_nonexp: msg_nonexp,
}; };
target_health_series target_health_series
.entry(format!("{}/{}", target_it.item_type, target_it.item_code)) .entry(format!("{}/{}", target_it.item_type, target_it.item_code))
@ -118,17 +145,19 @@ pub async fn run_effects(
} }
for (eff_item, l) in target_health_series.into_iter() { for (eff_item, l) in target_health_series.into_iter() {
trans.upsert_task(&Task { trans
meta: TaskMeta { .upsert_task(&Task {
task_code: format!("{}/{}", eff_item, task_ref), meta: TaskMeta {
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64), task_code: format!("{}/{}", eff_item, task_ref),
..Default::default() next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
}, ..Default::default()
details: TaskDetails::DelayedHealth { },
effect_series: l, details: TaskDetails::DelayedHealth {
item: eff_item, effect_series: l,
} item: eff_item,
}).await?; },
})
.await?;
} }
Ok(()) Ok(())

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). // If the skill gap is 1, probability of learning is 0.4 (20% less), and so on (exponential decrease).
const LAMBDA: f64 = -0.2231435513142097; // log 0.8 const LAMBDA: f64 = -0.2231435513142097; // log 0.8
if who.item_type == "player" if who.item_type == "player"
&& rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap as f64)).exp() && rand::thread_rng().gen::<f64>() < 0.5 * (LAMBDA * (gap.abs() as f64)).exp()
{ {
if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? { if let Some((sess, _sess_dat)) = trans.find_session_for_player(&who.item_code).await? {
if let Some(mut user) = trans.find_by_username(&who.item_code).await? { if let Some(mut user) = trans.find_by_username(&who.item_code).await? {

View File

@ -3,32 +3,33 @@
// dynamically. They can dynamically connect to the grid. // dynamically. They can dynamically connect to the grid.
// Apartments, planes, and boats are all expected to be specific instances of dynzones. // Apartments, planes, and boats are all expected to be specific instances of dynzones.
use super::room::{Direction, GridCoords}; use super::room::{Direction, GridCoords};
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{user_error, UResult}, message_handler::user_commands::{user_error, UResult},
models::item::{Item, ItemFlag, ItemSpecialData, DynamicEntrance, DoorState} models::item::{DoorState, DynamicEntrance, Item, ItemFlag, ItemSpecialData},
}; };
use mockall_double::double;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use mockall_double::double;
#[double] use crate::db::DBTrans;
mod cokmurl_apartment; mod cokmurl_apartment;
#[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)] #[derive(Eq, Clone, PartialEq, Ord, PartialOrd, Debug)]
pub enum DynzoneType { pub enum DynzoneType {
CokMurlApartment CokMurlApartment,
} }
impl DynzoneType { impl DynzoneType {
pub fn from_str(i: &str) -> Option<Self> { pub fn from_str(i: &str) -> Option<Self> {
match i { match i {
"CokMurlApartment" => Some(DynzoneType::CokMurlApartment), "CokMurlApartment" => Some(DynzoneType::CokMurlApartment),
_ => None _ => None,
} }
} }
pub fn to_str(&self) -> &'static str { pub fn to_str(&self) -> &'static str {
match self { match self {
DynzoneType::CokMurlApartment => "CokMurlApartment" DynzoneType::CokMurlApartment => "CokMurlApartment",
} }
} }
} }
@ -43,43 +44,51 @@ pub struct Dynzone {
impl Dynzone { impl Dynzone {
// Returns None if there is already an instance in the same exit direction. // Returns None if there is already an instance in the same exit direction.
pub async fn create_instance(&self, trans: &DBTrans, connect_where: &str, dup_message: &str, pub async fn create_instance(
new_owner: &Item, new_exit_direction: &Direction) -> UResult<String> { &self,
trans: &DBTrans,
connect_where: &str,
dup_message: &str,
new_owner: &Item,
new_exit_direction: &Direction,
) -> UResult<String> {
// Check exit not taken... // Check exit not taken...
if trans.find_exact_dyn_exit(connect_where, new_exit_direction).await?.is_some() { if trans
.find_exact_dyn_exit(connect_where, new_exit_direction)
.await?
.is_some()
{
user_error(dup_message.to_string())?; user_error(dup_message.to_string())?;
} }
let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code); let owner = format!("{}/{}", &new_owner.item_type, &new_owner.item_code);
let code = format!("{}", &trans.alloc_item_code().await?); let code = format!("{}", &trans.alloc_item_code().await?);
trans.create_item( trans
&Item { .create_item(&Item {
item_type: "dynzone".to_owned(), item_type: "dynzone".to_owned(),
item_code: code.clone(), item_code: code.clone(),
display: self.zonename.to_owned(), display: self.zonename.to_owned(),
special_data: Some( special_data: Some(ItemSpecialData::DynzoneData {
ItemSpecialData::DynzoneData { zone_exit: Some(connect_where.to_owned()),
zone_exit: Some(connect_where.to_owned()), vacate_after: None,
vacate_after: None }),
}
),
owner: Some(owner.clone()), owner: Some(owner.clone()),
location: format!("dynzone/{}", &code), location: format!("dynzone/{}", &code),
..Default::default() ..Default::default()
} })
).await?; .await?;
let mut should_connect = true; let mut should_connect = true;
for (_, room) in &self.dyn_rooms { for (_, room) in &self.dyn_rooms {
let roomcode = format!("{}/{}", &code, room.subcode); let roomcode = format!("{}/{}", &code, room.subcode);
let will_connect = should_connect && let will_connect = should_connect
room.exits.iter().any(|r| match r.target { && room.exits.iter().any(|r| match r.target {
ExitTarget::ExitZone => true, ExitTarget::ExitZone => true,
_ => false _ => false,
}); });
should_connect &= !will_connect; should_connect &= !will_connect;
trans.create_item( trans
&Item { .create_item(&Item {
item_type: "dynroom".to_owned(), item_type: "dynroom".to_owned(),
item_code: roomcode, item_code: roomcode,
display: room.name.to_owned(), display: room.name.to_owned(),
@ -88,31 +97,41 @@ impl Dynzone {
location: format!("dynzone/{}", &code), location: format!("dynzone/{}", &code),
special_data: Some(ItemSpecialData::DynroomData { special_data: Some(ItemSpecialData::DynroomData {
dynzone_code: self.zonetype.to_str().to_owned(), dynzone_code: self.zonetype.to_str().to_owned(),
dynroom_code: room.subcode.to_owned() dynroom_code: room.subcode.to_owned(),
}), }),
dynamic_entrance: if will_connect { dynamic_entrance: if will_connect {
Some(DynamicEntrance { Some(DynamicEntrance {
direction: new_exit_direction.clone(), direction: new_exit_direction.clone(),
source_item: connect_where.to_owned() source_item: connect_where.to_owned(),
}) })
} else { None }, } else {
None
},
flags: room.item_flags.clone(), flags: room.item_flags.clone(),
owner: Some(owner.clone()), owner: Some(owner.clone()),
door_states: Some(room.exits.iter() door_states: Some(
.filter_map(|ex| room.exits
if let ExitType::Doored { description } = ex.exit_type { .iter()
Some((ex.direction.clone(), DoorState { .filter_map(|ex| {
open: false, if let ExitType::Doored { description } = ex.exit_type {
description: description.to_owned() Some((
})) ex.direction.clone(),
} else { DoorState {
None open: false,
}).collect()), description: description.to_owned(),
},
))
} else {
None
}
})
.collect(),
),
..Default::default() ..Default::default()
} })
).await?; .await?;
} }
Ok(format!("dynzone/{}", &code)) Ok(format!("dynzone/{}", &code))
} }
} }
@ -161,6 +180,7 @@ pub struct Dynroom {
pub should_caption: bool, pub should_caption: bool,
pub item_flags: Vec<ItemFlag>, pub item_flags: Vec<ItemFlag>,
pub grid_coords: GridCoords, pub grid_coords: GridCoords,
pub has_power: bool,
} }
impl Default for Dynroom { impl Default for Dynroom {
@ -171,55 +191,61 @@ impl Default for Dynroom {
short: "XX", short: "XX",
description: "A generic room", description: "A generic room",
description_less_explicit: None, description_less_explicit: None,
exits: vec!(), exits: vec![],
should_caption: false, should_caption: false,
item_flags: vec!(), item_flags: vec![],
grid_coords: GridCoords { x: 0, y: 0, z: 0 }, grid_coords: GridCoords { x: 0, y: 0, z: 0 },
has_power: true,
} }
} }
} }
pub fn dynzone_list() -> &'static Vec<Dynzone> { pub fn dynzone_list() -> &'static Vec<Dynzone> {
static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new(); static CELL: OnceCell<Vec<Dynzone>> = OnceCell::new();
CELL.get_or_init( CELL.get_or_init(|| vec![cokmurl_apartment::zone()])
|| vec!(
cokmurl_apartment::zone()
)
)
} }
pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> { pub fn dynzone_by_type() -> &'static BTreeMap<&'static DynzoneType, Dynzone> {
static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new(); static CELL: OnceCell<BTreeMap<&'static DynzoneType, Dynzone>> = OnceCell::new();
CELL.get_or_init( CELL.get_or_init(|| {
|| dynzone_list().iter().map(|z| (&z.zonetype, (*z).clone())).collect() dynzone_list()
) .iter()
.map(|z| (&z.zonetype, (*z).clone()))
.collect()
})
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::super::room::Direction;
use super::{dynzone_list, DynzoneType, ExitTarget};
use itertools::Itertools; use itertools::Itertools;
use super::super::room::{Direction};
use super::{dynzone_list, ExitTarget, DynzoneType};
#[test] #[test]
fn dynzone_types_unique() { fn dynzone_types_unique() {
let mut sorted_list = dynzone_list().clone(); let mut sorted_list = dynzone_list().clone();
sorted_list.sort(); sorted_list.sort();
assert_eq!(Vec::<(&DynzoneType, usize)>::new(), assert_eq!(
sorted_list.iter() Vec::<(&DynzoneType, usize)>::new(),
.group_by(|v| &v.zonetype) sorted_list
.into_iter() .iter()
.map(|v| (v.0, v.1.count())) .group_by(|v| &v.zonetype)
.filter(|v| v.1 > 1) .into_iter()
.collect::<Vec<(&DynzoneType, usize)>>() .map(|v| (v.0, v.1.count()))
); .filter(|v| v.1 > 1)
.collect::<Vec<(&DynzoneType, usize)>>()
);
} }
#[test] #[test]
fn dynroom_codes_match_struct() { fn dynroom_codes_match_struct() {
for dynzone in dynzone_list() { for dynzone in dynzone_list() {
assert_eq!( assert_eq!(
dynzone.dyn_rooms.iter().filter(|v| *v.0 != v.1.subcode) dynzone
.map(|v| *v.0).collect::<Vec<&str>>(), .dyn_rooms
.iter()
.filter(|v| *v.0 != v.1.subcode)
.map(|v| *v.0)
.collect::<Vec<&str>>(),
Vec::<&str>::new() Vec::<&str>::new()
); );
} }
@ -228,25 +254,39 @@ mod test {
#[test] #[test]
fn dynzone_has_dynroom() { fn dynzone_has_dynroom() {
for dynzone in dynzone_list() { for dynzone in dynzone_list() {
assert_ne!(0, dynzone.dyn_rooms.len(), "# rooms in zone {}", assert_ne!(
dynzone.zonetype.to_str()) 0,
dynzone.dyn_rooms.len(),
"# rooms in zone {}",
dynzone.zonetype.to_str()
)
} }
} }
#[test] #[test]
fn dynroom_exits_subcodes_exists() { fn dynroom_exits_subcodes_exists() {
for dynzone in dynzone_list() { for dynzone in dynzone_list() {
for dynroom in dynzone.dyn_rooms.iter() { for dynroom in dynzone.dyn_rooms.iter() {
let exits = dynroom.1.exits.iter().filter( let exits = dynroom
|ex| .1
if let ExitTarget::Intrazone { subcode } = ex.target { .exits
!dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode) .iter()
} else { .filter(|ex| {
false if let ExitTarget::Intrazone { subcode } = ex.target {
}).map(|ex| &ex.direction).collect::<Vec<&Direction>>(); !dynzone.dyn_rooms.iter().any(|r| r.1.subcode == subcode)
assert_eq!(Vec::<&Direction>::new(), exits, } else {
"exits to invalid subcode in room {} in zone {}", dynroom.0, false
dynzone.zonetype.to_str()); }
})
.map(|ex| &ex.direction)
.collect::<Vec<&Direction>>();
assert_eq!(
Vec::<&Direction>::new(),
exits,
"exits to invalid subcode in room {} in zone {}",
dynroom.0,
dynzone.zonetype.to_str()
);
} }
} }
} }

View File

@ -1,14 +1,6 @@
use super::{ use super::{super::room::GridCoords, Dynroom, Dynzone, DynzoneType, Exit, ExitTarget, ExitType};
Dynzone,
DynzoneType,
Dynroom,
Exit,
ExitTarget,
ExitType,
super::room::GridCoords
};
use crate::static_content::room::Direction;
use crate::models::item::ItemFlag; use crate::models::item::ItemFlag;
use crate::static_content::room::Direction;
pub fn zone() -> Dynzone { pub fn zone() -> Dynzone {
Dynzone { Dynzone {
@ -54,6 +46,7 @@ pub fn zone() -> Dynzone {
), ),
grid_coords: GridCoords { x: 1, y: 0, z: 0 }, grid_coords: GridCoords { x: 1, y: 0, z: 0 },
should_caption: true, should_caption: true,
has_power: true,
item_flags: vec!(ItemFlag::DroppedItemsDontExpire, item_flags: vec!(ItemFlag::DroppedItemsDontExpire,
ItemFlag::PrivatePlace), ItemFlag::PrivatePlace),
..Default::default() ..Default::default()

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; use super::species::SpeciesType;
#[double]
use crate::db::DBTrans;
use crate::{
models::{item::Item, journal::JournalType, user::User},
DResult,
};
use async_trait::async_trait;
use itertools::Itertools; use itertools::Itertools;
use log::warn;
use mockall_double::double;
use once_cell::sync::OnceCell;
use std::collections::BTreeMap;
mod first_dog; mod first_dog;
@ -31,7 +28,7 @@ pub trait JournalChecker {
trans: &DBTrans, trans: &DBTrans,
user: &mut User, user: &mut User,
player: &mut Item, player: &mut Item,
victim: &Item victim: &Item,
) -> DResult<bool>; ) -> DResult<bool>;
} }
@ -39,7 +36,6 @@ pub struct JournalData {
name: &'static str, name: &'static str,
details: &'static str, details: &'static str,
xp: u64, xp: u64,
} }
pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> { pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
@ -55,34 +51,32 @@ pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.", details: "dying for the first time. Fortunately, you can come back by recloning in to a fresh body, just with fewer credits, a bit less experience, and a bruised ego! All your stuff is still on your body, so better go find it, or give up on it.",
xp: 150 xp: 150
}) })
).into_iter().collect()) ).into_iter().collect());
} }
pub fn journal_checkers() -> &'static Vec<&'static (dyn JournalChecker + Sync + Send)> { pub fn journal_checkers() -> &'static Vec<&'static (dyn JournalChecker + Sync + Send)> {
static CHECKERS: OnceCell<Vec<&'static (dyn JournalChecker + Sync + Send)>> = OnceCell::new(); static CHECKERS: OnceCell<Vec<&'static (dyn JournalChecker + Sync + Send)>> = OnceCell::new();
CHECKERS.get_or_init(|| vec!( CHECKERS.get_or_init(|| vec![&first_dog::CHECKER])
&first_dog::CHECKER
))
} }
pub fn checkers_by_species() -> pub fn checkers_by_species(
&'static BTreeMap<SpeciesType, ) -> &'static BTreeMap<SpeciesType, Vec<&'static (dyn JournalChecker + Sync + Send)>> {
Vec<&'static (dyn JournalChecker + Sync + Send)>> static MAP: OnceCell<BTreeMap<SpeciesType, Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
{
static MAP: OnceCell<BTreeMap<SpeciesType,
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
OnceCell::new(); OnceCell::new();
MAP.get_or_init(|| { MAP.get_or_init(|| {
let species_groups = journal_checkers().iter().flat_map( let species_groups = journal_checkers()
|jc| .iter()
jc.kill_subscriptions().into_iter() .flat_map(|jc| {
.filter_map(|sub| jc.kill_subscriptions()
match sub { .into_iter()
KillSubscriptionType::SpecificNPCSpecies { species } => .filter_map(|sub| match sub {
Some((species.clone(), jc.clone())), KillSubscriptionType::SpecificNPCSpecies { species } => {
_ => None Some((species.clone(), *jc))
}) }
).group_by(|v| v.0.clone()); _ => None,
})
})
.group_by(|v| v.0.clone());
species_groups species_groups
.into_iter() .into_iter()
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect())) .map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
@ -90,24 +84,22 @@ pub fn checkers_by_species() ->
}) })
} }
pub fn checkers_by_npc() -> pub fn checkers_by_npc(
&'static BTreeMap<&'static str, ) -> &'static BTreeMap<&'static str, Vec<&'static (dyn JournalChecker + Sync + Send)>> {
Vec<&'static (dyn JournalChecker + Sync + Send)>> static MAP: OnceCell<BTreeMap<&'static str, Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
{
static MAP: OnceCell<BTreeMap<&'static str,
Vec<&'static (dyn JournalChecker + Sync + Send)>>> =
OnceCell::new(); OnceCell::new();
MAP.get_or_init(|| { MAP.get_or_init(|| {
let npc_groups = journal_checkers().iter().flat_map( let npc_groups = journal_checkers()
|jc| .iter()
jc.kill_subscriptions().into_iter() .flat_map(|jc| {
.filter_map(|sub| jc.kill_subscriptions()
match sub { .into_iter()
KillSubscriptionType::SpecificNPC { code } => .filter_map(|sub| match sub {
Some((code.clone(), jc.clone())), KillSubscriptionType::SpecificNPC { code } => Some((code.clone(), *jc)),
_ => None _ => None,
}) })
).group_by(|v| v.0.clone()); })
.group_by(|v| v.0.clone());
npc_groups npc_groups
.into_iter() .into_iter()
.map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect())) .map(|(species, g)| (species, g.into_iter().map(|v| v.1).collect()))
@ -115,61 +107,81 @@ pub fn checkers_by_npc() ->
}) })
} }
pub async fn award_journal_if_needed(trans: &DBTrans, pub async fn award_journal_if_needed(
user: &mut User, trans: &DBTrans,
player: &mut Item, user: &mut User,
journal: JournalType) -> DResult<bool> { player: &mut Item,
if user.experience.journals.completed_journals.contains(&journal) { journal: JournalType,
) -> DResult<bool> {
if user
.experience
.journals
.completed_journals
.contains(&journal)
{
return Ok(false); return Ok(false);
} }
let journal_data = match journal_types().get(&journal) { let journal_data = match journal_types().get(&journal) {
None => { None => {
warn!("Tried to award journal type {:#?} that doesn't exist.", &journal); warn!(
"Tried to award journal type {:#?} that doesn't exist.",
&journal
);
return Ok(false); return Ok(false);
}, }
Some(v) => v Some(v) => v,
}; };
user.experience.journals.completed_journals.insert(journal); user.experience.journals.completed_journals.insert(journal);
// Note: Not counted as 'change for this reroll' since it is permanent. // Note: Not counted as 'change for this reroll' since it is permanent.
player.total_xp += journal_data.xp; player.total_xp += journal_data.xp;
if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? { if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? {
trans.queue_for_session( trans
&sess, .queue_for_session(
Some(&format!("Journal earned: {} - You earned {} XP for {}\n", &sess,
journal_data.name, journal_data.xp, journal_data.details) Some(&format!(
)).await?; "Journal earned: {} - You earned {} XP for {}\n",
journal_data.name, journal_data.xp, journal_data.details
)),
)
.await?;
} }
Ok(true) Ok(true)
} }
pub async fn check_journal_for_kill(trans: &DBTrans, pub async fn check_journal_for_kill(
player: &mut Item, trans: &DBTrans,
victim: &Item) -> DResult<bool> { player: &mut Item,
victim: &Item,
) -> DResult<bool> {
if player.item_type != "player" { if player.item_type != "player" {
return Ok(false); return Ok(false);
} }
let mut user = match trans.find_by_username(&player.item_code).await? { let mut user = match trans.find_by_username(&player.item_code).await? {
None => return Ok(false), None => return Ok(false),
Some(u) => u Some(u) => u,
}; };
let mut did_work = false; let mut did_work = false;
if let Some(checkers) = checkers_by_species().get(&victim.species) { if let Some(checkers) = checkers_by_species().get(&victim.species) {
for checker in checkers { for checker in checkers {
did_work = did_work || did_work = did_work
checker.handle_kill(trans, &mut user, player, victim).await?; || checker
.handle_kill(trans, &mut user, player, victim)
.await?;
} }
} }
if let Some(checkers) = checkers_by_npc().get(victim.item_code.as_str()) { if let Some(checkers) = checkers_by_npc().get(victim.item_code.as_str()) {
for checker in checkers { for checker in checkers {
did_work = did_work || did_work = did_work
checker.handle_kill(trans, &mut user, player, victim).await?; || checker
.handle_kill(trans, &mut user, player, victim)
.await?;
} }
} }
if did_work { if did_work {
trans.save_user_model(&user).await?; trans.save_user_model(&user).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::{ use crate::{
DResult, models::{item::Item, journal::JournalType, user::User},
static_content::species::SpeciesType, static_content::species::SpeciesType,
models::{ DResult,
user::User,
item::Item,
journal::JournalType,
}
}; };
use async_trait::async_trait; use async_trait::async_trait;
use mockall_double::double; use mockall_double::double;
#[double] use crate::db::DBTrans;
pub struct FirstDogChecker; pub struct FirstDogChecker;
#[async_trait] #[async_trait]
impl JournalChecker for FirstDogChecker { impl JournalChecker for FirstDogChecker {
fn kill_subscriptions(&self) -> Vec<KillSubscriptionType> { fn kill_subscriptions(&self) -> Vec<KillSubscriptionType> {
vec!( vec![KillSubscriptionType::SpecificNPCSpecies {
KillSubscriptionType::SpecificNPCSpecies { species: SpeciesType::Dog,
species: SpeciesType::Dog }]
}
)
} }
async fn handle_kill( async fn handle_kill(
@ -28,7 +23,7 @@ impl JournalChecker for FirstDogChecker {
trans: &DBTrans, trans: &DBTrans,
user: &mut User, user: &mut User,
player: &mut Item, player: &mut Item,
_victim: &Item _victim: &Item,
) -> DResult<bool> { ) -> DResult<bool> {
award_journal_if_needed(trans, user, player, JournalType::SlayedMeanDog).await award_journal_if_needed(trans, user, player, JournalType::SlayedMeanDog).await
} }

View File

@ -1,16 +1,21 @@
#[double]
use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{UResult, VerbContext}, message_handler::user_commands::{UResult, VerbContext},
models::consent::ConsentType, models::consent::ConsentType,
models::item::{Item, Pronouns, SkillType}, models::item::{Item, ItemFlag, Pronouns, SkillType},
regular_tasks::queued_command::QueuedCommandContext, regular_tasks::queued_command::QueuedCommandContext,
static_content::{room::Direction, species::BodyPart}, static_content::{room::Direction, species::BodyPart},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use mockall_double::double;
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet}; use std::collections::{BTreeMap, BTreeSet};
mod bags;
mod benches;
mod blade; mod blade;
mod books; mod books;
mod corp_licence; mod corp_licence;
@ -256,6 +261,11 @@ impl ContainerCheck for PermissiveContainerCheck {
} }
} }
#[async_trait]
pub trait BenchData {
async fn check_make(&self, trans: &DBTrans, bench: &Item, recipe: &Item) -> UResult<()>;
}
pub struct ContainerData { pub struct ContainerData {
pub max_weight: u64, pub max_weight: u64,
pub base_weight: u64, pub base_weight: u64,
@ -293,8 +303,10 @@ pub struct PossessionData {
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>, pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>, pub write_handler: Option<&'static (dyn WriteHandler + Sync + Send)>,
pub can_butcher: bool, pub can_butcher: bool,
pub bench_data: Option<&'static (dyn BenchData + Sync + Send)>,
pub wear_data: Option<WearData>, pub wear_data: Option<WearData>,
pub container_data: Option<ContainerData>, pub container_data: Option<ContainerData>,
pub default_flags: Vec<ItemFlag>,
} }
impl Default for PossessionData { impl Default for PossessionData {
@ -316,8 +328,10 @@ impl Default for PossessionData {
sign_handler: None, sign_handler: None,
write_handler: None, write_handler: None,
can_butcher: false, can_butcher: false,
bench_data: None,
wear_data: None, wear_data: None,
container_data: None, container_data: None,
default_flags: vec![],
} }
} }
} }
@ -346,7 +360,7 @@ impl WeaponAttackData {
} }
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PossessionType { pub enum PossessionType {
// Special values that substitute for possessions. // Special values that substitute for possessions.
Fangs, // Default weapon for certain animals Fangs, // Default weapon for certain animals
@ -367,12 +381,18 @@ pub enum PossessionType {
// Corporate // Corporate
NewCorpLicence, NewCorpLicence,
CertificateOfIncorporation, CertificateOfIncorporation,
// Storage
DuffelBag,
// Security // Security
Scanlock, Scanlock,
// Crafting // Food
GrilledSteak,
// Crafting inputs
Steak, Steak,
AnimalSkin, AnimalSkin,
SeveredHead, SeveredHead,
// Craft benches
KitchenStove,
// Recipes // Recipes
CulinaryEssentials, CulinaryEssentials,
GrilledSteakRecipe, GrilledSteakRecipe,
@ -395,6 +415,7 @@ impl Into<Item> for PossessionType {
.collect(), .collect(),
health: possession_dat.max_health, health: possession_dat.max_health,
weight: possession_dat.weight, weight: possession_dat.weight,
flags: possession_dat.default_flags.clone(),
pronouns: Pronouns { pronouns: Pronouns {
is_proper: false, is_proper: false,
..Pronouns::default_inanimate() ..Pronouns::default_inanimate()
@ -446,6 +467,8 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
vec![(Fangs, fangs::data())] vec![(Fangs, fangs::data())]
.into_iter() .into_iter()
.chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(whip::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(bags::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(benches::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(blade::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain( .chain(
@ -509,6 +532,7 @@ pub fn can_butcher_possessions() -> &'static Vec<PossessionType> {
}) })
} }
#[derive(PartialEq, Debug, Clone)]
pub struct CraftData { pub struct CraftData {
pub skill: SkillType, pub skill: SkillType,
pub difficulty: f64, pub difficulty: f64,
@ -516,6 +540,12 @@ pub struct CraftData {
pub output: PossessionType, pub output: PossessionType,
} }
pub struct RecipeCraftData {
pub craft_data: CraftData,
pub recipe: PossessionType,
pub bench: Option<PossessionType>,
}
pub fn improv_table() -> &'static Vec<CraftData> { pub fn improv_table() -> &'static Vec<CraftData> {
static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new(); static IMPROV_CELL: OnceCell<Vec<CraftData>> = OnceCell::new();
IMPROV_CELL.get_or_init(|| { IMPROV_CELL.get_or_init(|| {
@ -563,6 +593,32 @@ pub fn improv_by_output() -> &'static BTreeMap<PossessionType, &'static CraftDat
}) })
} }
pub fn recipe_craft_table() -> &'static Vec<RecipeCraftData> {
static RECIPE_CELL: OnceCell<Vec<RecipeCraftData>> = OnceCell::new();
RECIPE_CELL.get_or_init(|| {
vec![RecipeCraftData {
craft_data: CraftData {
skill: SkillType::Craft,
difficulty: 5.0,
inputs: vec![PossessionType::Steak],
output: PossessionType::GrilledSteak,
},
recipe: PossessionType::GrilledSteakRecipe,
bench: Some(PossessionType::KitchenStove),
}]
})
}
pub fn recipe_craft_by_recipe() -> &'static BTreeMap<PossessionType, &'static RecipeCraftData> {
static MAP_CELL: OnceCell<BTreeMap<PossessionType, &'static RecipeCraftData>> = OnceCell::new();
MAP_CELL.get_or_init(|| {
recipe_craft_table()
.iter()
.map(|rcd| (rcd.recipe.clone(), rcd))
.collect()
})
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use itertools::Itertools; use itertools::Itertools;
@ -624,6 +680,55 @@ mod tests {
); );
} }
#[test]
fn every_improv_item_has_possession_data_for_inputs() {
assert_eq!(
improv_table()
.iter()
.flat_map(|cd| cd.inputs.iter())
.filter_map(|inp| if possession_data().get(inp).is_none() {
Some(inp)
} else {
None
})
.collect::<Vec<&'static PossessionType>>(),
Vec::<&'static PossessionType>::new()
);
}
#[test]
fn every_recipe_has_possession_data_for_output() {
assert_eq!(
recipe_craft_table()
.iter()
.filter_map(
|rcd| if possession_data().get(&rcd.craft_data.output).is_none() {
Some(&rcd.craft_data.output)
} else {
None
}
)
.collect::<Vec<&'static PossessionType>>(),
Vec::<&'static PossessionType>::new()
);
}
#[test]
fn every_recipe_has_possession_data_for_inputs() {
assert_eq!(
recipe_craft_table()
.iter()
.flat_map(|rcd| rcd.craft_data.inputs.iter())
.filter_map(|inp| if possession_data().get(inp).is_none() {
Some(inp)
} else {
None
})
.collect::<Vec<&'static PossessionType>>(),
Vec::<&'static PossessionType>::new()
);
}
#[test] #[test]
fn container_weight_should_match_calculated_weight() { fn container_weight_should_match_calculated_weight() {
for (_pt, pd) in possession_data().iter() { for (_pt, pd) in possession_data().iter() {

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 super::{PossessionData, PossessionType};
use crate::{ use crate::{
message_handler::user_commands::{UResult, UserError}, message_handler::user_commands::{UResult, UserError},
models::item::Item, models::item::{Item, ItemFlag},
static_content::possession_type::{ContainerCheck, ContainerData}, static_content::possession_type::{ContainerCheck, ContainerData},
}; };
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
@ -48,6 +48,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
checker: &RECIPES_ONLY_CHECKER, checker: &RECIPES_ONLY_CHECKER,
..Default::default() ..Default::default()
}), }),
default_flags: vec![ItemFlag::Book],
..Default::default() ..Default::default()
}), }),
(PossessionType::GrilledSteakRecipe, (PossessionType::GrilledSteakRecipe,
@ -56,6 +57,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
aliases: vec!["grilled steak"], aliases: vec!["grilled steak"],
details: "Instructions for how to make a basic but mouthwatering steak", details: "Instructions for how to make a basic but mouthwatering steak",
weight: 10, weight: 10,
default_flags: vec![ItemFlag::Instructions],
..Default::default() ..Default::default()
}), }),
)) ))

View File

@ -32,5 +32,14 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
..Default::default() ..Default::default()
} }
), ),
(
PossessionType::GrilledSteak,
PossessionData {
display: "grilled steak",
details: "A mouth-wateringly grilled steak, its outer brown surface a perfect demonstration of the Maillard reaction, with a thin bit of fat adjoining the lean protein",
weight: 250,
..Default::default()
}
),
)) ))
} }

View File

@ -321,6 +321,7 @@ pub struct Room {
// What can be rented here... // What can be rented here...
pub rentable_dynzone: Vec<RentInfo>, pub rentable_dynzone: Vec<RentInfo>,
pub material_type: MaterialType, pub material_type: MaterialType,
pub has_power: bool,
} }
impl Default for Room { impl Default for Room {
@ -341,6 +342,7 @@ impl Default for Room {
stock_list: vec![], stock_list: vec![],
rentable_dynzone: vec![], rentable_dynzone: vec![],
material_type: MaterialType::Normal, material_type: MaterialType::Normal,
has_power: false,
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File