diff --git a/blastmud_game/src/message_handler/user_commands/look.rs b/blastmud_game/src/message_handler/user_commands/look.rs index bd293ccf..8a79bb13 100644 --- a/blastmud_game/src/message_handler/user_commands/look.rs +++ b/blastmud_game/src/message_handler/user_commands/look.rs @@ -255,20 +255,28 @@ pub async fn describe_normal_item( } if item.item_type == "possession" { - if let Some(charge_data) = item + if let Some(poss_data) = item .possession_type .as_ref() .and_then(|pt| possession_data().get(&pt)) - .and_then(|pd| pd.charge_data.as_ref()) { - let unit = if item.charges == 1 { - charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_suffix - } else { - language::pluralise(charge_data.charge_name_prefix) - + " " - + charge_data.charge_name_suffix - }; - contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit)); + if let Some(describer) = poss_data.computed_extra_details { + contents_desc.push_str( + &describer + .describe_for(&ctx.trans, item, player_item) + .await?, + ); + } + if let Some(charge_data) = poss_data.charge_data.as_ref() { + let unit = if item.charges == 1 { + charge_data.charge_name_prefix.to_owned() + " " + charge_data.charge_name_suffix + } else { + language::pluralise(charge_data.charge_name_prefix) + + " " + + charge_data.charge_name_suffix + }; + contents_desc.push_str(&format!("It has {} {} left.\n", item.charges, unit)); + } } if let Some(recipe_craft_data) = item diff --git a/blastmud_game/src/message_handler/user_commands/remove.rs b/blastmud_game/src/message_handler/user_commands/remove.rs index 27192652..306e2365 100644 --- a/blastmud_game/src/message_handler/user_commands/remove.rs +++ b/blastmud_game/src/message_handler/user_commands/remove.rs @@ -61,7 +61,7 @@ async fn check_removeable(ctx: &mut QueuedCommandContext<'_>, item: &Item) -> UR covers_parts.contains(&part) && other_item .action_type_started - .map(|other_worn_since| other_worn_since < my_worn_since) + .map(|other_worn_since| other_worn_since > my_worn_since) .unwrap_or(false) } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index b783e65e..27b6455a 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -12,7 +12,7 @@ use crate::{ services::{charging, combat, effect, environment, idlepark, sharing, spawn, tempbuff, urges}, static_content::{ npc::{self, computer_museum_npcs}, - possession_type::lights, + possession_type::utilities, room::general_hospital, }, DResult, @@ -70,7 +70,7 @@ fn task_handler_registry( ("IdlePark", idlepark::IDLEPARK_HANDLER), ("HospitalERSeePatient", general_hospital::SEE_PATIENT_TASK), ("ExpireBuff", tempbuff::EXPIRE_BUFF_TASK), - ("DischargeLight", lights::DISCHARGE_TASK), + ("DischargeLight", utilities::DISCHARGE_TASK), ("ChargeItem", charging::TASK_HANDLER), ( "ApplyEnvironmentalEffects", diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 8bb210a6..0741709c 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -79,8 +79,9 @@ pub async fn soak_damage( break; } let soak_amount: f64 = ((soak.max_soak - soak.min_soak) - * rand::thread_rng().gen::()) - .min(damage_amount); + * rand::thread_rng().gen::() + + soak.min_soak) + .min(damage_amount); damage_amount -= soak_amount; let clothes_damage = ((0..(soak_amount as i64)) .filter(|_| rand::thread_rng().gen::() < soak.damage_probability_per_soak) diff --git a/blastmud_game/src/services/environment.rs b/blastmud_game/src/services/environment.rs index b6db8596..aca6d8b4 100644 --- a/blastmud_game/src/services/environment.rs +++ b/blastmud_game/src/services/environment.rs @@ -3,6 +3,7 @@ use std::time; use async_trait::async_trait; use chrono::Utc; use log::warn; +use rand::Rng; use uuid::Uuid; use crate::{ @@ -10,14 +11,17 @@ use crate::{ movement::attempt_move_immediate, CommandHandlingError, UResult, }, models::{ - item::{Item, SkillType}, + item::{Item, LocationActionType, SkillType}, task::{Task, TaskDetails, TaskMeta}, }, regular_tasks::{ queued_command::{MovementSource, QueueCommand, QueuedCommandContext}, TaskHandler, TaskRunContext, }, - static_content::room::{room_map_by_code, Direction, MaterialType, Room}, + static_content::{ + possession_type::{possession_data, DamageType}, + room::{room_map_by_code, Direction, MaterialType, Room}, + }, DResult, }; @@ -257,7 +261,28 @@ pub async fn rad_environment_effects( return Ok(false); } - player_item.raddamage += room.environment.radiation; + let clothes = ctx + .trans + .find_by_action_and_location(&player_item.refstr(), &LocationActionType::Worn) + .await?; + let shielding: u64 = clothes + .iter() + .filter_map(|c| { + c.possession_type + .as_ref() + .and_then(|pt| possession_data().get(&pt)) + .and_then(|pd| pd.wear_data.as_ref()) + .and_then(|wd| wd.soaks.get(&DamageType::Radiation)) + .map(|sd| { + (sd.max_soak - sd.min_soak) * rand::thread_rng().gen::() + sd.min_soak + }) + }) + .sum::() as u64; + if shielding >= room.environment.radiation { + return Ok(false); + } + + player_item.raddamage += room.environment.radiation - shielding; let existing_task = ctx .trans .fetch_specific_task("ApplyEnvironmentalConsequences", &player_item.refstr()) diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index ccfeed6c..166d74e9 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -29,13 +29,13 @@ mod food; pub mod head_armour; mod junk; mod keys; -pub mod lights; pub mod lock; pub mod lower_armour; mod meat; mod testpapers; pub mod torso_armour; mod trauma_kit; +pub mod utilities; mod whip; pub type AttackMessageChoice = Vec String + 'static + Sync + Send>>; @@ -57,6 +57,7 @@ pub enum DamageType { Pierce, Shock, Bullet, + Radiation, } impl DamageType { @@ -69,6 +70,7 @@ impl DamageType { Pierce => "pierce", Shock => "shock", Bullet => "bullet", + Radiation => "radiation", } } } @@ -323,6 +325,11 @@ pub struct SitData { pub stress_impact: i16, } +#[async_trait] +pub trait Describer { + async fn describe_for(&self, trans: &DBTrans, item: &Item, player: &Item) -> UResult; +} + pub struct PossessionData { pub weapon_data: Option, pub display: &'static str, @@ -347,6 +354,7 @@ pub struct PossessionData { pub eat_data: Option, pub sit_data: Option, pub static_special_data: Option, + pub computed_extra_details: Option<&'static (dyn Describer + Sync + Send)>, } impl Default for PossessionData { @@ -375,6 +383,7 @@ impl Default for PossessionData { eat_data: None, sit_data: None, static_special_data: None, + computed_extra_details: None, } } } @@ -402,13 +411,14 @@ pub enum PossessionType { // Special values that substitute for possessions. Fangs, // Default weapon for certain animals // Real possessions from here on: - // Armour + // Armour / Clothes RustyMetalPot, HockeyMask, Shirt, LeatherJacket, Jeans, LeatherPants, + RadSuit, // Weapons: Whips AntennaWhip, LeatherWhip, @@ -454,8 +464,9 @@ pub enum PossessionType { // Recipes CulinaryEssentials, GrilledSteakRecipe, - // Lights + // Utilities ElectricLantern, + GeigerCounter, } impl Into for PossessionType { @@ -579,7 +590,7 @@ pub fn possession_data() -> &'static BTreeMap &'static Vec<(PossessionType, PossessionData)> { ..Default::default() } ), + ( + PossessionType::RadSuit, + PossessionData { + display: "radiation suit", + details: "A suit with a yellow outer layer and a foil lining that covers every part of the body from head to toe, featuring a tight fitting respirator, a hood that covers the head, gloves, plastic overalls, and socks. It seems designed not to let anything in from the outside except through the filter of the respirator, although it probably will hinder movement and won't protect against anything except chemicals or radiation", + aliases: vec!("rad suit", "radsuit"), + weight: 2000, + wear_data: Some(WearData { + covers_parts: vec!( + BodyPart::Head, + BodyPart::Face, + BodyPart::Hands, + BodyPart::Arms, + BodyPart::Chest, + BodyPart::Back, + BodyPart::Groin, + BodyPart::Legs, + BodyPart::Feet + ), + thickness: 4.0, + dodge_penalty: 1.0, + soaks: vec!( + (DamageType::Radiation, + SoakData { + min_soak: 500.0, + max_soak: 600.0, + damage_probability_per_soak: 0.01 + } + ), + ).into_iter().collect(), + }), + ..Default::default() + } + ), )) } diff --git a/blastmud_game/src/static_content/possession_type/lights.rs b/blastmud_game/src/static_content/possession_type/utilities.rs similarity index 70% rename from blastmud_game/src/static_content/possession_type/lights.rs rename to blastmud_game/src/static_content/possession_type/utilities.rs index 02e40dd2..67885674 100644 --- a/blastmud_game/src/static_content/possession_type/lights.rs +++ b/blastmud_game/src/static_content/possession_type/utilities.rs @@ -1,3 +1,5 @@ +#[double] +use crate::db::DBTrans; use crate::{ message_handler::user_commands::{user_error, UResult, VerbContext}, models::{ @@ -6,16 +8,17 @@ use crate::{ }, regular_tasks::{TaskHandler, TaskRunContext}, services::comms::broadcast_to_room, - static_content::possession_type::ChargeData, + static_content::{possession_type::ChargeData, room::room_map_by_code}, DResult, }; use async_trait::async_trait; use chrono::Utc; use log::warn; +use mockall_double::double; use once_cell::sync::OnceCell; use std::time; -use super::{PossessionData, PossessionType, TurnToggleHandler}; +use super::{Describer, PossessionData, PossessionType, TurnToggleHandler}; pub struct LanternHandler {} @@ -163,21 +166,55 @@ impl TaskHandler for DischargeTaskHandler { } pub static DISCHARGE_TASK: &(dyn TaskHandler + Sync + Send) = &DischargeTaskHandler; +pub struct GeigerCounterDescriber; + +#[async_trait] +impl Describer for GeigerCounterDescriber { + async fn describe_for(&self, _trans: &DBTrans, _item: &Item, player: &Item) -> UResult { + let (loc_type, loc_code) = match player.location.split_once("/") { + None => return Ok("It doesn't currently show a reading.\n".to_owned()), + Some(v) => v, + }; + let level = if loc_type != "room" { + 0 + } else { + room_map_by_code() + .get(loc_code) + .map(|r| r.environment.radiation) + .unwrap_or(0) + }; + Ok(format!( + "It shows a reading of {} millisieverts/hour.\n", + level * 360 + )) + } +} + pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { static D: OnceCell> = OnceCell::new(); - &D.get_or_init(|| vec![(PossessionType::ElectricLantern, PossessionData { - display: "electric lantern", - aliases: vec!["lantern"], - details: "A rechargeable electric lantern. It is made of yellow plastic, interspersed with banks of tiny flat white LEDs. It looks like it would illuminate a dark room well, although it probably isn't bright enough to let you see anything in the next room over.", - weight: 300, - turn_toggle_handler: Some(&LANTERN_HANDLER), - charge_data: Some(ChargeData { - max_charges: 20, - charge_name_prefix: "bar", - charge_name_suffix: "of power", - electric_recharge: true, - ..ChargeData::default() + &D.get_or_init(|| vec![ + (PossessionType::ElectricLantern, PossessionData { + display: "electric lantern", + aliases: vec!["lantern"], + details: "A rechargeable electric lantern. It is made of yellow plastic, interspersed with banks of tiny flat white LEDs. It looks like it would illuminate a dark room well, although it probably isn't bright enough to let you see anything in the next room over.", + weight: 300, + turn_toggle_handler: Some(&LANTERN_HANDLER), + charge_data: Some(ChargeData { + max_charges: 20, + charge_name_prefix: "bar", + charge_name_suffix: "of power", + electric_recharge: true, + ..ChargeData::default() + }), + ..Default::default() }), - ..Default::default() - })]) + (PossessionType::GeigerCounter, PossessionData { + display: "geiger counter", + aliases: vec!["counter"], + details: "A sturdy black rectangular box with a yellow rubber protective case. On the front is a display showing numbers. Beneath the display is the text: 'OORANS approved Geiger Counter. Calibrated to predict effective dose of radiation from exposure to standard fallout. For use by trained personnel only'. ", + weight: 300, + computed_extra_details: Some(&GeigerCounterDescriber), + ..Default::default() + }) + ]) } diff --git a/blastmud_game/src/static_content/room/melbs.yaml b/blastmud_game/src/static_content/room/melbs.yaml index 3ad02692..2f80411a 100644 --- a/blastmud_game/src/static_content/room/melbs.yaml +++ b/blastmud_game/src/static_content/room/melbs.yaml @@ -1631,9 +1631,10 @@ y: 0 z: 0 exits: - - direction: west - - direction: east - direction: north + - direction: east + - direction: south + - direction: west - zone: oorans code: oorans_training name: OORANS Training Centre @@ -1695,8 +1696,10 @@ exits: - direction: north stock_list: - - possession_type: !RadSafetyTest - list_price: 1000 + - possession_type: !GeigerCounter + list_price: 2500 + - possession_type: !RadSuit + list_price: 4000 - zone: melbs code: melbs_williamsst_collinsst name: Williams St & Collins St