Allow buying a lantern that lights up the sewer.

It will be rechargeable but that's not fully implemented yet.
This commit is contained in:
Condorra 2024-01-15 22:40:45 +11:00
parent 6126bc984b
commit 15b96c1b50
9 changed files with 348 additions and 1 deletions

View File

@ -74,6 +74,7 @@ mod staff_show;
pub mod stand; pub mod stand;
mod status; mod status;
mod stop; mod stop;
mod turn;
mod uninstall; mod uninstall;
pub mod use_cmd; pub mod use_cmd;
mod vacate; mod vacate;
@ -267,6 +268,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"status" => status::VERB, "status" => status::VERB,
"stop" => stop::VERB, "stop" => stop::VERB,
"turn" => turn::VERB,
"uninstall" => uninstall::VERB, "uninstall" => uninstall::VERB,
"use" => use_cmd::VERB, "use" => use_cmd::VERB,
"vacate" => vacate::VERB, "vacate" => vacate::VERB,

View File

@ -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 <bold>turn on something<reset> or <bold>turn off something<reset>.")
.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 <bold>turn on something<reset> or <bold>turn off something<reset>.")
.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;

View File

@ -73,6 +73,12 @@ pub enum TaskDetails {
item: String, item: String,
code: String, code: String,
}, },
DischargeLight {
item: String,
},
ChargeItem {
item: String,
},
} }
impl TaskDetails { impl TaskDetails {
pub fn name(self: &Self) -> &'static str { pub fn name(self: &Self) -> &'static str {
@ -99,6 +105,8 @@ impl TaskDetails {
ResetHanoi => "ResetHanoi", ResetHanoi => "ResetHanoi",
HospitalERSeePatient { .. } => "HospitalERSeePatient", HospitalERSeePatient { .. } => "HospitalERSeePatient",
ExpireBuff { .. } => "ExpireBuff", ExpireBuff { .. } => "ExpireBuff",
DischargeLight { .. } => "DischargeLight",
ChargeItem { .. } => "ChargeItem",
// Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too. // Don't forget to add to TASK_HANDLER_REGISTRY in regular_tasks.rs too.
} }
} }

View File

@ -9,9 +9,10 @@ use crate::{
listener::{ListenerMap, ListenerSend}, listener::{ListenerMap, ListenerSend},
message_handler::user_commands::{delete, drop, hire, open, rent}, message_handler::user_commands::{delete, drop, hire, open, rent},
models::task::Task, models::task::Task,
services::{combat, effect, sharing, spawn, tempbuff, urges}, services::{charging, combat, effect, sharing, spawn, tempbuff, urges},
static_content::{ static_content::{
npc::{self, computer_museum_npcs}, npc::{self, computer_museum_npcs},
possession_type::lights,
room::general_hospital, room::general_hospital,
}, },
DResult, DResult,
@ -68,6 +69,8 @@ fn task_handler_registry(
("ResetHanoi", computer_museum_npcs::RESET_GAME_HANDLER), ("ResetHanoi", computer_museum_npcs::RESET_GAME_HANDLER),
("HospitalERSeePatient", general_hospital::SEE_PATIENT_TASK), ("HospitalERSeePatient", general_hospital::SEE_PATIENT_TASK),
("ExpireBuff", tempbuff::EXPIRE_BUFF_TASK), ("ExpireBuff", tempbuff::EXPIRE_BUFF_TASK),
("DischargeLight", lights::DISCHARGE_TASK),
("ChargeItem", charging::TASK_HANDLER),
] ]
.into_iter() .into_iter()
.collect() .collect()

View File

@ -17,6 +17,7 @@ use crate::{
use mockall_double::double; use mockall_double::double;
pub mod capacity; pub mod capacity;
pub mod charging;
pub mod combat; pub mod combat;
pub mod comms; pub mod comms;
pub mod display; pub mod display;

View File

@ -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<Option<time::Duration>> {
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;

View File

@ -28,6 +28,7 @@ mod fangs;
mod food; mod food;
pub mod head_armour; pub mod head_armour;
mod junk; mod junk;
pub mod lights;
pub mod lock; pub mod lock;
pub mod lower_armour; pub mod lower_armour;
mod meat; mod meat;
@ -441,6 +442,8 @@ pub enum PossessionType {
// Recipes // Recipes
CulinaryEssentials, CulinaryEssentials,
GrilledSteakRecipe, GrilledSteakRecipe,
// Lights
ElectricLantern,
} }
impl Into<Item> for PossessionType { impl Into<Item> for PossessionType {
@ -561,6 +564,7 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, &'static Possessio
.map(|v| ((*v).0.clone(), &(*v).1)), .map(|v| ((*v).0.clone(), &(*v).1)),
) )
.chain(books::data().iter().map(|v| ((*v).0.clone(), &(*v).1))) .chain(books::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.chain(lights::data().iter().map(|v| ((*v).0.clone(), &(*v).1)))
.collect() .collect()
}) })
} }

View File

@ -0,0 +1,181 @@
use crate::{
message_handler::user_commands::{user_error, UResult, VerbContext},
models::{
item::{Item, ItemFlag, ItemSpecialData},
task::{Task, TaskDetails, TaskMeta},
},
regular_tasks::{TaskHandler, TaskRunContext},
services::comms::broadcast_to_room,
static_content::possession_type::ChargeData,
DResult,
};
use async_trait::async_trait;
use chrono::Utc;
use log::warn;
use once_cell::sync::OnceCell;
use std::time;
use super::{PossessionData, PossessionType, TurnToggleHandler};
pub struct LanternHandler {}
#[async_trait]
impl TurnToggleHandler for LanternHandler {
async fn turn_cmd(
&self,
ctx: &mut VerbContext,
player: &Item,
what: &Item,
turn_on: bool,
) -> 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<Option<time::Duration>> {
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<Vec<(PossessionType, PossessionData)>> = 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()
})])
}

View File

@ -1624,6 +1624,9 @@
- possession_type: DrinkBottle - possession_type: DrinkBottle
list_price: 80 list_price: 80
poverty_discount: false poverty_discount: false
- possession_type: ElectricLantern
list_price: 500
poverty_discount: false
scavtable: CityStreet scavtable: CityStreet
- zone: melbs - zone: melbs
code: melbs_bourkest_190 code: melbs_bourkest_190