Apply limits on carrying capacity, and make roboporter strong.

This commit is contained in:
Condorra 2023-07-11 21:23:34 +10:00
parent e83cc19698
commit ea45530a39
18 changed files with 191 additions and 110 deletions

View File

@ -4,7 +4,7 @@ use super::{
};
use crate::{
models::item::Item,
services::capacity::{check_item_capacity, CapacityLevel},
services::capacity::{check_item_capacity, check_item_ref_capacity, CapacityLevel},
static_content::possession_type::possession_data,
static_content::room,
};
@ -74,17 +74,17 @@ impl UserVerb for Verb {
)?;
}
user.credits -= stock.list_price;
let player_item_str = format!("player/{}", &player_item.item_code);
let player_item_str = player_item.refstr();
let item_code = ctx.trans.alloc_item_code().await?;
let loc = match check_item_capacity(
ctx.trans,
&player_item_str,
&player_item,
possession_type.weight,
)
.await?
{
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
match check_item_capacity(
match check_item_ref_capacity(
ctx.trans,
&player_item.location,
possession_type.weight,

View File

@ -194,7 +194,7 @@ impl QueueCommandHandler for QueueHandler {
}
}
match check_item_capacity(&ctx.trans, &ctx.item.refstr(), possession_data.weight).await? {
match check_item_capacity(&ctx.trans, &ctx.item, possession_data.weight).await? {
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("You have too much stuff to take that on!".to_owned())?
}

View File

@ -14,7 +14,7 @@ use crate::{
TaskHandler, TaskRunContext,
},
services::{
capacity::{check_item_capacity, CapacityLevel},
capacity::{check_item_ref_capacity, CapacityLevel},
comms::broadcast_to_room,
},
static_content::possession_type::possession_data,
@ -200,7 +200,8 @@ impl QueueCommandHandler for QueueHandler {
Some(pd) => pd,
};
match check_item_capacity(ctx.trans, &ctx.item.location, possession_data.weight).await? {
match check_item_ref_capacity(ctx.trans, &ctx.item.location, possession_data.weight).await?
{
CapacityLevel::AboveItemLimit => user_error(format!(
"You can't drop {}, because it is so cluttered here there is no where to put it!",
&item.display_for_sentence(ctx.explicit().await?, 1, false)

View File

@ -101,8 +101,7 @@ impl QueueCommandHandler for QueueHandler {
Some(pd) => pd,
};
let player_as_loc = format!("{}/{}", &ctx.item.item_type, &ctx.item.item_code);
match check_item_capacity(ctx.trans, &player_as_loc, possession_data.weight).await? {
match check_item_capacity(ctx.trans, &ctx.item, possession_data.weight).await? {
CapacityLevel::AboveItemLimit => {
user_error("You just can't hold that many things!".to_owned())?
}
@ -136,7 +135,7 @@ impl QueueCommandHandler for QueueHandler {
)
.await?;
let mut item_mut = (*item).clone();
item_mut.location = player_as_loc;
item_mut.location = ctx.item.refstr();
item_mut.action_type = LocationActionType::Normal;
ctx.trans.save_item_model(&item_mut).await?;
Ok(())

View File

@ -40,6 +40,7 @@ impl UserVerb for Verb {
.map(|(_, g)| g.collect::<Vec<&Arc<Item>>>())
.collect::<Vec<Vec<&Arc<Item>>>>();
let mut response = String::new();
let mut total: u64 = 0;
for items in all_groups {
let item = items[0];
if item.item_type != "possession" {
@ -51,6 +52,7 @@ impl UserVerb for Verb {
.as_ref()
.unwrap_or(&PossessionType::AntennaWhip),
) {
total += items.len() as u64 * posdat.weight;
response.push_str(&format!(
"{} [{}]{}\n",
item.display_for_sentence(
@ -67,6 +69,11 @@ impl UserVerb for Verb {
));
}
}
response.push_str(&format!(
"Total weight: {} ({} max)\n",
weight(total),
weight(player_item.max_carry())
));
if response == "" {
response.push_str("You aren't carrying anything.\n");
}

View File

@ -6,7 +6,7 @@ use crate::{
db::ItemSearchParams,
models::item::{ItemFlag, ItemSpecialData},
services::{
capacity::{check_item_capacity, CapacityLevel},
capacity::{check_item_capacity, check_item_ref_capacity, CapacityLevel},
comms::broadcast_to_room,
},
};
@ -76,14 +76,16 @@ impl UserVerb for Verb {
for item in items {
if reverse {
match check_item_capacity(&ctx.trans, &player_item.location, item.weight).await? {
match check_item_ref_capacity(&ctx.trans, &player_item.location, item.weight)
.await?
{
CapacityLevel::AboveItemLimit => user_error(
"There is not enough space here to unload another item.".to_owned(),
)?,
_ => {}
}
} else {
match check_item_capacity(&ctx.trans, &npc.refstr(), item.weight).await? {
match check_item_capacity(&ctx.trans, &npc, item.weight).await? {
CapacityLevel::AboveItemLimit | CapacityLevel::OverBurdened => {
user_error("There is not enough capacity to load that item.".to_owned())?
}

View File

@ -447,6 +447,10 @@ impl Item {
pub fn refstr(&self) -> String {
format!("{}/{}", &self.item_type, &self.item_code)
}
pub fn max_carry(&self) -> u64 {
(50.0 * 2.0_f64.powf(*self.total_stats.get(&StatType::Brawn).unwrap_or(&0.0))).ceil() as u64
}
}
impl Default for Item {

View File

@ -1,18 +1,19 @@
#[double]
use crate::db::DBTrans;
use crate::{
DResult,
models::item::Item,
models::consent::{Consent, ConsentType, ConsentStatus},
static_content::npc::npc_by_code,
message_handler::user_commands::drop::consider_expire_job_for_item,
models::consent::{Consent, ConsentStatus, ConsentType},
models::item::Item,
static_content::npc::npc_by_code,
DResult,
};
use mockall_double::double;
#[double] use crate::db::DBTrans;
pub mod comms;
pub mod combat;
pub mod skills;
pub mod capacity;
pub mod combat;
pub mod comms;
pub mod effect;
pub mod skills;
fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
if let Some((loctype, loccode)) = target.location.split_once("/") {
@ -38,14 +39,17 @@ fn check_one_consent(consent: &Consent, action: &str, target: &Item) -> bool {
return false;
}
}
true
}
pub async fn check_consent(trans: &DBTrans, action: &str,
consent_type: &ConsentType,
by: &Item,
target: &Item) -> DResult<bool> {
pub async fn check_consent(
trans: &DBTrans,
action: &str,
consent_type: &ConsentType,
by: &Item,
target: &Item,
) -> DResult<bool> {
// Consent is only a factor on actions by players towards other players or npcs.
if by.item_type != "player" || (target.item_type != "player" && target.item_type != "npc") {
return Ok(true);
@ -53,51 +57,63 @@ pub async fn check_consent(trans: &DBTrans, action: &str,
if target.item_type == "npc" {
return Ok(match npc_by_code().get(target.item_code.as_str()) {
None => false,
Some(npc) => npc.player_consents.contains(consent_type)
Some(npc) => npc.player_consents.contains(consent_type),
});
}
if target.item_code == by.item_code {
return Ok(true)
return Ok(true);
}
trans.delete_expired_user_consent().await?;
if let Some(consent) = trans.find_user_consent_by_parties_type(
&target.item_code, &by.item_code, consent_type).await? {
if let Some(consent) = trans
.find_user_consent_by_parties_type(&target.item_code, &by.item_code, consent_type)
.await?
{
if check_one_consent(&consent, action, &target) {
return Ok(true);
}
}
trans.delete_expired_corp_consent().await?;
if let Some(consent) = trans.find_corp_consent_by_user_parties_type(
&target.item_code, &by.item_code, consent_type).await? {
if let Some(consent) = trans
.find_corp_consent_by_user_parties_type(&target.item_code, &by.item_code, consent_type)
.await?
{
if check_one_consent(&consent, action, &target) {
return Ok(true);
}
}
Ok(false)
}
pub async fn destroy_container(trans: &DBTrans, container: &Item) -> DResult<()> {
trans.delete_item(&container.item_type, &container.item_code).await?;
for item in trans.find_items_by_location(
&container.refstr()
).await?.into_iter() {
trans
.delete_item(&container.item_type, &container.item_code)
.await?;
for item in trans
.find_items_by_location(&container.refstr())
.await?
.into_iter()
{
let mut item_mut = (*item).clone();
// We only update this to support consider_expire_job - it gets updated in bulk
// by transfer_all_possession below.
item_mut.location = container.location.clone();
match capacity::check_item_capacity(trans, &container.location, item_mut.weight).await? {
capacity::CapacityLevel::OverBurdened | capacity::CapacityLevel::AboveItemLimit =>
trans.delete_item(&item_mut.item_type, &item_mut.item_code).await?,
_ => consider_expire_job_for_item(trans, &item_mut).await?
match capacity::check_item_ref_capacity(trans, &container.location, item_mut.weight).await?
{
capacity::CapacityLevel::OverBurdened | capacity::CapacityLevel::AboveItemLimit => {
trans
.delete_item(&item_mut.item_type, &item_mut.item_code)
.await?
}
_ => consider_expire_job_for_item(trans, &item_mut).await?,
}
}
trans.transfer_all_possessions_code(
&container.refstr(),
&container.location).await?;
trans
.transfer_all_possessions_code(&container.refstr(), &container.location)
.await?;
Ok(())
}

View File

@ -1,7 +1,7 @@
use crate::DResult;
#[double]
use crate::db::DBTrans;
use crate::{models::item::Item, DResult};
use mockall_double::double;
#[double] use crate::db::DBTrans;
#[derive(Debug, PartialEq)]
pub enum CapacityLevel {
@ -11,75 +11,118 @@ pub enum CapacityLevel {
OverBurdened,
AboveItemLimit,
}
pub async fn check_item_capacity(trans: &DBTrans,
container: &str,
proposed_weight: u64) -> DResult<CapacityLevel> {
let stats = trans.get_location_stats(
container
).await?;
pub async fn check_item_ref_capacity(
trans: &DBTrans,
container: &str,
proposed_weight: u64,
) -> DResult<CapacityLevel> {
if let Some((item_type, item_code)) = container.split_once("/") {
if item_type != "player" && item_type != "npc" {
// Fast path...
let stats = trans.get_location_stats(&container).await?;
if stats.total_count >= 50 || proposed_weight > 0 && stats.total_count >= 49 {
Ok(CapacityLevel::AboveItemLimit)
} else {
Ok(CapacityLevel::Unburdened)
}
} else {
if let Some(item) = trans.find_item_by_type_code(item_type, item_code).await? {
check_item_capacity(trans, &item, proposed_weight).await
} else {
Err("Invalid item.")?
}
}
} else {
Err("Invalid item format.")?
}
}
pub async fn check_item_capacity(
trans: &DBTrans,
container: &Item,
proposed_weight: u64,
) -> DResult<CapacityLevel> {
let stats = trans.get_location_stats(&container.refstr()).await?;
if stats.total_count >= 50 || proposed_weight > 0 && stats.total_count >= 49 {
return Ok(CapacityLevel::AboveItemLimit);
}
if let Some((item_type, _item_code)) = container.split_once("/") {
if item_type == "player" || item_type == "npc" {
let max_weight = 20000; // TODO Calculate properly
let new_weight = stats.total_weight + proposed_weight;
if new_weight >= max_weight {
return Ok(CapacityLevel::OverBurdened);
} else if new_weight >= max_weight * 4 / 5 {
return Ok(CapacityLevel::HeavilyBurdened);
} else if new_weight >= max_weight / 2 {
return Ok(CapacityLevel::SlightlyBurdened);
}
}
if container.item_type == "player" || container.item_type == "npc" {
let max_weight = container.max_carry();
let new_weight = stats.total_weight + proposed_weight;
if new_weight >= max_weight {
return Ok(CapacityLevel::OverBurdened);
} else if new_weight >= max_weight * 4 / 5 {
return Ok(CapacityLevel::HeavilyBurdened);
} else if new_weight >= max_weight / 2 {
return Ok(CapacityLevel::SlightlyBurdened);
}
}
Ok(CapacityLevel::Unburdened)
}
#[cfg(test)]
mod test {
use crate::db::{
MockDBTrans,
LocationStats
};
use super::*;
use crate::db::{LocationStats, MockDBTrans};
#[tokio::test]
async fn check_item_capacity_should_say_above_item_limit_if_over() {
let mut mock_db = MockDBTrans::new();
mock_db.expect_get_location_stats()
mock_db
.expect_get_location_stats()
.withf(|s| s == "player/foo")
.returning(|_| Ok(LocationStats {
total_count: 49,
total_weight: 100,
}));
assert_eq!(check_item_capacity(&mock_db, "player/foo", 10).await.unwrap(),
CapacityLevel::AboveItemLimit);
.returning(|_| {
Ok(LocationStats {
total_count: 49,
total_weight: 100,
})
});
assert_eq!(
check_item_ref_capacity(&mock_db, "player/foo", 10)
.await
.unwrap(),
CapacityLevel::AboveItemLimit
);
}
#[tokio::test]
async fn check_item_capacity_should_say_overburdened_if_over() {
let mut mock_db = MockDBTrans::new();
mock_db.expect_get_location_stats()
mock_db
.expect_get_location_stats()
.withf(|s| s == "player/foo")
.returning(|_| Ok(LocationStats {
total_count: 2,
total_weight: 100,
}));
assert_eq!(check_item_capacity(&mock_db, "player/foo", 1000000).await.unwrap(),
CapacityLevel::OverBurdened);
.returning(|_| {
Ok(LocationStats {
total_count: 2,
total_weight: 100,
})
});
assert_eq!(
check_item_ref_capacity(&mock_db, "player/foo", 1000000)
.await
.unwrap(),
CapacityLevel::OverBurdened
);
}
#[tokio::test]
async fn check_item_capacity_should_say_unburdened_when_low_weight() {
let mut mock_db = MockDBTrans::new();
mock_db.expect_get_location_stats()
mock_db
.expect_get_location_stats()
.withf(|s| s == "player/foo")
.returning(|_| Ok(LocationStats {
total_count: 2,
total_weight: 100,
}));
assert_eq!(check_item_capacity(&mock_db, "player/foo", 50).await.unwrap(),
CapacityLevel::Unburdened);
.returning(|_| {
Ok(LocationStats {
total_count: 2,
total_weight: 100,
})
});
assert_eq!(
check_item_ref_capacity(&mock_db, "player/foo", 50)
.await
.unwrap(),
CapacityLevel::Unburdened
);
}
}

View File

@ -13,7 +13,7 @@ use crate::{
},
models::{
consent::ConsentType,
item::{Item, ItemFlag, Pronouns, SkillType},
item::{Item, ItemFlag, Pronouns, SkillType, StatType},
task::{Task, TaskDetails, TaskMeta, TaskRecurrence},
},
regular_tasks::{
@ -99,6 +99,7 @@ pub struct NPC {
pub intrinsic_weapon: Option<PossessionType>,
pub total_xp: u64,
pub total_skills: BTreeMap<SkillType, f64>,
pub total_stats: BTreeMap<StatType, f64>,
pub species: SpeciesType,
pub wander_zones: Vec<&'static str>,
pub kill_bonus: Option<KillBonus>,
@ -128,6 +129,7 @@ impl Default for NPC {
)
})
.collect(),
total_stats: vec![].into_iter().collect(),
aggression: 0,
max_health: 24,
intrinsic_weapon: None,
@ -202,6 +204,7 @@ pub fn npc_static_items() -> Box<dyn Iterator<Item = StaticItem>> {
pronouns: c.pronouns.clone(),
total_xp: c.total_xp.clone(),
total_skills: c.total_skills.clone(),
total_stats: c.total_stats.clone(),
species: c.species.clone(),
health: c.max_health.clone(),
flags,

View File

@ -6,7 +6,7 @@ use crate::{
user_commands::{rent::recursively_destroy_or_move_item, UResult},
ListenerSession,
},
models::item::{FollowData, FollowState, Item, ItemFlag, Pronouns},
models::item::{FollowData, FollowState, Item, ItemFlag, Pronouns, StatType},
services::comms::broadcast_to_room,
static_content::{possession_type::PossessionType, species::SpeciesType},
DResult,
@ -120,6 +120,7 @@ macro_rules! roboporter {
price: 100,
}),
extra_flags: vec![ItemFlag::CanLoad],
total_stats: vec![(StatType::Brawn, 20.0)].into_iter().collect(),
..Default::default()
}
}

View File

@ -254,7 +254,7 @@ pub struct PossessionData {
pub charge_data: Option<ChargeData>,
pub use_data: Option<UseData>,
pub becomes_on_spent: Option<PossessionType>,
pub weight: u64,
pub weight: u64, // should be realistic in grams.
pub install_handler: Option<&'static (dyn InstallHandler + Sync + Send)>,
pub lockcheck_handler: Option<&'static (dyn LockcheckHandler + Sync + Send)>,
pub sign_handler: Option<&'static (dyn ArglessHandler + Sync + Send)>,
@ -273,7 +273,7 @@ impl Default for PossessionData {
details_less_explicit: None,
aliases: vec![],
max_health: 10,
weight: 100,
weight: 250,
charge_data: None,
becomes_on_spent: None,
use_data: None,

View File

@ -11,6 +11,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "rusty metal pot",
details: "A metal pot that has rusted and is a bit dinged up - it looks like someone that way inclined could wear it as a serviceable helmet.",
aliases: vec!("pot", "rusted", "rusty"),
weight: 600,
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Head),
thickness: 4.0,
@ -48,6 +49,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "hockey mask",
details: "A white face-hugging fibreglass hockey mask, with small air holes across the face, but no specific hole for the mouth. It looks like it would give a degree of protection to the face, but it might also make someone look like a serial killer!",
aliases: vec!("mask"),
weight: 300,
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Face),
thickness: 4.0,

View File

@ -58,7 +58,7 @@ impl InstallHandler for ScanLockInstall {
user_error("There's already a lock on that door - uninstall it first.".to_owned())?;
}
match check_item_capacity(&ctx.trans, &room.refstr(), LOCK_WEIGHT).await? {
match check_item_capacity(&ctx.trans, &room, LOCK_WEIGHT).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => user_error(
"That room has so much stuff, you can't install anything new.".to_owned(),
)?,
@ -105,16 +105,15 @@ impl InstallHandler for ScanLockInstall {
let mut what_mut = (*what).clone();
let extra_text =
match check_item_capacity(&ctx.trans, &player.refstr(), LOCK_WEIGHT).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
", dropping it on the floor since he can't hold it."
}
_ => {
what_mut.location = player.refstr();
""
}
};
let extra_text = match check_item_capacity(&ctx.trans, &player, LOCK_WEIGHT).await? {
CapacityLevel::OverBurdened | CapacityLevel::AboveItemLimit => {
", dropping it on the floor since he can't hold it."
}
_ => {
what_mut.location = player.refstr();
""
}
};
what_mut.action_type = LocationActionType::Normal;
what_mut.owner = None;
ctx.trans.save_item_model(&what_mut).await?;

View File

@ -11,6 +11,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "pair of leather pants",
details: "Black leather pants that looks like they would protect you from falling off a motorbike, or maybe even offer some protection against certain weapons",
aliases: vec!("leather pants", "pants"),
weight: 500,
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Groin, BodyPart::Legs),
thickness: 4.0,

View File

@ -18,7 +18,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
PossessionData {
display: "steak",
details: "A hunk of raw red meat, dripping with blood",
weight: 100,
weight: 250,
..Default::default()
}
),
@ -28,7 +28,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "severed head",
aliases: vec!("head"),
details: "A head that has been chopped clean from the body",
weight: 250,
weight: 1500,
..Default::default()
}
),

View File

@ -11,6 +11,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "leather jacket",
details: "A black leather jacket that looks like it would protect you from falling off a motorbike, or maybe even offer some protection against certain weapons",
aliases: vec!("jacket"),
weight: 600,
wear_data: Some(WearData {
covers_parts: vec!(BodyPart::Arms,
BodyPart::Chest,

View File

@ -11,6 +11,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
details: "A crudely fashioned whip made from a broken metal antenna. It looks a bit flimsy, but it \
might do you until you get a better weapon!",
aliases: vec!("whip"),
weight: 1000,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Whips,
raw_min_to_learn: 0.0,
@ -48,6 +49,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
display: "leather whip",
details: "A whip made from stitched together animal skins... it looks like a formidable weapon, and in the right hands will make someone look like Indiana Jones!",
aliases: vec!("whip"),
weight: 800,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Whips,
raw_min_to_learn: 0.0,