Create subsewer rooms

This commit is contained in:
Condorra 2024-02-24 01:38:39 +11:00
parent b1fecab1b0
commit a2652e471d
12 changed files with 360 additions and 11 deletions

View File

@ -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<i64> {
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,

View File

@ -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?;

View File

@ -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<dyn Fn(&Item, &Item, &Item) -> String + Sync + Send>,
},
DirectMessage {
delay_secs: u64,
messagef: Box<dyn Fn(&Item, &Item, &Item) -> 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<dyn Fn(&Item) -> 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()),
}
}
}
}
}

View File

@ -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;

View File

@ -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

View File

@ -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<SimpleEffect>,
}
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(())
}
}

View File

@ -14,6 +14,7 @@ fn exit_to_simple_exit(exit: &Exit) -> Option<SimpleExit> {
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<SimpleRoom<()>> {
journal: room.journal.clone(),
scavtable: room.scavtable.clone(),
scan_code: room.scan_code.clone(),
effects: None,
extra: (),
})
}

View File

@ -146,7 +146,7 @@ pub fn npc_list() -> Vec<NPC> {
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<NPC> {
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()

View File

@ -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<bool> {
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<ExitClimb>,
pub needs_scan: Option<ScanBlockerInfo>,
pub needs_npc_cleared: Option<NpcBlockerInfo>,
}
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<Exit> 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<WristpadHack>,
pub scan_code: Option<ScanCode>,
pub journal: Option<JournalType>,
pub enter_trigger: Option<&'static (dyn RoomEnterTrigger + Sync + Send)>,
pub enter_trigger: Option<Box<dyn RoomEnterTrigger + Sync + Send>>,
pub exit_trigger: Option<Box<dyn RoomExitTrigger + Sync + Send>>,
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<T> {
pub scan_code: Option<ScanCode>,
pub journal: Option<JournalType>,
pub scavtable: ScavtableType,
pub effects: Option<Vec<SimpleEffect>>,
pub extra: T,
}
@ -556,7 +605,14 @@ impl<T> Into<Room> for SimpleRoom<T> {
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<T> {
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(())

View File

@ -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<Room> {
},
),
should_caption: true,
enter_trigger: Some(&ENTER_ER_TRIGGER),
enter_trigger: Some(Box::new(EnterERTrigger)),
..Default::default()
},
)

View File

@ -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: <bgblack><blue>VV<reset>
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: <bgblack><yellow>==<reset>
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: <bgblack><green>JO<reset>
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: "<blue>Josephine whispers to you: \"Welcome - glad to see you! I don't get many customers ever since those filthy stinkfiends moved in.\"<reset>"
- !DirectMessage
delay_secs: 5
message: "<blue>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!\"<reset>"
- !DirectMessage
delay_secs: 15
message: "<blue>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.\"<reset>"
- !DirectMessage
delay_secs: 20
message: "<blue>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.\"<reset>"
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

View File

@ -71,7 +71,7 @@
<node TEXT="Obtain Treasury key" ID="ID_668131045" CREATED="1706963637887" MODIFIED="1706966065457">
<node TEXT="Obtain red code share" ID="ID_1686458255" CREATED="1706963694319" MODIFIED="1706964021755">
<node TEXT="Make Josephine in sub-sewer like you" ID="ID_1532259397" CREATED="1706964229320" MODIFIED="1707305448126">
<node TEXT="Bring him 10x Radiant Predator daggers" ID="ID_497635414" CREATED="1706964576337" MODIFIED="1706964864488"/>
<node TEXT="Bring her 10x Radiant Predator daggers" ID="ID_497635414" CREATED="1706964576337" MODIFIED="1707736596095"/>
<node TEXT="Access sub-sewer" ID="ID_1089305975" CREATED="1706964531187" MODIFIED="1706964572264">
<node TEXT="Fight Stinkfiends" ID="ID_1546136234" CREATED="1706964876006" MODIFIED="1706964919208">
<node TEXT="Fight crocs" ID="ID_703382098" CREATED="1706964926243" MODIFIED="1706964933162">