From a2652e471dd6334af756a0732f8ef24dbfdffa18 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 24 Feb 2024 01:38:39 +1100 Subject: [PATCH] Create subsewer rooms --- blastmud_game/src/db.rs | 14 +++ .../message_handler/user_commands/movement.rs | 7 +- blastmud_game/src/models/effect.rs | 69 +++++++++++++++ blastmud_game/src/services.rs | 1 + blastmud_game/src/services/effect.rs | 53 ++++++++++++ blastmud_game/src/services/room_effects.rs | 52 ++++++++++++ blastmud_game/src/static_content/dumper.rs | 2 + .../src/static_content/npc/sewer_npcs.rs | 4 +- blastmud_game/src/static_content/room.rs | 85 ++++++++++++++++++- .../static_content/room/general_hospital.rs | 3 +- .../src/static_content/room/melbs_sewers.yaml | 79 ++++++++++++++++- docs/ImperialLootQuestDesign.mm | 2 +- 12 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 blastmud_game/src/services/room_effects.rs diff --git a/blastmud_game/src/db.rs b/blastmud_game/src/db.rs index 3b8a2cb..fc132d3 100644 --- a/blastmud_game/src/db.rs +++ b/blastmud_game/src/db.rs @@ -750,6 +750,20 @@ impl DBTrans { .collect()) } + pub async fn count_items_by_location_type<'a>( + self: &'a Self, + location: &'a str, + item_type: &'a str, + ) -> DResult { + Ok(self + .pg_trans()? + .query_one( + "SELECT COUNT(*) FROM items WHERE details->>'location' = $1 AND details->>'item_type' = $2 AND details->>'death_data' IS NULL", + &[&location, &item_type], + ).await?.get(0) + ) + } + pub async fn find_items_by_location_possession_type_excluding<'a>( self: &'a Self, location: &'a str, diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index e962966..1864096 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -31,7 +31,10 @@ use crate::{ static_content::{ dynzone::{dynzone_by_type, DynzoneType, ExitTarget as DynExitTarget}, npc::check_for_instant_aggro, - room::{self, check_for_enter_action, Direction, ExitClimb, ExitType, MaterialType}, + room::{ + self, check_for_enter_action, check_for_exit_action, Direction, ExitClimb, ExitType, + MaterialType, + }, species::species_info_map, }, DResult, @@ -723,6 +726,8 @@ async fn attempt_move_immediate( } } } + + check_for_exit_action(ctx, old_loc_type, old_loc_code).await?; } check_for_instant_aggro(&ctx.trans, &mut ctx.item).await?; diff --git a/blastmud_game/src/models/effect.rs b/blastmud_game/src/models/effect.rs index a11c7fb..8b0fa74 100644 --- a/blastmud_game/src/models/effect.rs +++ b/blastmud_game/src/models/effect.rs @@ -1,4 +1,5 @@ use super::item::Item; +use ansi_markup::parse_ansi_markup; use serde::{Deserialize, Serialize}; #[derive(PartialEq, Eq, PartialOrd, Clone, Serialize, Deserialize, Debug, Ord)] @@ -7,6 +8,7 @@ pub enum EffectType { Bandages, Bleed, Stunned, + CurrentRoom, } pub struct EffectSet { @@ -20,6 +22,10 @@ pub enum Effect { delay_secs: u64, messagef: Box String + Sync + Send>, }, + DirectMessage { + delay_secs: u64, + messagef: Box String + Sync + Send>, + }, // skill_multiplier is always positive - sign flipped for crit fails. ChangeTargetHealth { delay_secs: u64, @@ -29,3 +35,66 @@ pub enum Effect { message: Box String + Sync + Send>, }, } + +#[derive(Serialize, Deserialize)] +pub enum SimpleEffect { + BroadcastMessage { + delay_secs: u64, + message: String, + }, + DirectMessage { + delay_secs: u64, + message: String, + }, + // skill_multiplier is always positive - sign flipped for crit fails. + ChangeTargetHealth { + delay_secs: u64, + base_effect: i64, + skill_multiplier: f64, + max_effect: i64, + message: String, + }, +} + +impl From<&SimpleEffect> for Effect { + fn from(simple: &SimpleEffect) -> Effect { + match simple { + SimpleEffect::BroadcastMessage { + delay_secs, + message, + } => { + let messagem = parse_ansi_markup(message).unwrap() + "\n"; + Effect::BroadcastMessage { + delay_secs: *delay_secs, + messagef: Box::new(move |_, _, _| messagem.clone()), + } + } + SimpleEffect::DirectMessage { + delay_secs, + message, + } => { + let messagem = parse_ansi_markup(message).unwrap() + "\n"; + Effect::DirectMessage { + delay_secs: *delay_secs, + messagef: Box::new(move |_, _, _| messagem.clone()), + } + } + SimpleEffect::ChangeTargetHealth { + delay_secs, + base_effect, + skill_multiplier, + max_effect, + message, + } => { + let messagem = parse_ansi_markup(message).unwrap() + "\n"; + Effect::ChangeTargetHealth { + delay_secs: *delay_secs, + base_effect: *base_effect, + skill_multiplier: *skill_multiplier, + max_effect: *max_effect, + message: Box::new(move |_| messagem.clone()), + } + } + } + } +} diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index fc2cb83..9ff18a0 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -22,6 +22,7 @@ pub mod combat; pub mod comms; pub mod display; pub mod effect; +pub mod room_effects; pub mod sharing; pub mod skills; pub mod spawn; diff --git a/blastmud_game/src/services/effect.rs b/blastmud_game/src/services/effect.rs index 5acefb4..f113972 100644 --- a/blastmud_game/src/services/effect.rs +++ b/blastmud_game/src/services/effect.rs @@ -32,6 +32,7 @@ pub struct DelayedHealthEffect { pub struct DelayedMessageEffect { delay: u64, message: String, + is_direct: bool, } pub struct DelayedHealthTaskHandler; @@ -123,6 +124,20 @@ impl TaskHandler for DelayedMessageTaskHandler { } match item_effect_series.1.pop_front() { None => Ok(None), + Some(DelayedMessageEffect { + message, is_direct, .. + }) if is_direct => { + match ctx.trans.find_session_for_player(&item_code).await? { + None => {} + Some((sess, _)) => { + ctx.trans.queue_for_session(&sess, Some(&message)).await?; + } + } + Ok(item_effect_series + .1 + .front() + .map(|it| time::Duration::from_secs(it.delay))) + } Some(DelayedMessageEffect { message, .. }) => { broadcast_to_room(&ctx.trans, &item.location, None, &message).await?; Ok(item_effect_series @@ -203,6 +218,44 @@ pub async fn run_effects( let fx = DelayedMessageEffect { delay: *delay_secs, message: msg, + is_direct: false, + }; + let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); + target_message_series + .entry(format!( + "{}/{}", + actual_target.item_type, actual_target.item_code + )) + .and_modify(|l| l.push_back(fx.clone())) + .or_insert(VecDeque::from([fx])); + } + } + Effect::DirectMessage { + delay_secs, + messagef, + } => { + let msg = messagef( + player, + item, + target.as_ref().map(|t| &**t).unwrap_or(player), + ); + if *delay_secs == 0 { + let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); + match trans + .find_session_for_player(&actual_target.item_code) + .await? + { + None => {} + Some((sess, _)) => { + trans.queue_for_session(&sess, Some(&msg)).await?; + } + } + } else { + dispel_time_secs = dispel_time_secs.max(*delay_secs); + let fx = DelayedMessageEffect { + delay: *delay_secs, + message: msg, + is_direct: true, }; let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); target_message_series diff --git a/blastmud_game/src/services/room_effects.rs b/blastmud_game/src/services/room_effects.rs new file mode 100644 index 0000000..68b53bb --- /dev/null +++ b/blastmud_game/src/services/room_effects.rs @@ -0,0 +1,52 @@ +use async_trait::async_trait; + +use crate::{ + message_handler::user_commands::UResult, + models::effect::{EffectSet, EffectType, SimpleEffect}, + regular_tasks::queued_command::QueuedCommandContext, + static_content::room::{Room, RoomEnterTrigger, RoomExitTrigger}, +}; + +use super::effect::{cancel_effect, run_effects}; + +pub struct RoomEffectEntryTrigger { + pub effects: Vec, +} + +pub struct RoomEffectExitTrigger; + +#[async_trait] +impl RoomEnterTrigger for RoomEffectEntryTrigger { + async fn handle_enter( + self: &Self, + ctx: &mut QueuedCommandContext, + _room: &Room, + ) -> UResult<()> { + run_effects( + &ctx.trans, + &EffectSet { + effect_type: EffectType::CurrentRoom, + effects: self.effects.iter().map(|x| x.into()).collect(), + }, + &mut ctx.item, + &Default::default(), + None, + 0.0, + ) + .await?; + Ok(()) + } +} + +#[async_trait] +impl RoomExitTrigger for RoomEffectExitTrigger { + async fn handle_exit(self: &Self, ctx: &mut QueuedCommandContext, _room: &Room) -> UResult<()> { + for effect in &ctx.item.active_effects { + if effect.0 == EffectType::CurrentRoom { + cancel_effect(ctx.trans, &ctx.item, effect).await?; + } + } + + Ok(()) + } +} diff --git a/blastmud_game/src/static_content/dumper.rs b/blastmud_game/src/static_content/dumper.rs index 32b947e..8da5a95 100644 --- a/blastmud_game/src/static_content/dumper.rs +++ b/blastmud_game/src/static_content/dumper.rs @@ -14,6 +14,7 @@ fn exit_to_simple_exit(exit: &Exit) -> Option { target: exit.target.clone(), exit_climb: exit.exit_climb.clone(), needs_scan: None, + needs_npc_cleared: None, }) } @@ -49,6 +50,7 @@ fn room_to_simpleroom(room: &Room) -> Option> { journal: room.journal.clone(), scavtable: room.scavtable.clone(), scan_code: room.scan_code.clone(), + effects: None, extra: (), }) } diff --git a/blastmud_game/src/static_content/npc/sewer_npcs.rs b/blastmud_game/src/static_content/npc/sewer_npcs.rs index 1f617eb..57e765c 100644 --- a/blastmud_game/src/static_content/npc/sewer_npcs.rs +++ b/blastmud_game/src/static_content/npc/sewer_npcs.rs @@ -146,7 +146,7 @@ pub fn npc_list() -> Vec { msg: "On your wristpad: I can't believe you took down a salty! Here's something for your trouble.", payment: 300, }), - max_health: 100, + max_health: 60, aggro_pc_only: true, total_xp: 20000, total_skills: SkillType::values() @@ -184,7 +184,7 @@ pub fn npc_list() -> Vec { msg: "On your wristpad: You legend - you killed a bloody stinkfiend! You're braver than I am mate - that deserves a reward!", payment: 500, }), - max_health: 120, + max_health: 70, aggro_pc_only: true, total_xp: 30000, total_skills: SkillType::values() diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index ab8b1b2..6c90dd3 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -7,11 +7,13 @@ use crate::db::DBTrans; use crate::{ message_handler::user_commands::{CommandHandlingError, UResult}, models::{ + effect::SimpleEffect, item::{DoorState, Item, ItemFlag}, journal::JournalType, user::WristpadHack, }, regular_tasks::queued_command::QueuedCommandContext, + services::room_effects::{RoomEffectEntryTrigger, RoomEffectExitTrigger}, DResult, }; use ansi_markup::parse_ansi_markup; @@ -345,6 +347,38 @@ impl ExitBlocker for ScanBlockerInfo { } } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct NpcBlockerInfo { + block_message: String, +} +#[async_trait] +impl ExitBlocker for NpcBlockerInfo { + async fn attempt_exit(&self, ctx: &mut QueuedCommandContext, _exit: &Exit) -> UResult { + if ctx.item.item_type != "player" { + return Ok(false); + } + if ctx + .trans + .count_items_by_location_type(&ctx.item.location, "npc") + .await? + == 0 + { + return Ok(true); + } + + if let Some((sess, _sess_dat)) = ctx + .trans + .find_session_for_player(&ctx.item.item_code) + .await? + { + ctx.trans + .queue_for_session(&sess, Some(&format!("{}\n", &self.block_message))) + .await?; + } + Ok(false) + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[serde(default)] pub struct SimpleExit { @@ -352,6 +386,7 @@ pub struct SimpleExit { pub target: ExitTarget, pub exit_climb: Option, pub needs_scan: Option, + pub needs_npc_cleared: Option, } impl Default for SimpleExit { @@ -361,6 +396,7 @@ impl Default for SimpleExit { target: ExitTarget::UseGPS, exit_climb: None, needs_scan: None, + needs_npc_cleared: None, } } } @@ -371,7 +407,13 @@ impl Into for SimpleExit { direction: self.direction, target: self.target, exit_type: match self.needs_scan { - None => ExitType::Free, + None => match self.needs_npc_cleared { + None => ExitType::Free, + Some(s) => ExitType::Blocked(Box::new(NpcBlockerInfo { + block_message: parse_ansi_markup(&s.block_message).unwrap(), + ..s + })), + }, Some(s) => ExitType::Blocked(Box::new(ScanBlockerInfo { block_message: parse_ansi_markup(&s.block_message).unwrap(), ..s @@ -440,6 +482,10 @@ pub enum MaterialType { pub trait RoomEnterTrigger { async fn handle_enter(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>; } +#[async_trait] +pub trait RoomExitTrigger { + async fn handle_exit(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>; +} pub struct Room { pub zone: String, @@ -464,7 +510,8 @@ pub struct Room { pub wristpad_hack_allowed: Option, pub scan_code: Option, pub journal: Option, - pub enter_trigger: Option<&'static (dyn RoomEnterTrigger + Sync + Send)>, + pub enter_trigger: Option>, + pub exit_trigger: Option>, pub scavtable: ScavtableType, } @@ -491,6 +538,7 @@ impl Default for Room { scan_code: None, journal: None, enter_trigger: None, + exit_trigger: None, scavtable: ScavtableType::Nothing, } } @@ -522,6 +570,7 @@ pub struct SimpleRoom { pub scan_code: Option, pub journal: Option, pub scavtable: ScavtableType, + pub effects: Option>, pub extra: T, } @@ -556,7 +605,14 @@ impl Into for SimpleRoom { wristpad_hack_allowed: self.wristpad_hack_allowed, scan_code: self.scan_code, journal: self.journal, - enter_trigger: None, + exit_trigger: self.effects.as_ref().map(|_fx| { + Box::new(RoomEffectExitTrigger) + as Box<(dyn RoomExitTrigger + std::marker::Send + Sync + 'static)> + }), + enter_trigger: self.effects.map(|fx| { + Box::new(RoomEffectEntryTrigger { effects: fx }) + as Box<(dyn RoomEnterTrigger + std::marker::Send + Sync + 'static)> + }), scavtable: self.scavtable, } } @@ -585,6 +641,7 @@ impl<'a, T: Default> Default for SimpleRoom { scan_code: None, journal: None, scavtable: ScavtableType::Nothing, + effects: None, extra: Default::default(), } } @@ -714,6 +771,26 @@ pub async fn refresh_room_exits(trans: &DBTrans, template: &Item) -> DResult<()> Ok(()) } +pub async fn check_for_exit_action( + ctx: &mut QueuedCommandContext<'_>, + exit_type: &str, + exit_code: &str, +) -> UResult<()> { + if exit_type != "room" { + return Ok(()); + } + let room = match room_map_by_code().get(exit_code) { + Some(r) => r, + _ => return Ok(()), + }; + match room.exit_trigger { + Some(ref trigger) => trigger.handle_exit(ctx, room).await?, + _ => {} + } + + Ok(()) +} + pub async fn check_for_enter_action(ctx: &mut QueuedCommandContext<'_>) -> UResult<()> { let room_code = match ctx.item.location.split_once("/") { Some((loc_type, _)) if loc_type != "room" => return Ok(()), @@ -737,7 +814,7 @@ pub async fn check_for_enter_action(ctx: &mut QueuedCommandContext<'_>) -> UResu _ => {} } match room.enter_trigger { - Some(trigger) => trigger.handle_enter(ctx, room).await?, + Some(ref trigger) => trigger.handle_enter(ctx, room).await?, _ => {} } Ok(()) diff --git a/blastmud_game/src/static_content/room/general_hospital.rs b/blastmud_game/src/static_content/room/general_hospital.rs index ee47779..bf1b62c 100644 --- a/blastmud_game/src/static_content/room/general_hospital.rs +++ b/blastmud_game/src/static_content/room/general_hospital.rs @@ -52,7 +52,6 @@ impl RoomEnterTrigger for EnterERTrigger { Ok(()) } } -static ENTER_ER_TRIGGER: EnterERTrigger = EnterERTrigger; pub struct SeePatientTaskHandler; #[async_trait] @@ -190,7 +189,7 @@ pub fn room_list() -> Vec { }, ), should_caption: true, - enter_trigger: Some(&ENTER_ER_TRIGGER), + enter_trigger: Some(Box::new(EnterERTrigger)), ..Default::default() }, ) diff --git a/blastmud_game/src/static_content/room/melbs_sewers.yaml b/blastmud_game/src/static_content/room/melbs_sewers.yaml index a19988a..bc8adf5 100644 --- a/blastmud_game/src/static_content/room/melbs_sewers.yaml +++ b/blastmud_game/src/static_content/room/melbs_sewers.yaml @@ -957,7 +957,7 @@ x: 9 y: 2 z: -1 - description: A vast underground cavern, featuring giant vats of sewage, apparently forming a completely automated underground sewage treatment system. Grotesque howls that chill you to the bone ring through the foul air + description: A vast underground cavern, featuring giant vats of sewage, apparently forming a completely automated underground sewage treatment system. Grotesque howls that chill you to the bone ring through the foul air. You notice a ladder, protected by some kind of mechanical arm, and a small parapet keeping out the sewage, leading down exits: - direction: north - direction: east @@ -965,11 +965,88 @@ - direction: west - direction: southeast - direction: southwest + - direction: down + exit_climb: + height: -5 + difficulty: 6 + needs_npc_cleared: + block_message: "A shrill alarm blares as a robotic arm, emerging from the darkness below, snatches at your feet with steel-clawed grasp. A speaker crackles a message: 'for the safety of everyone, access to this area is only possible once all dangerous beings have been neutralised'." should_caption: false scavtable: CitySewer item_flags: - !DarkPlace repel_npc: true +- zone: melbs_sewers + code: melbs_sewers_platform1 + name: Subsewer access platform + short: VV + grid_coords: + x: 9 + y: 2 + z: -2 + description: A metalic platform separating a ladder leading down, and a ladder leading up. While the smell of sewage is still noticeable here, it smells slightly better here than the sewers, with a fresh breeze coming in from some small crack in the rock + exits: + - direction: up + exit_climb: + height: 5 + difficulty: 6 + - direction: down + exit_climb: + height: -5 + difficulty: 6 + item_flags: + - !DarkPlace + repel_npc: true +- zone: melbs_sewers + code: melbs_sewers_subsewer_landing1 + name: Subsewer access landing + short: == + grid_coords: + x: 9 + y: 2 + z: -3 + exits: + - direction: up + exit_climb: + height: 5 + difficulty: 6 + - direction: north + item_flags: + - !DarkPlace + repel_npc: true + description: Some kind of service tunnel that has been carved into the bedrock far beneath the sewers. Solid rock walls surround you in all directions except up and to the north. A dim light emanates from some kind of subterranean room to the north +- zone: melbs_sewers + code: melbs_sewers_subsewer_josephine + name: Josephine's cavern + short: JO + grid_coords: + x: 9 + y: 1 + z: -3 + description: A large room that has been carved into the bedrock, its walls, floor formed of grey stone. A gentle fresh breeze blows in through cracks in the rock. The room is dimly lit by yellow glowing light bulbs, suspended by cables, and apparently powered by cables that snake across the stone ceiling and up through a hole in the rock. Some of the cables snake down to power points on the wall. The room is stacked with unlabelled crates. In the northeast corner of the room, a woman sits in an office chair behind a basic wooden desk. Above her head, afixed to the wall, is a banner that says "Welcome the Josephine's Shop!" + has_power: true + exits: + - direction: south + effects: + - !DirectMessage + delay_secs: 0 + message: "Josephine whispers to you: \"Welcome - glad to see you! I don't get many customers ever since those filthy stinkfiends moved in.\"" + - !DirectMessage + delay_secs: 5 + message: "Josephine whispers to you: \"I hope you like my shop. I've got ample medical supplies, and do a burger for food. I've got power here, and my defence system keeps the baddies out!\"" + - !DirectMessage + delay_secs: 15 + message: "Josephine whispers to you: \"I'm building a more advanced defence system that will fight off enemies all through the sewers. Right now I need lots of radiant predator blades to help me build it.\"" + - !DirectMessage + delay_secs: 20 + message: "Josephine whispers to you: \"I've got these special red key code cards that were apparently used by the emperor as part of some security system - my supplier apparently used to guard them for the emperor before the empire fell, but even he doesn't know what they are for except that it's one part of a key to some ultra-secure security system. I'll give you one for every five radiant predator blades you sell here.\"" + stock_list: + - possession_type: !MediumTraumaKit + list_price: 120 + poverty_discount: false + - possession_type: !GreasyBurger + list_price: 15 + poverty_discount: false - zone: melbs_sewers code: melbs_sewers_10h name: Vast sewer cavern diff --git a/docs/ImperialLootQuestDesign.mm b/docs/ImperialLootQuestDesign.mm index 9f3db32..6ba66bc 100644 --- a/docs/ImperialLootQuestDesign.mm +++ b/docs/ImperialLootQuestDesign.mm @@ -71,7 +71,7 @@ - +