Add wristpad hack concept - enhancements to characters.

And add one hidden in the computer museum.
This commit is contained in:
Condorra 2023-09-23 23:55:29 +10:00
parent 6dc4a870fc
commit 92d7b22921
12 changed files with 334 additions and 44 deletions

View File

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

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

View File

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

View File

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

View File

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

View File

@ -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));
}
_ => {}
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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