Add a fill command to fill bottles etc...

This commit is contained in:
Condorra 2023-08-12 01:36:46 +10:00
parent fab18d604e
commit 3023b5317a
9 changed files with 455 additions and 12 deletions

View File

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

View File

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

View File

@ -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<time::Duration> {
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::<u64>();
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::<u64>())
.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<LiquidType, u64> = 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 <bold>fill<reset> container <bold>from<reset> 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;

View File

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

View File

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

View File

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

View File

@ -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<PossessionType, &'static Possessio
.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(bottles::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(trauma_kit::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(
corp_licence::data()

View File

@ -0,0 +1,26 @@
use super::{PossessionData, PossessionType};
use crate::static_content::possession_type::LiquidContainerData;
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::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()
},
)]
})
}

View File

@ -2555,6 +2555,11 @@ pub fn room_list() -> Vec<Room> {
list_price: 250,
..Default::default()
},
RoomStock {
possession_type: PossessionType::DrinkBottle,
list_price: 80,
..Default::default()
},
),
should_caption: true,
..Default::default()