From 92d7b22921b735ea4b797246600b9574c55b1288 Mon Sep 17 00:00:00 2001 From: Condorra Date: Sat, 23 Sep 2023 23:55:29 +1000 Subject: [PATCH] Add wristpad hack concept - enhancements to characters. And add one hidden in the computer museum. --- .../src/message_handler/user_commands.rs | 3 +- .../src/message_handler/user_commands/hack.rs | 96 +++++++++++++ .../message_handler/user_commands/score.rs | 37 ++++- blastmud_game/src/models/user.rs | 133 +++++++++++++++++- blastmud_game/src/services/combat.rs | 6 +- blastmud_game/src/services/skills.rs | 62 ++++---- blastmud_game/src/static_content/journals.rs | 7 +- .../npc/computer_museum_npcs.rs | 15 +- .../possession_type/lower_armour.rs | 2 +- .../possession_type/torso_armour.rs | 2 +- blastmud_game/src/static_content/room.rs | 7 +- .../static_content/room/computer_museum.rs | 8 +- 12 files changed, 334 insertions(+), 44 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/hack.rs diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 4827aca0..d206c11d 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -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, diff --git a/blastmud_game/src/message_handler/user_commands/hack.rs b/blastmud_game/src/message_handler/user_commands/hack.rs new file mode 100644 index 00000000..7677ec78 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/hack.rs @@ -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 hack {}"), + &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; diff --git a/blastmud_game/src/message_handler/user_commands/score.rs b/blastmud_game/src/message_handler/user_commands/score.rs index 69eec71a..8dd87791 100644 --- a/blastmud_game/src/message_handler/user_commands/score.rs +++ b/blastmud_game/src/message_handler/user_commands/score.rs @@ -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::>(); + 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(()) diff --git a/blastmud_game/src/models/user.rs b/blastmud_game/src/models/user.rs index 29215e32..222e456e 100644 --- a/blastmud_game/src/models/user.rs +++ b/blastmud_game/src/models/user.rs @@ -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, +} + +pub fn wristpad_hack_data() -> &'static BTreeMap { + static D: OnceCell> = 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>, pub abandoned_at: Option>, pub chargen_last_completed_at: Option>, - pub terms: UserTermData, pub experience: UserExperienceData, pub raw_skills: BTreeMap, pub raw_stats: BTreeMap, + pub wristpad_hacks: Vec, pub last_skill_improve: BTreeMap>, pub last_page_from: Option, pub credits: u64, @@ -51,6 +91,93 @@ pub struct User { // Reminder: Consider backwards compatibility when updating this. } +static NO_BUFFS: Vec = 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 + '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, diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 06fd4201..1e1a1946 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -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 }; diff --git a/blastmud_game/src/services/skills.rs b/blastmud_game/src/services/skills.rs index 1e609085..e7677956 100644 --- a/blastmud_game/src/services/skills.rs +++ b/blastmud_game/src/services/skills.rs @@ -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)); } + _ => {} } } } diff --git a/blastmud_game/src/static_content/journals.rs b/blastmud_game/src/static_content/journals.rs index 2ccceedd..100795a8 100644 --- a/blastmud_game/src/static_content/journals.rs +++ b/blastmud_game/src/static_content/journals.rs @@ -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) diff --git a/blastmud_game/src/static_content/npc/computer_museum_npcs.rs b/blastmud_game/src/static_content/npc/computer_museum_npcs.rs index a27086dd..45cd1f47 100644 --- a/blastmud_game/src/static_content/npc/computer_museum_npcs.rs +++ b/blastmud_game/src/static_content/npc/computer_museum_npcs.rs @@ -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")); diff --git a/blastmud_game/src/static_content/possession_type/lower_armour.rs b/blastmud_game/src/static_content/possession_type/lower_armour.rs index 0838b624..ef7c9cf6 100644 --- a/blastmud_game/src/static_content/possession_type/lower_armour.rs +++ b/blastmud_game/src/static_content/possession_type/lower_armour.rs @@ -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 { diff --git a/blastmud_game/src/static_content/possession_type/torso_armour.rs b/blastmud_game/src/static_content/possession_type/torso_armour.rs index ca65e6c8..a6ddaf39 100644 --- a/blastmud_game/src/static_content/possession_type/torso_armour.rs +++ b/blastmud_game/src/static_content/possession_type/torso_armour.rs @@ -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 { diff --git a/blastmud_game/src/static_content/room.rs b/blastmud_game/src/static_content/room.rs index 5037b8d2..652270b5 100644 --- a/blastmud_game/src/static_content/room.rs +++ b/blastmud_game/src/static_content/room.rs @@ -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>, + pub wristpad_hack_allowed: Option, } 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, } } } diff --git a/blastmud_game/src/static_content/room/computer_museum.rs b/blastmud_game/src/static_content/room/computer_museum.rs index 504dacfc..0bd982e3 100644 --- a/blastmud_game/src/static_content/room/computer_museum.rs +++ b/blastmud_game/src/static_content/room/computer_museum.rs @@ -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 { code: "computer_museum_hackers_club", name: "Hackers' Club", short: ansi!("HC"), - 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 hack superdork]"), description_less_explicit: None, grid_coords: GridCoords { x: 4, y: -1, z: -1 }, exits: vec!( @@ -183,6 +186,7 @@ pub fn room_list() -> Vec { ) ].into_iter().collect()), should_caption: true, + wristpad_hack_allowed: Some(WristpadHack::Superdork), ..Default::default() }, )