From 3023b5317a7d7b852475e23d66fe5a6122e5eff6 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 12 Aug 2023 01:36:46 +1000 Subject: [PATCH] Add a fill command to fill bottles etc... --- .../src/message_handler/user_commands.rs | 3 +- .../message_handler/user_commands/drink.rs | 21 +- .../src/message_handler/user_commands/fill.rs | 341 ++++++++++++++++++ blastmud_game/src/models/item.rs | 5 + .../src/regular_tasks/queued_command.rs | 13 +- blastmud_game/src/services/capacity.rs | 49 ++- .../src/static_content/possession_type.rs | 4 + .../static_content/possession_type/bottles.rs | 26 ++ .../src/static_content/room/melbs.rs | 5 + 9 files changed, 455 insertions(+), 12 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/fill.rs create mode 100644 blastmud_game/src/static_content/possession_type/bottles.rs diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index faa3399a..9394216e 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -29,6 +29,7 @@ mod describe; pub mod drink; pub mod drop; pub mod eat; +pub mod fill; mod fire; pub mod follow; mod gear; @@ -159,7 +160,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "drink" => drink::VERB, "drop" => drop::VERB, "eat" => eat::VERB, - + "fill" => fill::VERB, "fire" => fire::VERB, "follow" => follow::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/drink.rs b/blastmud_game/src/message_handler/user_commands/drink.rs index 5bbc5a34..be1182fe 100644 --- a/blastmud_game/src/message_handler/user_commands/drink.rs +++ b/blastmud_game/src/message_handler/user_commands/drink.rs @@ -4,9 +4,10 @@ use super::{ }; use crate::{ regular_tasks::queued_command::{ - queue_command, QueueCommand, QueueCommandHandler, QueuedCommandContext, + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, }, services::{ + capacity::recalculate_container_weight_mut, comms::broadcast_to_room, urges::{hunger_changed, thirst_changed}, }, @@ -177,6 +178,18 @@ impl QueueCommandHandler for QueueHandler { .and_modify(|v| *v -= how_many_drunk as u64); } } + match item_mut.liquid_details.as_mut() { + None => {} + Some(ld) => { + ld.contents = ld + .contents + .clone() + .into_iter() + .filter(|c| c.1 != 0) + .collect() + } + } + recalculate_container_weight_mut(&ctx.trans, &mut item_mut).await?; ctx.trans.save_item_model(&item_mut).await?; Ok(()) } @@ -223,7 +236,6 @@ impl UserVerb for Verb { )?; } - let mut player_item_mut = (*player_item).clone(); for target in targets { if target.item_type != "possession" && target.item_type != "fixed_item" { user_error("You can't drink that!".to_owned())?; @@ -231,9 +243,9 @@ impl UserVerb for Verb { if target.liquid_details.is_none() { user_error("There's nothing to drink!".to_owned())?; } - queue_command( + queue_command_and_save( ctx, - &mut player_item_mut, + &player_item, &QueueCommand::Drink { item_type: target.item_type.clone(), item_code: target.item_code.clone(), @@ -241,7 +253,6 @@ impl UserVerb for Verb { ) .await?; } - ctx.trans.save_item_model(&player_item_mut).await?; Ok(()) } } diff --git a/blastmud_game/src/message_handler/user_commands/fill.rs b/blastmud_game/src/message_handler/user_commands/fill.rs new file mode 100644 index 00000000..6ae6ed32 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/fill.rs @@ -0,0 +1,341 @@ +use super::{ + get_player_item_or_fail, search_item_for_user, user_error, ItemSearchParams, UResult, UserVerb, + UserVerbRef, VerbContext, +}; +use crate::{ + models::item::{Item, LiquidDetails, LiquidType}, + regular_tasks::queued_command::{ + queue_command_and_save, QueueCommand, QueueCommandHandler, QueuedCommandContext, + }, + services::{capacity::recalculate_container_weight_mut, comms::broadcast_to_room}, +}; +use ansi::ansi; +use async_trait::async_trait; +use std::collections::{btree_map::Entry, BTreeMap}; +use std::time; + +pub struct QueueHandler; +#[async_trait] +impl QueueCommandHandler for QueueHandler { + async fn start_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult { + if ctx.item.death_data.is_some() { + user_error( + "You try to fill it, but your ghostly hands slip through it uselessly".to_owned(), + )?; + } + let (from_item_type, from_item_code, to_item_type, to_item_code) = match ctx.command { + QueueCommand::Fill { + from_item_type, + from_item_code, + to_item_type, + to_item_code, + } => (from_item_type, from_item_code, to_item_type, to_item_code), + _ => user_error("Unexpected command".to_owned())?, + }; + let from_item = match ctx + .trans + .find_item_by_type_code(&from_item_type, &from_item_code) + .await? + { + None => user_error("Item not found".to_owned())?, + Some(it) => it, + }; + let to_item = match ctx + .trans + .find_item_by_type_code(&to_item_type, &to_item_code) + .await? + { + None => user_error("Item not found".to_owned())?, + Some(it) => it, + }; + if to_item.location != ctx.item.location && to_item.location != ctx.item.refstr() { + user_error(format!( + "You try to fill {} but realise you no longer have it", + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))? + } + if from_item.location != ctx.item.location && from_item.location != ctx.item.refstr() { + user_error(format!( + "You try to fill from {} but realise you no longer have it", + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))? + } + + let msg_exp = format!( + "{} prepares to fill {} from {}\n", + &ctx.item.display_for_sentence(true, 1, true), + &to_item.display_for_sentence(true, 1, false), + &from_item.display_for_sentence(true, 1, false), + ); + let msg_nonexp = format!( + "{} prepares to fill {} from {}\n", + &ctx.item.display_for_sentence(false, 1, true), + &to_item.display_for_sentence(false, 1, false), + &from_item.display_for_sentence(false, 1, false) + ); + broadcast_to_room( + ctx.trans, + &ctx.item.location, + None, + &msg_exp, + Some(&msg_nonexp), + ) + .await?; + Ok(time::Duration::from_secs(1)) + } + + #[allow(unreachable_patterns)] + async fn finish_command(&self, ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { + if ctx.item.death_data.is_some() { + user_error( + "You try to fill it, but your ghostly hands slip through it uselessly".to_owned(), + )?; + } + let (from_item_type, from_item_code, to_item_type, to_item_code) = match ctx.command { + QueueCommand::Fill { + from_item_type, + from_item_code, + to_item_type, + to_item_code, + } => (from_item_type, from_item_code, to_item_type, to_item_code), + _ => user_error("Unexpected command".to_owned())?, + }; + let from_item = match ctx + .trans + .find_item_by_type_code(&from_item_type, &from_item_code) + .await? + { + None => user_error("Item not found".to_owned())?, + Some(it) => it, + }; + let to_item = match ctx + .trans + .find_item_by_type_code(&to_item_type, &to_item_code) + .await? + { + None => user_error("Item not found".to_owned())?, + Some(it) => it, + }; + if to_item.location != ctx.item.location && to_item.location != ctx.item.refstr() { + user_error(format!( + "You try to fill {} but realise you no longer have it", + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))? + } + if from_item.location != ctx.item.location && from_item.location != ctx.item.refstr() { + user_error(format!( + "You try to fill from {} but realise you no longer have it", + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))? + } + + let from_liquid_details = match from_item.liquid_details.as_ref() { + None => user_error(format!( + "{} appears to be empty.", + from_item.display_for_sentence(ctx.explicit().await?, 1, true) + ))?, + Some(v) => v, + }; + + let available_vol = from_liquid_details + .contents + .iter() + .map(|r| r.1.clone()) + .sum::(); + if available_vol == 0 { + user_error(format!( + "{} appears to be empty.", + from_item.display_for_sentence(ctx.explicit().await?, 1, true) + ))? + } + + let into_liqdata = match to_item + .static_data() + .and_then(|pd| pd.liquid_container_data.as_ref()) + { + None => user_error(format!( + "You can't find a way to fill {}.", + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))?, + Some(v) => v, + }; + + if let Some(allowed) = &into_liqdata.allowed_contents { + for (liq_type, _) in &from_liquid_details.contents { + if !allowed.contains(&liq_type) { + user_error(format!( + "You don't think putting {} into {} is a good idea.", + liq_type.display(), + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))?; + } + } + } + + let capacity_remaining = into_liqdata.capacity + - to_item + .liquid_details + .as_ref() + .map(|ld| ld.contents.iter().map(|c| c.1.clone()).sum::()) + .unwrap_or(0); + let actually_transferred = available_vol.min(capacity_remaining); + + if actually_transferred == 0 { + user_error(format!( + "You don't think you can get any more into {}.", + to_item.display_for_sentence(ctx.explicit().await?, 1, false) + ))?; + } + + let transfer_frac = (actually_transferred as f64) / (available_vol as f64); + let mut remaining_total = actually_transferred; + let transfer_volumes: BTreeMap = from_liquid_details + .contents + .iter() + .flat_map(|(liqtype, vol)| { + let move_vol = (((*vol as f64) * transfer_frac).ceil() as u64).min(remaining_total); + remaining_total -= move_vol; + if move_vol > 0 { + Some((liqtype.clone(), move_vol)) + } else { + None + } + }) + .collect(); + + let mut to_item_mut: Item = (*to_item).clone(); + match to_item_mut.liquid_details.as_mut() { + None => { + to_item_mut.liquid_details = Some(LiquidDetails { + contents: transfer_volumes.clone(), + }) + } + Some(ld) => { + for (liq, vol) in &transfer_volumes { + ld.contents + .entry(liq.clone()) + .and_modify(|v| { + *v += *vol; + }) + .or_insert(vol.clone()); + } + } + } + let mut from_item_mut: Item = (*from_item).clone(); + if let Some(ld) = from_item_mut.liquid_details.as_mut() { + for (liq, vol) in &transfer_volumes { + match ld.contents.entry(liq.clone()) { + Entry::Vacant(_) => {} + Entry::Occupied(mut ent) => { + if ent.get() <= vol { + ent.remove(); + } else { + *(ent.get_mut()) -= *vol; + } + } + } + } + } + recalculate_container_weight_mut(&ctx.trans, &mut from_item_mut).await?; + recalculate_container_weight_mut(&ctx.trans, &mut to_item_mut).await?; + ctx.trans.save_item_model(&from_item_mut).await?; + ctx.trans.save_item_model(&to_item_mut).await?; + + let msg_exp = format!( + "{} fills {} from {}\n", + &ctx.item.display_for_sentence(true, 1, true), + &to_item.display_for_sentence(true, 1, false), + &from_item.display_for_sentence(true, 1, false), + ); + let msg_nonexp = format!( + "{} fills {} from {}\n", + &ctx.item.display_for_sentence(false, 1, true), + &to_item.display_for_sentence(false, 1, false), + &from_item.display_for_sentence(false, 1, false) + ); + broadcast_to_room( + ctx.trans, + &ctx.item.location, + None, + &msg_exp, + Some(&msg_nonexp), + ) + .await?; + + Ok(()) + } +} + +pub struct Verb; +#[async_trait] +impl UserVerb for Verb { + async fn handle( + self: &Self, + ctx: &mut VerbContext, + _verb: &str, + remaining: &str, + ) -> UResult<()> { + let player_item = get_player_item_or_fail(ctx).await?; + + let (to_str, from_str) = match remaining.split_once(" from ") { + None => user_error( + ansi!("Try fill container from container.").to_owned(), + )?, + Some((to_str, from_str)) => (to_str.trim(), from_str.trim()), + }; + + let to_target = search_item_for_user( + ctx, + &ItemSearchParams { + include_contents: true, + include_loc_contents: true, + ..ItemSearchParams::base(&player_item, to_str) + }, + ) + .await?; + let from_target = search_item_for_user( + ctx, + &ItemSearchParams { + include_contents: true, + include_loc_contents: true, + ..ItemSearchParams::base(&player_item, from_str) + }, + ) + .await?; + + if player_item.death_data.is_some() { + user_error( + "You try to fill it, but your ghostly hands slip through it uselessly".to_owned(), + )?; + } + + if from_target.item_type != "possession" && from_target.item_type != "fixed_item" { + user_error("You can't fill from that!".to_owned())?; + } + if to_target.item_type != "possession" && to_target.item_type != "fixed_item" { + user_error("You can't fill that!".to_owned())?; + } + if to_target.item_type == from_target.item_type + && to_target.item_code == from_target.item_code + { + user_error( + "You can't figure out how to fill something from itself - a shame!".to_owned(), + )?; + } + + queue_command_and_save( + ctx, + &player_item, + &QueueCommand::Fill { + from_item_type: from_target.item_type.clone(), + from_item_code: from_target.item_code.clone(), + to_item_type: to_target.item_type.clone(), + to_item_code: to_target.item_code.clone(), + }, + ) + .await?; + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index d98292b9..0af52e26 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -172,6 +172,11 @@ impl LiquidType { LiquidType::Water => "water", } } + pub fn density(&self) -> f64 { + match self { + LiquidType::Water => 1.0, // g / mL. + } + } } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index 47f49fb9..205b6c87 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -2,7 +2,7 @@ use super::{TaskHandler, TaskRunContext}; #[double] use crate::db::DBTrans; use crate::message_handler::user_commands::{ - close, cut, drink, drop, eat, get, improvise, make, movement, open, put, remove, use_cmd, + close, cut, drink, drop, eat, fill, get, improvise, make, movement, open, put, remove, use_cmd, user_error, wear, wield, CommandHandlingError, UResult, VerbContext, }; use crate::message_handler::ListenerSession; @@ -57,6 +57,12 @@ pub enum QueueCommand { from_corpse: String, what_part: String, }, + Fill { + from_item_type: String, + from_item_code: String, + to_item_type: String, + to_item_code: String, + }, Drink { item_type: String, item_code: String, @@ -121,6 +127,7 @@ impl QueueCommand { Drink { .. } => "Drink", Drop { .. } => "Drop", Eat { .. } => "Eat", + Fill { .. } => "Fill", Get { .. } => "Get", GetFromContainer { .. } => "GetFromContainer", Make { .. } => "Make", @@ -204,6 +211,10 @@ fn queue_command_registry( "Eat", &eat::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), ), + ( + "Fill", + &fill::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), + ), ( "Make", &make::QueueHandler as &(dyn QueueCommandHandler + Sync + Send), diff --git a/blastmud_game/src/services/capacity.rs b/blastmud_game/src/services/capacity.rs index 47244541..a2959c90 100644 --- a/blastmud_game/src/services/capacity.rs +++ b/blastmud_game/src/services/capacity.rs @@ -65,7 +65,19 @@ pub async fn check_item_capacity( Ok(CapacityLevel::Unburdened) } -pub async fn recalculate_container_weight(trans: &DBTrans, container: &Item) -> DResult<()> { +pub async fn recalculate_container_weight_mut( + trans: &DBTrans, + container: &mut Item, +) -> DResult { + let liq_weight = match container.liquid_details.as_ref() { + None => 0, + Some(ld) => ld + .contents + .iter() + .map(|(liq, vol)| (liq.density() * (*vol as f64)).ceil() as u64) + .sum(), + }; + if let Some(container_data) = container.possession_type.as_ref().and_then(|pt| { possession_data() .get(pt) @@ -73,12 +85,39 @@ pub async fn recalculate_container_weight(trans: &DBTrans, container: &Item) -> }) { let stats = trans.get_location_stats(&container.refstr()).await?; let new_weight = container_data.base_weight - + (((stats.total_weight as f64) * container_data.compression_ratio).ceil() as u64); + + (((stats.total_weight as f64) * container_data.compression_ratio).ceil() as u64) + + liq_weight; if new_weight != container.weight { - let mut container_mut = container.clone(); - container_mut.weight = new_weight; - trans.save_item_model(&container_mut).await?; + container.weight = new_weight; + Ok(true) + } else { + Ok(false) } + } else if liq_weight > 0 { + if let Some(pd) = container + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + { + let new_weight = liq_weight + pd.weight; + if new_weight != container.weight { + container.weight = new_weight; + Ok(true) + } else { + Ok(false) + } + } else { + Ok(false) + } + } else { + Ok(false) + } +} + +pub async fn recalculate_container_weight(trans: &DBTrans, container: &Item) -> DResult<()> { + let mut container_mut = (*container).clone(); + if recalculate_container_weight_mut(trans, &mut container_mut).await? { + trans.save_item_model(&container_mut).await?; } Ok(()) } diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 73a5c060..cb950fe3 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -18,6 +18,7 @@ mod bags; mod benches; mod blade; mod books; +mod bottles; mod corp_licence; mod fangs; pub mod head_armour; @@ -405,6 +406,8 @@ pub enum PossessionType { CertificateOfIncorporation, // Storage DuffelBag, + // Fluid containers + DrinkBottle, // Security Scanlock, // Food @@ -492,6 +495,7 @@ pub fn possession_data() -> &'static BTreeMap &'static Vec<(PossessionType, PossessionData)> { + static D: OnceCell> = OnceCell::new(); + &D.get_or_init(|| { + vec![( + PossessionType::DrinkBottle, + PossessionData { + display: "drink bottle", + aliases: vec!["bottle", "flask", "canteen"], + details: "A stainless steel bottle, dinged up a bit with dents \ + and scruff marks from use, but still looking perfectly \ + usable for whatever fluid you might want it to hold. It \ + seems to be the right size to hold about 1L.", + weight: 100, + liquid_container_data: Some(LiquidContainerData { + capacity: 1000, + ..Default::default() + }), + ..Default::default() + }, + )] + }) +} diff --git a/blastmud_game/src/static_content/room/melbs.rs b/blastmud_game/src/static_content/room/melbs.rs index f8eea5cf..d8eb615f 100644 --- a/blastmud_game/src/static_content/room/melbs.rs +++ b/blastmud_game/src/static_content/room/melbs.rs @@ -2555,6 +2555,11 @@ pub fn room_list() -> Vec { list_price: 250, ..Default::default() }, + RoomStock { + possession_type: PossessionType::DrinkBottle, + list_price: 80, + ..Default::default() + }, ), should_caption: true, ..Default::default()