Allow selling in stores, with Josephine special behaviour

Also added a staff invincible mode to help clean out NPCs with wrong
inventory.
This commit is contained in:
Condorra 2024-02-26 22:35:55 +11:00
parent a2652e471d
commit 19cef2d9c4
19 changed files with 376 additions and 12 deletions

View File

@ -44,6 +44,7 @@ mod ignore;
pub mod improvise; pub mod improvise;
mod install; mod install;
mod inventory; mod inventory;
mod invincible;
mod list; mod list;
pub mod load; pub mod load;
mod login; mod login;
@ -69,6 +70,7 @@ pub mod say;
mod scan; mod scan;
pub mod scavenge; pub mod scavenge;
mod score; mod score;
mod sell;
mod share; mod share;
mod sign; mod sign;
pub mod sit; pub mod sit;
@ -248,6 +250,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
"sc" => score::VERB, "sc" => score::VERB,
"score" => score::VERB, "score" => score::VERB,
"sell" => sell::VERB,
"share" => share::VERB, "share" => share::VERB,
"serious" => share::VERB, "serious" => share::VERB,
"amicable" => share::VERB, "amicable" => share::VERB,
@ -290,6 +294,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
}; };
static STAFF_COMMANDS: UserVerbRegistry = phf_map! { static STAFF_COMMANDS: UserVerbRegistry = phf_map! {
"staff_invincible" => invincible::VERB,
"staff_reset_spawns" => reset_spawns::VERB, "staff_reset_spawns" => reset_spawns::VERB,
"staff_show" => staff_show::VERB, "staff_show" => staff_show::VERB,
}; };

View File

@ -55,6 +55,9 @@ impl UserVerb for Verb {
} }
for stock in &room.stock_list { for stock in &room.stock_list {
if !stock.can_buy {
continue;
}
if let Some(possession_type) = possession_data().get(&stock.possession_type) { if let Some(possession_type) = possession_data().get(&stock.possession_type) {
if possession_type if possession_type
.display .display

View File

@ -95,6 +95,7 @@ async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> {
user_dat.raw_skills = BTreeMap::new(); user_dat.raw_skills = BTreeMap::new();
user_dat.wristpad_hacks = vec![]; user_dat.wristpad_hacks = vec![];
user_dat.scan_codes = vec![]; user_dat.scan_codes = vec![];
user_dat.quest_progress = None;
calculate_total_stats_skills_for_user(&mut player_item, &user_dat); calculate_total_stats_skills_for_user(&mut player_item, &user_dat);
ctx.trans.save_user_model(&user_dat).await?; ctx.trans.save_user_model(&user_dat).await?;
ctx.trans.save_item_model(&player_item).await?; ctx.trans.save_item_model(&player_item).await?;

View File

@ -0,0 +1,43 @@
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
use crate::models::item::ItemFlag;
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 requester = get_player_item_or_fail(ctx).await?;
let remaining = remaining.trim();
let state = if remaining == "on" {
true
} else if remaining == "off" {
false
} else {
return user_error(
ansi!("use <bold>staff_invincible on<reset> or <bold>staff_invincible off<reset>")
.to_owned(),
);
};
let mut requester = (*requester).clone();
requester.flags = requester
.flags
.into_iter()
.filter(|f| f != &ItemFlag::Invincible)
.collect();
if state {
requester.flags.push(ItemFlag::Invincible);
}
ctx.trans.save_item_model(&requester).await?;
Ok(())
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -46,6 +46,9 @@ impl UserVerb for Verb {
)); ));
for stock in &room.stock_list { for stock in &room.stock_list {
if !stock.can_buy {
continue;
}
if let Some(possession_type) = possession_data().get(&stock.possession_type) { if let Some(possession_type) = possession_data().get(&stock.possession_type) {
let display = &possession_type.display; let display = &possession_type.display;

View File

@ -15,7 +15,9 @@ use crate::{
models::{ models::{
consent::ConsentType, consent::ConsentType,
effect::EffectType, effect::EffectType,
item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType}, item::{
ActiveClimb, DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, SkillType,
},
}, },
regular_tasks::queued_command::{ regular_tasks::queued_command::{
queue_command, MovementSource, QueueCommand, QueueCommandHandler, QueuedCommandContext, queue_command, MovementSource, QueueCommand, QueueCommandHandler, QueuedCommandContext,
@ -627,6 +629,7 @@ async fn attempt_move_immediate(
) )
.await? .await?
>= 0.0 >= 0.0
|| ctx.item.flags.contains(&ItemFlag::Invincible)
{ {
if let Some((sess, _)) = session.as_ref() { if let Some((sess, _)) = session.as_ref() {
ctx.trans ctx.trans

View File

@ -0,0 +1,135 @@
use super::{
get_player_item_or_fail, search_item_for_user, user_error, UResult, UserVerb, UserVerbRef,
VerbContext,
};
use crate::{
db::ItemSearchParams,
language::pluralise,
models::item::Item,
services::combat::max_health,
static_content::{
possession_type::possession_data,
room::{self, Room},
},
};
use async_trait::async_trait;
async fn check_sell_trigger(
ctx: &mut VerbContext<'_>,
player_item: &Item,
room: &Room,
sell_item: &Item,
) -> UResult<()> {
let trigger = match room.sell_trigger.as_ref() {
None => return Ok(()),
Some(tr) => tr,
};
trigger.handle_sell(ctx, room, player_item, sell_item).await
}
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(
"Nobody seems to listen when you try to sell... possibly because you're dead."
.to_owned(),
)?
}
let (heretype, herecode) = player_item
.location
.split_once("/")
.unwrap_or(("room", "repro_xv_chargen"));
if heretype != "room" {
user_error("Can't sell anything because you're not in a shop.".to_owned())?;
}
let room = match room::room_map_by_code().get(herecode) {
None => user_error("Can't find that shop.".to_owned())?,
Some(r) => r,
};
if room.stock_list.is_empty() {
user_error("Can't sell anything because you're not in a shop.".to_owned())?
}
let sell_item = search_item_for_user(
ctx,
&ItemSearchParams {
include_contents: true,
item_type_only: Some("possession"),
..ItemSearchParams::base(&player_item, remaining)
},
)
.await?;
if let Some(charge_data) = sell_item
.possession_type
.as_ref()
.and_then(|pt| possession_data().get(pt))
.and_then(|pd| pd.charge_data.as_ref())
{
if charge_data.max_charges > sell_item.charges {
user_error(format!(
"No one will want to buy a used {}!",
&sell_item.display
))?
}
}
if sell_item.health < max_health(&sell_item) {
user_error(format!(
"No one will want to buy a damaged {}!",
&sell_item.display
))?
}
for stock in &room.stock_list {
if Some(stock.possession_type.clone()) != sell_item.possession_type {
continue;
}
let stats = ctx.trans.get_location_stats(&sell_item.refstr()).await?;
if stats.total_count > 0 {
user_error("Shouldn't you empty it first?".to_owned())?;
}
let sell_discount = match stock.can_sell {
None => continue,
Some(d) => d,
};
if let Some(user) = ctx.user_dat.as_mut() {
let sell_price =
((stock.list_price as f64) * (sell_discount as f64 / 10000.0)) as u64;
user.credits += stock.list_price;
ctx.trans
.delete_item(&sell_item.item_type, &sell_item.item_code)
.await?;
ctx.trans
.queue_for_session(
&ctx.session,
Some(&format!(
"Your wristpad beeps for a credit of {} credits.\n",
sell_price
)),
)
.await?;
check_sell_trigger(ctx, &player_item, &room, &sell_item).await?;
return Ok(());
}
}
user_error(format!(
"Sorry, this store doesn't buy {}!",
pluralise(&sell_item.display)
))?
}
}
static VERB_INT: Verb = Verb;
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;

View File

@ -340,6 +340,7 @@ pub enum ItemFlag {
NoUrgesHere, NoUrgesHere,
DontListInLook, DontListInLook,
AllowShare, AllowShare,
Invincible,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]

View File

@ -11,6 +11,7 @@ pub enum JournalType {
// Misc // Misc
Died, Died,
SharedWithPlayer, SharedWithPlayer,
BribedJosephineForRedCode,
} }
#[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]

View File

@ -27,6 +27,20 @@ pub struct UserExperienceData {
pub crafted_items: BTreeMap<String, u64>, pub crafted_items: BTreeMap<String, u64>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
#[serde(default)]
pub struct QuestProgress {
pub daggers_sold_to_josephine: u8,
}
impl Default for QuestProgress {
fn default() -> Self {
Self {
daggers_sold_to_josephine: 0,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)] #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub enum UserFlag { pub enum UserFlag {
Staff, Staff,
@ -89,6 +103,7 @@ pub struct User {
pub credits: u64, pub credits: u64,
pub danger_code: Option<String>, pub danger_code: Option<String>,
pub user_flags: Vec<UserFlag>, pub user_flags: Vec<UserFlag>,
pub quest_progress: Option<QuestProgress>,
// Reminder: Consider backwards compatibility when updating this. // Reminder: Consider backwards compatibility when updating this.
} }
@ -222,6 +237,7 @@ impl Default for User {
credits: 500, credits: 500,
danger_code: None, danger_code: None,
user_flags: vec![], user_flags: vec![],
quest_progress: None,
} }
} }
} }

View File

@ -163,6 +163,7 @@ async fn process_attack(
.map(|u| u.stress.value) .map(|u| u.stress.value)
.unwrap_or(0) .unwrap_or(0)
> 8000 > 8000
&& !attacker_item.flags.contains(&ItemFlag::Invincible)
{ {
let msg = format!( let msg = format!(
"{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n", "{} looks like {} wanted to attack {}, but was too tired and stressed to do it.\n",
@ -218,7 +219,9 @@ async fn process_attack(
change_stress_considering_cool(&ctx.trans, attacker_item, 100).await?; change_stress_considering_cool(&ctx.trans, attacker_item, 100).await?;
if dodge_result > attack_result { if (dodge_result > attack_result && !attacker_item.flags.contains(&ItemFlag::Invincible))
|| victim_item.flags.contains(&ItemFlag::Invincible)
{
let msg = format!( let msg = format!(
"{} dodges out of the way of {}'s attack.\n", "{} dodges out of the way of {}'s attack.\n",
victim_item.display_for_sentence(1, true), victim_item.display_for_sentence(1, true),
@ -559,7 +562,7 @@ pub async fn consider_reward_for(
by_item: &mut Item, by_item: &mut Item,
for_item: &Item, for_item: &Item,
) -> DResult<()> { ) -> DResult<()> {
if by_item.item_type != "player" { if by_item.item_type != "player" || by_item.flags.contains(&ItemFlag::Invincible) {
return Ok(()); return Ok(());
} }
let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? { let (session, _) = match trans.find_session_for_player(&by_item.item_code).await? {
@ -1045,7 +1048,7 @@ pub async fn switch_to_power_attack(ctx: &VerbContext<'_>, who: &Arc<Item>) -> U
.map(|lp| (lp + Duration::seconds(pow_delay)) - Utc::now()) .map(|lp| (lp + Duration::seconds(pow_delay)) - Utc::now())
{ {
None => {} None => {}
Some(d) if d < Duration::seconds(0) => {} Some(d) if d < Duration::seconds(0) || who.flags.contains(&ItemFlag::Invincible) => {}
Some(d) => user_error(format!( Some(d) => user_error(format!(
"You can't powerattack again for another {} seconds.", "You can't powerattack again for another {} seconds.",
d.num_seconds() d.num_seconds()

View File

@ -334,7 +334,7 @@ pub async fn change_stress_considering_cool(
who: &mut Item, who: &mut Item,
max_magnitude: i64, max_magnitude: i64,
) -> DResult<()> { ) -> DResult<()> {
if !who.flags.contains(&ItemFlag::HasUrges) { if !who.flags.contains(&ItemFlag::HasUrges) || who.flags.contains(&ItemFlag::Invincible) {
return Ok(()); return Ok(());
} }
let cool = who.total_stats.get(&StatType::Cool).unwrap_or(&8.0); let cool = who.total_stats.get(&StatType::Cool).unwrap_or(&8.0);

View File

@ -67,6 +67,11 @@ pub fn journal_types() -> &'static BTreeMap<JournalType, JournalData> {
details: "sharing knowledge in a conversation [with another player]", details: "sharing knowledge in a conversation [with another player]",
xp: 200, xp: 200,
}), }),
(JournalType::BribedJosephineForRedCode, JournalData {
name: "Bribed Josephine",
details: "got the red code off Josephine by selling lots of blades",
xp: 250,
}),
).into_iter().collect()); ).into_iter().collect());
} }

View File

@ -100,7 +100,7 @@ pub fn npc_list() -> Vec<NPC> {
spawn_location: format!("room/melbs_sewers_{}", &spawn_loc), spawn_location: format!("room/melbs_sewers_{}", &spawn_loc),
spawn_possessions: vec![ spawn_possessions: vec![
NPCSpawnPossession { NPCSpawnPossession {
what: PossessionType::Dagger, what: PossessionType::RadiantPredatorDagger,
action_type: LocationActionType::Wielded, action_type: LocationActionType::Wielded,
wear_layer: 0, wear_layer: 0,
}, },
@ -202,7 +202,7 @@ pub fn npc_list() -> Vec<NPC> {
player_consents: vec!(ConsentType::Fight), player_consents: vec!(ConsentType::Fight),
spawn_possessions: vec![ spawn_possessions: vec![
NPCSpawnPossession { NPCSpawnPossession {
what: PossessionType::Dagger, what: PossessionType::Sjambok,
action_type: LocationActionType::Wielded, action_type: LocationActionType::Wielded,
wear_layer: 0, wear_layer: 0,
}, },

View File

@ -415,6 +415,7 @@ pub enum PossessionType {
// Weapons: Blades // Weapons: Blades
ButcherKnife, ButcherKnife,
Dagger, Dagger,
RadiantPredatorDagger,
RusticKatana, RusticKatana,
SawtoothMachete, SawtoothMachete,
Electroblade, Electroblade,

View File

@ -84,6 +84,46 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
}), }),
..Default::default() ..Default::default()
}), }),
(PossessionType::RadiantPredatorDagger,
PossessionData {
display: "radiant predator dagger",
aliases: vec!["dagger"],
details: "A 30 cm long stainless steel blade, sharp on two edges with a pointy tip, this weapon looks ideal for getting started with bladed close combat. Carved into the blade is the text \"Radiant Predators are the superior lifeform. All others must DIE\"",
weight: 250,
can_butcher: true,
weapon_data: Some(WeaponData {
uses_skill: SkillType::Blades,
raw_min_to_learn: 1.0,
raw_max_to_learn: 3.0,
normal_attack: WeaponAttackData {
start_messages: vec!(
Box::new(|attacker, victim|
format!("{} points {} dagger menancingly, preparing to attack {}",
&attacker.display_for_sentence(1, true),
&attacker.pronouns.possessive,
&victim.display_for_sentence(1, false),
)
)
),
success_messages: vec!(
Box::new(|attacker, victim, part|
format!("{}'s dagger cuts into {}'s {}",
&attacker.display_for_sentence(1, true),
&victim.display_for_sentence(1, false),
&part.display(victim.sex.clone())
)
)
),
mean_damage: 3.0,
stdev_damage: 2.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec!((0.33334, DamageType::Pierce)),
..Default::default()
},
..Default::default()
}),
..Default::default()
}),
(PossessionType::SawtoothMachete, (PossessionType::SawtoothMachete,
PossessionData { PossessionData {
display: "sawtooth machete", display: "sawtooth machete",

View File

@ -5,7 +5,7 @@ use super::{
#[double] #[double]
use crate::db::DBTrans; use crate::db::DBTrans;
use crate::{ use crate::{
message_handler::user_commands::{CommandHandlingError, UResult}, message_handler::user_commands::{CommandHandlingError, UResult, VerbContext},
models::{ models::{
effect::SimpleEffect, effect::SimpleEffect,
item::{DoorState, Item, ItemFlag}, item::{DoorState, Item, ItemFlag},
@ -433,15 +433,19 @@ pub struct SecondaryZoneRecord {
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
#[serde(default)]
pub struct RoomStock { pub struct RoomStock {
pub possession_type: PossessionType, pub possession_type: PossessionType,
pub list_price: u64, pub list_price: u64,
pub poverty_discount: bool, pub poverty_discount: bool,
pub can_buy: bool,
pub can_sell: Option<u64>, // sell price in hundredths of a percent of the buy price, e.g. 8000 = 80% of buy price back.
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum ScanCode { pub enum ScanCode {
SewerAccess, SewerAccess,
RedImperialCode,
} }
impl Default for RoomStock { impl Default for RoomStock {
@ -450,6 +454,8 @@ impl Default for RoomStock {
possession_type: PossessionType::AntennaWhip, possession_type: PossessionType::AntennaWhip,
list_price: 1000000000, list_price: 1000000000,
poverty_discount: false, poverty_discount: false,
can_buy: true,
can_sell: Some(8000),
} }
} }
} }
@ -486,6 +492,16 @@ pub trait RoomEnterTrigger {
pub trait RoomExitTrigger { pub trait RoomExitTrigger {
async fn handle_exit(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>; async fn handle_exit(self: &Self, ctx: &mut QueuedCommandContext, room: &Room) -> UResult<()>;
} }
#[async_trait]
pub trait RoomSellTrigger {
async fn handle_sell(
&self,
ctx: &mut VerbContext,
room: &Room,
player_item: &Item,
sell_item: &Item,
) -> UResult<()>;
}
pub struct Room { pub struct Room {
pub zone: String, pub zone: String,
@ -512,6 +528,7 @@ pub struct Room {
pub journal: Option<JournalType>, pub journal: Option<JournalType>,
pub enter_trigger: Option<Box<dyn RoomEnterTrigger + Sync + Send>>, pub enter_trigger: Option<Box<dyn RoomEnterTrigger + Sync + Send>>,
pub exit_trigger: Option<Box<dyn RoomExitTrigger + Sync + Send>>, pub exit_trigger: Option<Box<dyn RoomExitTrigger + Sync + Send>>,
pub sell_trigger: Option<Box<dyn RoomSellTrigger + Sync + Send>>,
pub scavtable: ScavtableType, pub scavtable: ScavtableType,
} }
@ -539,6 +556,7 @@ impl Default for Room {
journal: None, journal: None,
enter_trigger: None, enter_trigger: None,
exit_trigger: None, exit_trigger: None,
sell_trigger: None,
scavtable: ScavtableType::Nothing, scavtable: ScavtableType::Nothing,
} }
} }
@ -613,6 +631,7 @@ impl<T> Into<Room> for SimpleRoom<T> {
Box::new(RoomEffectEntryTrigger { effects: fx }) Box::new(RoomEffectEntryTrigger { effects: fx })
as Box<(dyn RoomEnterTrigger + std::marker::Send + Sync + 'static)> as Box<(dyn RoomEnterTrigger + std::marker::Send + Sync + 'static)>
}), }),
sell_trigger: None,
scavtable: self.scavtable, scavtable: self.scavtable,
} }
} }

View File

@ -1,8 +1,17 @@
use super::{Room, SimpleRoom}; use super::{Room, RoomSellTrigger, ScanCode, SimpleRoom};
use crate::{ use crate::{
models::item::Scavtype, message_handler::user_commands::{UResult, UserError, VerbContext},
static_content::{possession_type::PossessionType, scavtable::Scavinfo}, models::{
item::{Item, Scavtype},
journal::JournalType,
user::QuestProgress,
},
static_content::{
journals::award_journal_if_needed, possession_type::PossessionType, scavtable::Scavinfo,
},
}; };
use ansi::ansi;
use async_trait::async_trait;
use serde_yaml::from_str as from_yaml_str; use serde_yaml::from_str as from_yaml_str;
pub fn sewer_scavtable() -> Vec<Scavinfo> { pub fn sewer_scavtable() -> Vec<Scavinfo> {
@ -15,10 +24,81 @@ pub fn sewer_scavtable() -> Vec<Scavinfo> {
}] }]
} }
pub struct JosephineSellTrigger;
#[async_trait]
impl RoomSellTrigger for JosephineSellTrigger {
async fn handle_sell(
&self,
ctx: &mut VerbContext,
_room: &Room,
player_item: &Item,
sell_item: &Item,
) -> UResult<()> {
if sell_item.possession_type == Some(PossessionType::RadiantPredatorDagger) {
let user_dat = ctx
.user_dat
.as_mut()
.ok_or_else(|| UserError("Selling while not logged in?".to_owned()))?;
if user_dat.scan_codes.contains(&ScanCode::RedImperialCode) {
return Ok(());
}
match user_dat.quest_progress.as_mut() {
None => {
user_dat.quest_progress = Some(QuestProgress {
daggers_sold_to_josephine: 1,
..Default::default()
});
}
Some(qd) => {
qd.daggers_sold_to_josephine += 1;
}
}
if user_dat
.quest_progress
.as_ref()
.map(|qp| qp.daggers_sold_to_josephine)
.unwrap_or(0)
>= 5
{
user_dat.scan_codes.push(ScanCode::RedImperialCode);
ctx.trans.queue_for_session(
&ctx.session,
Some(ansi!("<blue>Josephine whispers to you: \"Thank you - those daggers will really help me \
build my defence system.\"<reset>\n\
Josephine takes your hand and enters a code onto your wristpad. It beeps and flashes up \
a message saying: \"Red Imperial Code saved\".\n"))
).await?;
let mut player_item = (*player_item).clone();
award_journal_if_needed(
&ctx.trans,
user_dat,
&mut player_item,
JournalType::BribedJosephineForRedCode,
)
.await?;
}
ctx.trans.save_user_model(user_dat).await?;
return Ok(());
}
Ok(())
}
}
pub fn room_list() -> Vec<Room> { pub fn room_list() -> Vec<Room> {
from_yaml_str::<Vec<SimpleRoom<()>>>(include_str!("melbs_sewers.yaml")) from_yaml_str::<Vec<SimpleRoom<()>>>(include_str!("melbs_sewers.yaml"))
.unwrap() .unwrap()
.into_iter() .into_iter()
.map(|r| r.into()) .map(|r| r.into())
.map(|r: Room| {
if r.code == "melbs_sewers_subsewer_josephine" {
Room {
sell_trigger: Some(Box::new(JosephineSellTrigger)),
..r
}
} else {
r
}
})
.collect() .collect()
} }

View File

@ -1016,6 +1016,7 @@
repel_npc: true 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 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 - zone: melbs_sewers
# Caution: Code triggers special case sell trigger
code: melbs_sewers_subsewer_josephine code: melbs_sewers_subsewer_josephine
name: Josephine's cavern name: Josephine's cavern
short: <bgblack><green>JO<reset> short: <bgblack><green>JO<reset>
@ -1039,7 +1040,7 @@
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>" 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 - !DirectMessage
delay_secs: 20 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>" message: "<blue>Josephine whispers to you: \"I've got this special red key code that was apparently used by the emperor as part of some security system - my supplier apparently used to guard it 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 let you load it to your wristpad if you sell me five radiant predator blades.\"<reset>"
stock_list: stock_list:
- possession_type: !MediumTraumaKit - possession_type: !MediumTraumaKit
list_price: 120 list_price: 120
@ -1047,6 +1048,10 @@
- possession_type: !GreasyBurger - possession_type: !GreasyBurger
list_price: 15 list_price: 15
poverty_discount: false poverty_discount: false
- possession_type: !RadiantPredatorDagger
list_price: 120
can_buy: false
can_sell: 10000
- zone: melbs_sewers - zone: melbs_sewers
code: melbs_sewers_10h code: melbs_sewers_10h
name: Vast sewer cavern name: Vast sewer cavern