Add wristpad hack concept - enhancements to characters.
And add one hidden in the computer museum.
This commit is contained in:
parent
6dc4a870fc
commit
92d7b22921
@ -36,6 +36,7 @@ mod fire;
|
||||
pub mod follow;
|
||||
mod gear;
|
||||
pub mod get;
|
||||
mod hack;
|
||||
mod help;
|
||||
pub mod hire;
|
||||
mod ignore;
|
||||
@ -174,7 +175,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
||||
|
||||
"gear" => gear::VERB,
|
||||
"get" => get::VERB,
|
||||
|
||||
"hack" => hack::VERB,
|
||||
"hire" => hire::VERB,
|
||||
|
||||
"improv" => improvise::VERB,
|
||||
|
96
blastmud_game/src/message_handler/user_commands/hack.rs
Normal file
96
blastmud_game/src/message_handler/user_commands/hack.rs
Normal file
@ -0,0 +1,96 @@
|
||||
use crate::{
|
||||
models::user::{wristpad_hack_data, xp_to_hack_slots},
|
||||
services::skills::calculate_total_stats_skills_for_user,
|
||||
static_content::room::room_map_by_code,
|
||||
};
|
||||
|
||||
use super::{
|
||||
get_player_item_or_fail, user_error, UResult, UserError, UserVerb, UserVerbRef, VerbContext,
|
||||
};
|
||||
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 player_item = get_player_item_or_fail(ctx).await?;
|
||||
let (loc_type, loc_code) = player_item
|
||||
.location
|
||||
.split_once("/")
|
||||
.ok_or_else(|| UserError("Your location is invalid".to_owned()))?;
|
||||
if loc_type != "room" {
|
||||
user_error("You can't find a hacking unit here.".to_owned())?;
|
||||
}
|
||||
|
||||
let room = room_map_by_code()
|
||||
.get(&loc_code)
|
||||
.ok_or_else(|| UserError("Your location no longer exists!".to_owned()))?;
|
||||
|
||||
let allowed_hack = room
|
||||
.wristpad_hack_allowed
|
||||
.as_ref()
|
||||
.ok_or_else(|| UserError("You can't find a hacking unit here.".to_owned()))?;
|
||||
let hack_data = wristpad_hack_data()
|
||||
.get(allowed_hack)
|
||||
.ok_or_else(|| UserError("The hacking unit is currently broken.".to_owned()))?;
|
||||
if hack_data.name.to_lowercase() != remaining.trim() {
|
||||
user_error(format!(
|
||||
ansi!("The equipment here only allows you to <bold>hack {}<reset>"),
|
||||
&hack_data.name
|
||||
))?;
|
||||
}
|
||||
|
||||
let user = ctx
|
||||
.user_dat
|
||||
.as_mut()
|
||||
.ok_or(UserError("Please log in first".to_owned()))?;
|
||||
|
||||
let slots_available = xp_to_hack_slots(player_item.total_xp) as usize;
|
||||
let slots_used = user.wristpad_hacks.len();
|
||||
if slots_used >= slots_available {
|
||||
user_error(format!(
|
||||
"Your wristpad crashes and reboots, flashing up an error that \
|
||||
there was no space to install the hack. [You only have {} slots \
|
||||
total on your wristpad to install hacks - try getting some \
|
||||
more experience to earn more]",
|
||||
slots_available
|
||||
))?;
|
||||
}
|
||||
|
||||
if user.wristpad_hacks.contains(&allowed_hack) {
|
||||
user_error(
|
||||
"Your wristpad crashes and reboots, flashing up an error that \
|
||||
the same hack was already found on the device."
|
||||
.to_owned(),
|
||||
)?;
|
||||
}
|
||||
|
||||
user.wristpad_hacks.push(allowed_hack.clone());
|
||||
ctx.trans.save_user_model(&user).await?;
|
||||
|
||||
let mut player_mut = (*player_item).clone();
|
||||
calculate_total_stats_skills_for_user(&mut player_mut, user);
|
||||
ctx.trans.save_item_model(&player_mut).await?;
|
||||
|
||||
ctx.trans
|
||||
.queue_for_session(
|
||||
&ctx.session,
|
||||
Some(&format!(
|
||||
"Your wristpad beeps and reboots. You notice new icon on \
|
||||
it indicating the {} hack has been applied succesfully!\n",
|
||||
hack_data.name
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
static VERB_INT: Verb = Verb;
|
||||
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|
@ -1,5 +1,11 @@
|
||||
use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext};
|
||||
use crate::models::item::{SkillType, StatType};
|
||||
use crate::{
|
||||
language,
|
||||
models::{
|
||||
item::{SkillType, StatType},
|
||||
user::{wristpad_hack_data, xp_to_hack_slots},
|
||||
},
|
||||
};
|
||||
use ansi::ansi;
|
||||
use async_trait::async_trait;
|
||||
|
||||
@ -58,6 +64,35 @@ impl UserVerb for Verb {
|
||||
user.experience.spent_xp
|
||||
));
|
||||
|
||||
if !user.wristpad_hacks.is_empty() {
|
||||
let hack_names = user
|
||||
.wristpad_hacks
|
||||
.iter()
|
||||
.map(|h| {
|
||||
wristpad_hack_data()
|
||||
.get(h)
|
||||
.map(|hd| hd.name)
|
||||
.unwrap_or("UNKNOWN")
|
||||
})
|
||||
.collect::<Vec<&'static str>>();
|
||||
msg.push_str(&format!(
|
||||
"You have hacks installed on your wristpad: {}\n",
|
||||
&language::join_words(&hack_names)
|
||||
));
|
||||
}
|
||||
|
||||
let hack_slots = xp_to_hack_slots(player_item.total_xp) as usize;
|
||||
if hack_slots > user.wristpad_hacks.len() {
|
||||
let free_slots = hack_slots - user.wristpad_hacks.len();
|
||||
msg.push_str(&format!(
|
||||
"You have {} free hack slot{} on your wristpad.\n",
|
||||
free_slots,
|
||||
if free_slots == 1 { "" } else { "s" }
|
||||
));
|
||||
} else {
|
||||
msg.push_str("You have no free hack slots on your wristpad.\n");
|
||||
};
|
||||
|
||||
ctx.trans.queue_for_session(ctx.session, Some(&msg)).await?;
|
||||
|
||||
Ok(())
|
||||
|
@ -1,8 +1,13 @@
|
||||
use super::{
|
||||
item::{SkillType, StatType},
|
||||
item::{BuffImpact, Item, SkillType, StatType},
|
||||
journal::JournalState,
|
||||
};
|
||||
#[double]
|
||||
use crate::db::DBTrans;
|
||||
use crate::{message_handler::ListenerSession, DResult};
|
||||
use chrono::{DateTime, Utc};
|
||||
use mockall_double::double;
|
||||
use once_cell::sync::OnceCell;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
@ -27,6 +32,41 @@ pub enum UserFlag {
|
||||
Staff,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
|
||||
pub enum WristpadHack {
|
||||
Superdork,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)]
|
||||
pub struct WristpadHackData {
|
||||
pub name: &'static str,
|
||||
pub buff: Vec<BuffImpact>,
|
||||
}
|
||||
|
||||
pub fn wristpad_hack_data() -> &'static BTreeMap<WristpadHack, WristpadHackData> {
|
||||
static D: OnceCell<BTreeMap<WristpadHack, WristpadHackData>> = OnceCell::new();
|
||||
D.get_or_init(|| {
|
||||
vec![(
|
||||
WristpadHack::Superdork,
|
||||
WristpadHackData {
|
||||
name: "Superdork",
|
||||
buff: vec![
|
||||
BuffImpact::ChangeStat {
|
||||
stat: StatType::Brains,
|
||||
magnitude: 3.0,
|
||||
},
|
||||
BuffImpact::ChangeStat {
|
||||
stat: StatType::Cool,
|
||||
magnitude: -1.0,
|
||||
},
|
||||
],
|
||||
},
|
||||
)]
|
||||
.into_iter()
|
||||
.collect()
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(default)]
|
||||
pub struct User {
|
||||
@ -38,11 +78,11 @@ pub struct User {
|
||||
pub banned_until: Option<DateTime<Utc>>,
|
||||
pub abandoned_at: Option<DateTime<Utc>>,
|
||||
pub chargen_last_completed_at: Option<DateTime<Utc>>,
|
||||
|
||||
pub terms: UserTermData,
|
||||
pub experience: UserExperienceData,
|
||||
pub raw_skills: BTreeMap<SkillType, f64>,
|
||||
pub raw_stats: BTreeMap<StatType, f64>,
|
||||
pub wristpad_hacks: Vec<WristpadHack>,
|
||||
pub last_skill_improve: BTreeMap<SkillType, DateTime<Utc>>,
|
||||
pub last_page_from: Option<String>,
|
||||
pub credits: u64,
|
||||
@ -51,6 +91,93 @@ pub struct User {
|
||||
// Reminder: Consider backwards compatibility when updating this.
|
||||
}
|
||||
|
||||
static NO_BUFFS: Vec<BuffImpact> = vec![];
|
||||
|
||||
pub fn xp_to_hack_slots(xp: u64) -> u64 {
|
||||
if xp >= 1000000 {
|
||||
14
|
||||
} else if xp >= 500000 {
|
||||
13
|
||||
} else if xp >= 100000 {
|
||||
12
|
||||
} else if xp >= 70000 {
|
||||
11
|
||||
} else if xp >= 40000 {
|
||||
10
|
||||
} else if xp >= 20000 {
|
||||
9
|
||||
} else if xp >= 10000 {
|
||||
8
|
||||
} else if xp >= 8000 {
|
||||
7
|
||||
} else if xp >= 6000 {
|
||||
6
|
||||
} else if xp >= 4000 {
|
||||
5
|
||||
} else if xp >= 2000 {
|
||||
4
|
||||
} else if xp >= 1500 {
|
||||
3
|
||||
} else if xp >= 1000 {
|
||||
2
|
||||
} else if xp >= 500 {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn wristpad_hack_buffs<'a>(self: &'a Self) -> impl Iterator<Item = &'a BuffImpact> + 'a {
|
||||
self.wristpad_hacks.iter().flat_map(|h| {
|
||||
wristpad_hack_data()
|
||||
.get(h)
|
||||
.map(|hd| &hd.buff)
|
||||
.unwrap_or_else(|| &NO_BUFFS)
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn adjust_xp_for_reroll(
|
||||
self: &mut User,
|
||||
item: &mut Item,
|
||||
change: i64,
|
||||
trans: &DBTrans,
|
||||
sess: &ListenerSession,
|
||||
) -> DResult<()> {
|
||||
self.experience.xp_change_for_this_reroll += change;
|
||||
self.xp_adjusted(item, change, trans, sess).await
|
||||
}
|
||||
|
||||
pub async fn xp_adjusted(
|
||||
self: &mut User,
|
||||
item: &mut Item,
|
||||
change: i64,
|
||||
trans: &DBTrans,
|
||||
sess: &ListenerSession,
|
||||
) -> DResult<()> {
|
||||
let old_slots = xp_to_hack_slots(item.total_xp);
|
||||
if change >= 0 {
|
||||
item.total_xp += change as u64;
|
||||
} else if (-change) as u64 <= item.total_xp {
|
||||
item.total_xp -= (-change) as u64;
|
||||
} else {
|
||||
item.total_xp = 0;
|
||||
}
|
||||
let new_slots = xp_to_hack_slots(item.total_xp);
|
||||
|
||||
if new_slots > old_slots {
|
||||
trans
|
||||
.queue_for_session(
|
||||
sess,
|
||||
Some("You just earned a new hack slot on your wristpad!\n"),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for UserTermData {
|
||||
fn default() -> Self {
|
||||
UserTermData {
|
||||
@ -83,11 +210,11 @@ impl Default for User {
|
||||
banned_until: None,
|
||||
abandoned_at: None,
|
||||
chargen_last_completed_at: None,
|
||||
|
||||
terms: UserTermData::default(),
|
||||
experience: UserExperienceData::default(),
|
||||
raw_skills: BTreeMap::new(),
|
||||
raw_stats: BTreeMap::new(),
|
||||
wristpad_hacks: vec![],
|
||||
last_skill_improve: BTreeMap::new(),
|
||||
last_page_from: None,
|
||||
credits: 500,
|
||||
|
@ -418,11 +418,11 @@ pub async fn consider_reward_for(
|
||||
0
|
||||
} else {
|
||||
let xp_gain = (((for_item.total_xp - by_item.total_xp) as f64 * 10.0
|
||||
/ (by_item.total_xp + 1) as f64) as u64)
|
||||
/ (by_item.total_xp + 1) as f64) as i64)
|
||||
.min(100);
|
||||
|
||||
by_item.total_xp += xp_gain;
|
||||
user.experience.xp_change_for_this_reroll += xp_gain as i64;
|
||||
user.adjust_xp_for_reroll(by_item, xp_gain, trans, &session)
|
||||
.await?;
|
||||
xp_gain
|
||||
};
|
||||
|
||||
|
@ -28,22 +28,29 @@ pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User
|
||||
);
|
||||
}
|
||||
// 2: Apply stat (de)buffs...
|
||||
for buff in &target_item.temporary_buffs {
|
||||
for impact in &buff.impacts {
|
||||
match impact {
|
||||
BuffImpact::ChangeStat { stat, magnitude } => {
|
||||
target_item
|
||||
.total_stats
|
||||
.entry(stat.clone())
|
||||
.and_modify(|old_value| {
|
||||
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
||||
})
|
||||
.or_insert((*magnitude).max(0.0));
|
||||
}
|
||||
_ => {}
|
||||
for impact in target_item
|
||||
.temporary_buffs
|
||||
.iter()
|
||||
.flat_map(|buff| &buff.impacts)
|
||||
.chain(user.wristpad_hack_buffs())
|
||||
{
|
||||
match impact {
|
||||
BuffImpact::ChangeStat {
|
||||
ref stat,
|
||||
ref magnitude,
|
||||
} => {
|
||||
target_item
|
||||
.total_stats
|
||||
.entry(stat.clone())
|
||||
.and_modify(|old_value| {
|
||||
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
||||
})
|
||||
.or_insert((*magnitude).max(0.0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// 3: Total skills = raw skills
|
||||
target_item.total_skills = BTreeMap::new();
|
||||
for skill_type in SkillType::values() {
|
||||
@ -330,20 +337,23 @@ pub fn calculate_total_stats_skills_for_user(target_item: &mut Item, user: &User
|
||||
.and_modify(|sk| *sk += refl * 0.5)
|
||||
.or_insert(refl * 0.5);
|
||||
// 5: Apply skill (de)buffs...
|
||||
for buff in &target_item.temporary_buffs {
|
||||
for impact in &buff.impacts {
|
||||
match impact {
|
||||
BuffImpact::ChangeSkill { skill, magnitude } => {
|
||||
target_item
|
||||
.total_skills
|
||||
.entry(skill.clone())
|
||||
.and_modify(|old_value| {
|
||||
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
||||
})
|
||||
.or_insert((*magnitude).max(0.0));
|
||||
}
|
||||
_ => {}
|
||||
for impact in target_item
|
||||
.temporary_buffs
|
||||
.iter()
|
||||
.flat_map(|buff| &buff.impacts)
|
||||
.chain(user.wristpad_hack_buffs())
|
||||
{
|
||||
match impact {
|
||||
BuffImpact::ChangeSkill { skill, magnitude } => {
|
||||
target_item
|
||||
.total_skills
|
||||
.entry(skill.clone())
|
||||
.and_modify(|old_value| {
|
||||
*old_value = (*old_value + magnitude.clone() as f64).max(0.0)
|
||||
})
|
||||
.or_insert((*magnitude).max(0.0));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -132,9 +132,6 @@ pub async fn award_journal_if_needed(
|
||||
}
|
||||
Some(v) => v,
|
||||
};
|
||||
user.experience.journals.completed_journals.insert(journal);
|
||||
// Note: Not counted as 'change for this reroll' since it is permanent.
|
||||
player.total_xp += journal_data.xp;
|
||||
if let Some((sess, _)) = trans.find_session_for_player(&player.item_code).await? {
|
||||
trans
|
||||
.queue_for_session(
|
||||
@ -145,6 +142,10 @@ pub async fn award_journal_if_needed(
|
||||
)),
|
||||
)
|
||||
.await?;
|
||||
user.experience.journals.completed_journals.insert(journal);
|
||||
// Note: Not counted as 'change for this reroll' since it is permanent.
|
||||
user.xp_adjusted(player, journal_data.xp as i64, trans, &sess)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(true)
|
||||
|
@ -23,7 +23,7 @@ use mockall_double::double;
|
||||
use nom::{
|
||||
bytes::complete::tag,
|
||||
character::complete::{multispace1, u8},
|
||||
combinator::eof,
|
||||
combinator::{eof, opt},
|
||||
sequence::{delimited, pair, preceded, terminated},
|
||||
};
|
||||
use std::time;
|
||||
@ -44,7 +44,13 @@ fn parse_move_message(mut input: &str) -> Result<(u8, u8), &str> {
|
||||
input = input.trim();
|
||||
let (from, to) = match terminated(
|
||||
preceded(
|
||||
preceded(tag("move"), multispace1::<&str, ()>),
|
||||
preceded(
|
||||
tag("move"),
|
||||
preceded(
|
||||
multispace1::<&str, ()>,
|
||||
opt(preceded(tag("from"), multispace1::<&str, ()>)),
|
||||
),
|
||||
),
|
||||
pair(
|
||||
u8,
|
||||
preceded(delimited(multispace1, tag("to"), multispace1), u8),
|
||||
@ -131,6 +137,11 @@ mod test {
|
||||
assert_eq!(parse_move_message(" move 1 to 2"), Ok((1, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_move_message_accepts_valid_with_from() {
|
||||
assert_eq!(parse_move_message(" move from 1 to 2"), Ok((1, 2)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_move_message_rejects_badstart() {
|
||||
assert_eq!(parse_move_message("eat 1 to 2"), Err("Invalid command, feeble human. I only understand commands like: -doorbot move 1 to 2"));
|
||||
|
@ -34,7 +34,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
wear_data: Some(WearData {
|
||||
covers_parts: vec!(BodyPart::Groin, BodyPart::Legs),
|
||||
thickness: 4.0,
|
||||
dodge_penalty: 0.5,
|
||||
dodge_penalty: 0.25,
|
||||
soaks: vec!(
|
||||
(DamageType::Beat,
|
||||
SoakData {
|
||||
|
@ -37,7 +37,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
|
||||
BodyPart::Chest,
|
||||
BodyPart::Back),
|
||||
thickness: 4.0,
|
||||
dodge_penalty: 0.5,
|
||||
dodge_penalty: 0.3,
|
||||
soaks: vec!(
|
||||
(DamageType::Beat,
|
||||
SoakData {
|
||||
|
@ -3,7 +3,10 @@ use super::{possession_type::PossessionType, StaticItem};
|
||||
use crate::db::DBTrans;
|
||||
use crate::{
|
||||
message_handler::user_commands::UResult,
|
||||
models::item::{DoorState, Item, ItemFlag},
|
||||
models::{
|
||||
item::{DoorState, Item, ItemFlag},
|
||||
user::WristpadHack,
|
||||
},
|
||||
regular_tasks::queued_command::QueuedCommandContext,
|
||||
DResult,
|
||||
};
|
||||
@ -335,6 +338,7 @@ pub struct Room {
|
||||
pub material_type: MaterialType,
|
||||
pub has_power: bool,
|
||||
pub door_states: Option<BTreeMap<Direction, DoorState>>,
|
||||
pub wristpad_hack_allowed: Option<WristpadHack>,
|
||||
}
|
||||
|
||||
impl Default for Room {
|
||||
@ -357,6 +361,7 @@ impl Default for Room {
|
||||
material_type: MaterialType::Normal,
|
||||
has_power: false,
|
||||
door_states: None,
|
||||
wristpad_hack_allowed: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
use crate::{
|
||||
models::item::{DoorState, LocationActionType},
|
||||
models::{
|
||||
item::{DoorState, LocationActionType},
|
||||
user::WristpadHack,
|
||||
},
|
||||
static_content::{
|
||||
fixed_item::FixedItem,
|
||||
possession_type::{possession_data, PossessionData, PossessionType},
|
||||
@ -164,7 +167,7 @@ pub fn room_list() -> Vec<Room> {
|
||||
code: "computer_museum_hackers_club",
|
||||
name: "Hackers' Club",
|
||||
short: ansi!("<bgblack><green>HC<reset>"),
|
||||
description: ansi!("A room full of beeping and whirring equipment. One shiny stainless steel piece of equipment really catches your eye. It has a large plaque on it saying: Wristpad hacking unit - intelligence upgrade program"),
|
||||
description: ansi!("A room full of beeping and whirring equipment. One shiny stainless steel piece of equipment really catches your eye. It has a large plaque on it saying: Wristpad hacking unit - intelligence upgrade program. [You realise you can hack your wristpad here, if you have a free wristpad hack slot, to make yourself a superdork; it will increase your brains by 3, but decrease your cool by 1. To do it, type <bold>hack superdork<reset>]"),
|
||||
description_less_explicit: None,
|
||||
grid_coords: GridCoords { x: 4, y: -1, z: -1 },
|
||||
exits: vec!(
|
||||
@ -183,6 +186,7 @@ pub fn room_list() -> Vec<Room> {
|
||||
)
|
||||
].into_iter().collect()),
|
||||
should_caption: true,
|
||||
wristpad_hack_allowed: Some(WristpadHack::Superdork),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
|
Loading…
Reference in New Issue
Block a user