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:
parent
a2652e471d
commit
19cef2d9c4
@ -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,
|
||||||
};
|
};
|
||||||
|
@ -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
|
||||||
|
@ -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?;
|
||||||
|
@ -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;
|
@ -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;
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
135
blastmud_game/src/message_handler/user_commands/sell.rs
Normal file
135
blastmud_game/src/message_handler/user_commands/sell.rs
Normal 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;
|
@ -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)]
|
||||||
|
@ -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)]
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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);
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -415,6 +415,7 @@ pub enum PossessionType {
|
|||||||
// Weapons: Blades
|
// Weapons: Blades
|
||||||
ButcherKnife,
|
ButcherKnife,
|
||||||
Dagger,
|
Dagger,
|
||||||
|
RadiantPredatorDagger,
|
||||||
RusticKatana,
|
RusticKatana,
|
||||||
SawtoothMachete,
|
SawtoothMachete,
|
||||||
Electroblade,
|
Electroblade,
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user