diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index e17f1c4..b30c571 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -74,6 +74,7 @@ mod staff_show; pub mod stand; mod status; mod stop; +mod turn; mod uninstall; pub mod use_cmd; mod vacate; @@ -267,6 +268,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "status" => status::VERB, "stop" => stop::VERB, + "turn" => turn::VERB, "uninstall" => uninstall::VERB, "use" => use_cmd::VERB, "vacate" => vacate::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/turn.rs b/blastmud_game/src/message_handler/user_commands/turn.rs new file mode 100644 index 0000000..f181e56 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/turn.rs @@ -0,0 +1,73 @@ +use crate::{db::ItemSearchParams, static_content::possession_type::possession_data}; + +use super::{ + get_player_item_or_fail, search_item_for_user, user_error, UResult, UserError, UserVerb, + UserVerbRef, VerbContext, +}; +use ansi::ansi; +use async_trait::async_trait; + +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?; + if player_item.death_data.is_some() { + user_error("You can't figure out to turn things on/off whilst dead.".to_owned())?; + } + + let (state, item_name) = remaining.split_once(" ").ok_or_else(|| { + UserError( + ansi!("Try turn on something or turn off something.") + .to_owned(), + ) + })?; + + let state = state.trim().to_lowercase(); + let to_state = if state == "on" { + true + } else if state == "off" { + false + } else { + user_error( + ansi!("Try turn on something or turn off something.") + .to_owned(), + )? + }; + + let item_name = item_name.trim(); + + let item = search_item_for_user( + ctx, + &ItemSearchParams { + include_contents: true, + include_loc_contents: true, + limit: 1, + ..ItemSearchParams::base(&player_item, item_name) + }, + ) + .await?; + + let handler = item + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + .and_then(|pd| pd.turn_toggle_handler) + .ok_or_else(|| { + UserError(format!( + "You can't turn that {}!", + if to_state { "on" } else { "off" } + )) + })?; + handler.turn_cmd(ctx, &player_item, &item, to_state).await?; + + Ok(()) + } +} +static VERB_INT: Verb = Verb; +pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef; diff --git a/blastmud_game/src/models/task.rs b/blastmud_game/src/models/task.rs index 30eea73..c503d1f 100644 --- a/blastmud_game/src/models/task.rs +++ b/blastmud_game/src/models/task.rs @@ -73,6 +73,12 @@ pub enum TaskDetails { item: String, code: String, }, + DischargeLight { + item: String, + }, + ChargeItem { + item: String, + }, } impl TaskDetails { pub fn name(self: &Self) -> &'static str { @@ -99,6 +105,8 @@ impl TaskDetails { ResetHanoi => "ResetHanoi", HospitalERSeePatient { .. } => "HospitalERSeePatient", ExpireBuff { .. } => "ExpireBuff", + DischargeLight { .. } => "DischargeLight", + ChargeItem { .. } => "ChargeItem", // Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too. } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index c69c1c4..f4f34b8 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -9,9 +9,10 @@ use crate::{ listener::{ListenerMap, ListenerSend}, message_handler::user_commands::{delete, drop, hire, open, rent}, models::task::Task, - services::{combat, effect, sharing, spawn, tempbuff, urges}, + services::{charging, combat, effect, sharing, spawn, tempbuff, urges}, static_content::{ npc::{self, computer_museum_npcs}, + possession_type::lights, room::general_hospital, }, DResult, @@ -68,6 +69,8 @@ fn task_handler_registry( ("ResetHanoi", computer_museum_npcs::RESET_GAME_HANDLER), ("HospitalERSeePatient", general_hospital::SEE_PATIENT_TASK), ("ExpireBuff", tempbuff::EXPIRE_BUFF_TASK), + ("DischargeLight", lights::DISCHARGE_TASK), + ("ChargeItem", charging::TASK_HANDLER), ] .into_iter() .collect() diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index a12a9ad..fc2cb83 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -17,6 +17,7 @@ use crate::{ use mockall_double::double; pub mod capacity; +pub mod charging; pub mod combat; pub mod comms; pub mod display; diff --git a/blastmud_game/src/services/charging.rs b/blastmud_game/src/services/charging.rs new file mode 100644 index 0000000..4444ab5 --- /dev/null +++ b/blastmud_game/src/services/charging.rs @@ -0,0 +1,72 @@ +use async_trait::async_trait; +use log::warn; + +use crate::{ + models::task::TaskDetails, + regular_tasks::{TaskHandler, TaskRunContext}, + static_content::possession_type::possession_data, + DResult, +}; +use std::time; + +use super::comms::broadcast_to_room; + +#[derive(Clone)] +pub struct RechargeTaskHandler; +#[async_trait] +impl TaskHandler for RechargeTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + let item_ref = match &ctx.task.details { + TaskDetails::ChargeItem { ref item } => item, + t => { + warn!("RechargeTaskHandler got unexpected task {:#?}", t); + return Ok(None); + } + }; + let (item_type, item_code) = match item_ref.split_once("/") { + Some(v) => v, + None => { + warn!("RechargeTaskHandler got unexpected item ref {}", item_ref); + return Ok(None); + } + }; + let item = match ctx + .trans + .find_item_by_type_code(item_type, item_code) + .await? + { + None => return Ok(None), + Some(v) => v, + }; + + let charge_data = match item + .possession_type + .as_ref() + .and_then(|pt| possession_data().get(pt)) + .and_then(|pd| pd.charge_data.as_ref()) + { + None => return Ok(None), + Some(v) => v, + }; + let mut item = (*item).clone(); + + item.charges = (item.charges + 1).min(charge_data.max_charges); + ctx.trans.save_item_model(&item).await?; + if item.charges == charge_data.max_charges { + broadcast_to_room( + &ctx.trans, + &item.location, + None, + &format!( + "{} beeps to indicate it is fully charged.\n", + item.display_for_sentence(1, true) + ), + ) + .await?; + Ok(None) + } else { + Ok(Some(time::Duration::from_secs(60))) + } + } +} +pub static TASK_HANDLER: &(dyn TaskHandler + Sync + Send) = &RechargeTaskHandler; diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index cedd67c..7378142 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -28,6 +28,7 @@ mod fangs; mod food; pub mod head_armour; mod junk; +pub mod lights; pub mod lock; pub mod lower_armour; mod meat; @@ -441,6 +442,8 @@ pub enum PossessionType { // Recipes CulinaryEssentials, GrilledSteakRecipe, + // Lights + ElectricLantern, } impl Into for PossessionType { @@ -561,6 +564,7 @@ pub fn possession_data() -> &'static BTreeMap UResult<()> { + let mut item_mut = (*what).clone(); + let old_state = match item_mut.special_data.as_ref() { + Some(ItemSpecialData::ToggleData { turned_on }) => *turned_on, + _ => false, + }; + if old_state == turn_on { + user_error(format!( + "It's already turned {}!", + if turn_on { "on" } else { "off" } + ))?; + } + if turn_on && item_mut.charges <= 0 { + user_error("It won't turn on! It probably needs charging.".to_owned())?; + } + + broadcast_to_room( + &ctx.trans, + &player.location, + None, + &format!( + "{} flicks {} a lantern.\n", + &player.display_for_sentence(1, true), + if turn_on { "on" } else { "off" } + ), + ) + .await?; + + item_mut.special_data = Some(ItemSpecialData::ToggleData { turned_on: turn_on }); + if turn_on { + ctx.trans + .upsert_task(&Task { + meta: TaskMeta { + task_code: item_mut.refstr(), + next_scheduled: Utc::now() + chrono::Duration::minutes(5), + ..Default::default() + }, + details: TaskDetails::DischargeLight { + item: item_mut.refstr(), + }, + }) + .await?; + item_mut.flags.push(ItemFlag::IlluminatesRoom); + // To make it a bit harder to use well-timed periods of darkness to + // avoid drawing any charge, use one charge to turn it on (but if they + // had exactly one left, don't immediately turn it off). + item_mut.charges -= 1; + } else { + ctx.trans + .delete_task("DischargeLight", &item_mut.refstr()) + .await?; + item_mut.flags.retain(|f| f != &ItemFlag::IlluminatesRoom); + } + ctx.trans.save_item_model(&item_mut).await?; + + Ok(()) + } +} + +static LANTERN_HANDLER: LanternHandler = LanternHandler {}; + +#[derive(Clone)] +pub struct DischargeTaskHandler; +#[async_trait] +impl TaskHandler for DischargeTaskHandler { + async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { + let item_ref = match &ctx.task.details { + TaskDetails::DischargeLight { ref item } => item, + t => { + warn!("DischargeTaskHandler got unexpected task {:#?}", t); + return Ok(None); + } + }; + let (item_type, item_code) = match item_ref.split_once("/") { + Some(v) => v, + None => { + warn!("DischargeTaskHandler got unexpected item ref {}", item_ref); + return Ok(None); + } + }; + let item = match ctx + .trans + .find_item_by_type_code(item_type, item_code) + .await? + { + None => return Ok(None), + Some(v) => v, + }; + let mut item = (*item).clone(); + item.charges = item.charges.max(1) - 1; + let lit_location = match item.location.split_once("/") { + Some((loc_type, loc_code)) if loc_type == "player" || loc_type == "npc" => { + match ctx.trans.find_item_by_type_code(loc_type, loc_code).await? { + None => item.location.clone(), + Some(pl) => pl.location.clone(), + } + } + _ => item.location.clone(), + }; + if item.charges == 1 { + broadcast_to_room( + &ctx.trans, + &lit_location, + None, + &format!( + "{} dims noticeably as if it was running out of power.\n", + item.display_for_sentence(1, true) + ), + ) + .await? + } + if item.charges == 0 { + broadcast_to_room( + &ctx.trans, + &lit_location, + None, + &format!( + "{} dims briefly before completely going out.\n", + item.display_for_sentence(1, true) + ), + ) + .await?; + item.flags.retain(|fl| { + fl != &ItemFlag::IlluminatesRoom && fl != &ItemFlag::IlluminatesAdjacentRoom + }); + } + ctx.trans.save_item_model(&item).await?; + if item.charges == 0 { + Ok(None) + } else { + Ok(Some(time::Duration::from_secs(300))) + } + } +} +pub static DISCHARGE_TASK: &(dyn TaskHandler + Sync + Send) = &DischargeTaskHandler; + +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", + }), + ..Default::default() + })]) +} diff --git a/blastmud_game/src/static_content/room/melbs.yaml b/blastmud_game/src/static_content/room/melbs.yaml index 98ca8ff..f285baa 100644 --- a/blastmud_game/src/static_content/room/melbs.yaml +++ b/blastmud_game/src/static_content/room/melbs.yaml @@ -1624,6 +1624,9 @@ - possession_type: DrinkBottle list_price: 80 poverty_discount: false + - possession_type: ElectricLantern + list_price: 500 + poverty_discount: false scavtable: CityStreet - zone: melbs code: melbs_bourkest_190