diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 8666bf2..9cf198e 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -44,6 +44,7 @@ mod ignore; pub mod improvise; mod install; mod inventory; +mod invincible; mod list; pub mod load; mod login; @@ -69,6 +70,7 @@ pub mod say; mod scan; pub mod scavenge; mod score; +mod sell; mod share; mod sign; pub mod sit; @@ -248,6 +250,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "sc" => score::VERB, "score" => score::VERB, + "sell" => sell::VERB, + "share" => share::VERB, "serious" => share::VERB, "amicable" => share::VERB, @@ -290,6 +294,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { }; static STAFF_COMMANDS: UserVerbRegistry = phf_map! { + "staff_invincible" => invincible::VERB, "staff_reset_spawns" => reset_spawns::VERB, "staff_show" => staff_show::VERB, }; diff --git a/blastmud_game/src/message_handler/user_commands/buy.rs b/blastmud_game/src/message_handler/user_commands/buy.rs index 9f3b0cf..f151b30 100644 --- a/blastmud_game/src/message_handler/user_commands/buy.rs +++ b/blastmud_game/src/message_handler/user_commands/buy.rs @@ -55,6 +55,9 @@ impl UserVerb for Verb { } for stock in &room.stock_list { + if !stock.can_buy { + continue; + } if let Some(possession_type) = possession_data().get(&stock.possession_type) { if possession_type .display diff --git a/blastmud_game/src/message_handler/user_commands/delete.rs b/blastmud_game/src/message_handler/user_commands/delete.rs index ef2b826..5bf4324 100644 --- a/blastmud_game/src/message_handler/user_commands/delete.rs +++ b/blastmud_game/src/message_handler/user_commands/delete.rs @@ -95,6 +95,7 @@ async fn reset_stats(ctx: &mut VerbContext<'_>) -> UResult<()> { user_dat.raw_skills = BTreeMap::new(); user_dat.wristpad_hacks = vec![]; user_dat.scan_codes = vec![]; + user_dat.quest_progress = None; calculate_total_stats_skills_for_user(&mut player_item, &user_dat); ctx.trans.save_user_model(&user_dat).await?; ctx.trans.save_item_model(&player_item).await?; diff --git a/blastmud_game/src/message_handler/user_commands/invincible.rs b/blastmud_game/src/message_handler/user_commands/invincible.rs new file mode 100644 index 0000000..437f1d8 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/invincible.rs @@ -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 staff_invincible on or staff_invincible off") + .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; diff --git a/blastmud_game/src/message_handler/user_commands/list.rs b/blastmud_game/src/message_handler/user_commands/list.rs index 0222c07..0f8a410 100644 --- a/blastmud_game/src/message_handler/user_commands/list.rs +++ b/blastmud_game/src/message_handler/user_commands/list.rs @@ -46,6 +46,9 @@ impl UserVerb for Verb { )); for stock in &room.stock_list { + if !stock.can_buy { + continue; + } if let Some(possession_type) = possession_data().get(&stock.possession_type) { let display = &possession_type.display; diff --git a/blastmud_game/src/message_handler/user_commands/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index 1864096..e09154b 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -15,7 +15,9 @@ use crate::{ models::{ consent::ConsentType, effect::EffectType, - item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType}, + item::{ + ActiveClimb, DoorState, Item, ItemFlag, ItemSpecialData, LocationActionType, SkillType, + }, }, regular_tasks::queued_command::{ queue_command, MovementSource, QueueCommand, QueueCommandHandler, QueuedCommandContext, @@ -627,6 +629,7 @@ async fn attempt_move_immediate( ) .await? >= 0.0 + || ctx.item.flags.contains(&ItemFlag::Invincible) { if let Some((sess, _)) = session.as_ref() { ctx.trans diff --git a/blastmud_game/src/message_handler/user_commands/sell.rs b/blastmud_game/src/message_handler/user_commands/sell.rs new file mode 100644 index 0000000..a248922 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/sell.rs @@ -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; diff --git a/blastmud_game/src/models/item.rs b/blastmud_game/src/models/item.rs index 92b0da3..c7f1cfd 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -340,6 +340,7 @@ pub enum ItemFlag { NoUrgesHere, DontListInLook, AllowShare, + Invincible, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] diff --git a/blastmud_game/src/models/journal.rs b/blastmud_game/src/models/journal.rs index 29ff993..35ec7bc 100644 --- a/blastmud_game/src/models/journal.rs +++ b/blastmud_game/src/models/journal.rs @@ -11,6 +11,7 @@ pub enum JournalType { // Misc Died, SharedWithPlayer, + BribedJosephineForRedCode, } #[derive(Serialize, Deserialize, Clone, Debug, Ord, PartialOrd, Eq, PartialEq)] diff --git a/blastmud_game/src/models/user.rs b/blastmud_game/src/models/user.rs index fcbf283..48f6871 100644 --- a/blastmud_game/src/models/user.rs +++ b/blastmud_game/src/models/user.rs @@ -27,6 +27,20 @@ pub struct UserExperienceData { pub crafted_items: BTreeMap, } +#[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)] pub enum UserFlag { Staff, @@ -89,6 +103,7 @@ pub struct User { pub credits: u64, pub danger_code: Option, pub user_flags: Vec, + pub quest_progress: Option, // Reminder: Consider backwards compatibility when updating this. } @@ -222,6 +237,7 @@ impl Default for User { credits: 500, danger_code: None, user_flags: vec![], + quest_progress: None, } } } diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index a19e3d4..a659d35 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -163,6 +163,7 @@ async fn process_attack( .map(|u| u.stress.value) .unwrap_or(0) > 8000 + && !attacker_item.flags.contains(&ItemFlag::Invincible) { let msg = format!( "{} 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?; - 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!( "{} dodges out of the way of {}'s attack.\n", victim_item.display_for_sentence(1, true), @@ -559,7 +562,7 @@ pub async fn consider_reward_for( by_item: &mut Item, for_item: &Item, ) -> DResult<()> { - if by_item.item_type != "player" { + if by_item.item_type != "player" || by_item.flags.contains(&ItemFlag::Invincible) { return Ok(()); } 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) -> U .map(|lp| (lp + Duration::seconds(pow_delay)) - Utc::now()) { 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!( "You can't powerattack again for another {} seconds.", d.num_seconds() diff --git a/blastmud_game/src/services/urges.rs b/blastmud_game/src/services/urges.rs index 04ebf01..654e622 100644 --- a/blastmud_game/src/services/urges.rs +++ b/blastmud_game/src/services/urges.rs @@ -334,7 +334,7 @@ pub async fn change_stress_considering_cool( who: &mut Item, max_magnitude: i64, ) -> DResult<()> { - if !who.flags.contains(&ItemFlag::HasUrges) { + if !who.flags.contains(&ItemFlag::HasUrges) || who.flags.contains(&ItemFlag::Invincible) { return Ok(()); } let cool = who.total_stats.get(&StatType::Cool).unwrap_or(&8.0); diff --git a/blastmud_game/src/static_content/journals.rs b/blastmud_game/src/static_content/journals.rs index cd597f3..1218ead 100644 --- a/blastmud_game/src/static_content/journals.rs +++ b/blastmud_game/src/static_content/journals.rs @@ -67,6 +67,11 @@ pub fn journal_types() -> &'static BTreeMap { details: "sharing knowledge in a conversation [with another player]", 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()); } diff --git a/blastmud_game/src/static_content/npc/sewer_npcs.rs b/blastmud_game/src/static_content/npc/sewer_npcs.rs index 57e765c..8727cf9 100644 --- a/blastmud_game/src/static_content/npc/sewer_npcs.rs +++ b/blastmud_game/src/static_content/npc/sewer_npcs.rs @@ -100,7 +100,7 @@ pub fn npc_list() -> Vec { spawn_location: format!("room/melbs_sewers_{}", &spawn_loc), spawn_possessions: vec![ NPCSpawnPossession { - what: PossessionType::Dagger, + what: PossessionType::RadiantPredatorDagger, action_type: LocationActionType::Wielded, wear_layer: 0, }, @@ -202,7 +202,7 @@ pub fn npc_list() -> Vec { player_consents: vec!(ConsentType::Fight), spawn_possessions: vec![ NPCSpawnPossession { - what: PossessionType::Dagger, + what: PossessionType::Sjambok, action_type: LocationActionType::Wielded, wear_layer: 0, }, diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index e8fea47..5572d1d 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -415,6 +415,7 @@ pub enum PossessionType { // Weapons: Blades ButcherKnife, Dagger, + RadiantPredatorDagger, RusticKatana, SawtoothMachete, Electroblade, diff --git a/blastmud_game/src/static_content/possession_type/blade.rs b/blastmud_game/src/static_content/possession_type/blade.rs index 1ffff4a..3f9490e 100644 --- a/blastmud_game/src/static_content/possession_type/blade.rs +++ b/blastmud_game/src/static_content/possession_type/blade.rs @@ -84,6 +84,46 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { }), ..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, PossessionData { display: "sawtooth machete", diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index 6c90dd3..9147f97 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -5,7 +5,7 @@ use super::{ #[double] use crate::db::DBTrans; use crate::{ - message_handler::user_commands::{CommandHandlingError, UResult}, + message_handler::user_commands::{CommandHandlingError, UResult, VerbContext}, models::{ effect::SimpleEffect, item::{DoorState, Item, ItemFlag}, @@ -433,15 +433,19 @@ pub struct SecondaryZoneRecord { } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +#[serde(default)] pub struct RoomStock { pub possession_type: PossessionType, pub list_price: u64, pub poverty_discount: bool, + pub can_buy: bool, + pub can_sell: Option, // 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)] pub enum ScanCode { SewerAccess, + RedImperialCode, } impl Default for RoomStock { @@ -450,6 +454,8 @@ impl Default for RoomStock { possession_type: PossessionType::AntennaWhip, list_price: 1000000000, poverty_discount: false, + can_buy: true, + can_sell: Some(8000), } } } @@ -486,6 +492,16 @@ pub trait RoomEnterTrigger { pub trait RoomExitTrigger { 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 zone: String, @@ -512,6 +528,7 @@ pub struct Room { pub journal: Option, pub enter_trigger: Option>, pub exit_trigger: Option>, + pub sell_trigger: Option>, pub scavtable: ScavtableType, } @@ -539,6 +556,7 @@ impl Default for Room { journal: None, enter_trigger: None, exit_trigger: None, + sell_trigger: None, scavtable: ScavtableType::Nothing, } } @@ -613,6 +631,7 @@ impl Into for SimpleRoom { Box::new(RoomEffectEntryTrigger { effects: fx }) as Box<(dyn RoomEnterTrigger + std::marker::Send + Sync + 'static)> }), + sell_trigger: None, scavtable: self.scavtable, } } diff --git a/blastmud_game/src/static_content/room/melbs_sewers.rs b/blastmud_game/src/static_content/room/melbs_sewers.rs index 5834f3e..d746386 100644 --- a/blastmud_game/src/static_content/room/melbs_sewers.rs +++ b/blastmud_game/src/static_content/room/melbs_sewers.rs @@ -1,8 +1,17 @@ -use super::{Room, SimpleRoom}; +use super::{Room, RoomSellTrigger, ScanCode, SimpleRoom}; use crate::{ - models::item::Scavtype, - static_content::{possession_type::PossessionType, scavtable::Scavinfo}, + message_handler::user_commands::{UResult, UserError, VerbContext}, + 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; pub fn sewer_scavtable() -> Vec { @@ -15,10 +24,81 @@ pub fn sewer_scavtable() -> Vec { }] } +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!("Josephine whispers to you: \"Thank you - those daggers will really help me \ + build my defence system.\"\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 { from_yaml_str::>>(include_str!("melbs_sewers.yaml")) .unwrap() .into_iter() .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() } diff --git a/blastmud_game/src/static_content/room/melbs_sewers.yaml b/blastmud_game/src/static_content/room/melbs_sewers.yaml index bc8adf5..44a9257 100644 --- a/blastmud_game/src/static_content/room/melbs_sewers.yaml +++ b/blastmud_game/src/static_content/room/melbs_sewers.yaml @@ -1016,6 +1016,7 @@ 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 + # Caution: Code triggers special case sell trigger code: melbs_sewers_subsewer_josephine name: Josephine's cavern short: JO @@ -1039,7 +1040,7 @@ 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.\"" + message: "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.\"" stock_list: - possession_type: !MediumTraumaKit list_price: 120 @@ -1047,6 +1048,10 @@ - possession_type: !GreasyBurger list_price: 15 poverty_discount: false + - possession_type: !RadiantPredatorDagger + list_price: 120 + can_buy: false + can_sell: 10000 - zone: melbs_sewers code: melbs_sewers_10h name: Vast sewer cavern