From aa4828469ae7a65c15d6ff28c9c7f51f1f0f93c6 Mon Sep 17 00:00:00 2001 From: Condorra Date: Wed, 18 Oct 2023 22:25:08 +1100 Subject: [PATCH] Allow for occassional 'power' attacks They do more damage but take longer. --- .../src/message_handler/user_commands.rs | 6 + .../src/message_handler/user_commands/pow.rs | 25 +++ blastmud_game/src/models/item.rs | 165 +++++++++++------- blastmud_game/src/services/combat.rs | 128 ++++++++++++-- .../src/static_content/possession_type.rs | 22 +++ .../static_content/possession_type/whip.rs | 50 ++++++ 6 files changed, 319 insertions(+), 77 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/pow.rs diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 5fdd3b79..261809de 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -55,6 +55,7 @@ pub mod open; mod page; pub mod parsing; pub mod pay; +mod pow; pub mod put; mod quit; pub mod recline; @@ -217,6 +218,11 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "reply" => page::VERB, "pay" => pay::VERB, + + "pow" => pow::VERB, + "power" => pow::VERB, + "powerattack" => pow::VERB, + "put" => put::VERB, "recline" => recline::VERB, "remove" => remove::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/pow.rs b/blastmud_game/src/message_handler/user_commands/pow.rs new file mode 100644 index 00000000..f4238508 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/pow.rs @@ -0,0 +1,25 @@ +use crate::services::combat::switch_to_power_attack; + +use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext}; +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?; + if player_item.death_data.is_some() { + user_error("Power attack while dead? You can't even do a regular attack.".to_owned())?; + } + switch_to_power_attack(ctx, &player_item).await?; + + Ok(()) + } +} +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 428c98c3..1b6f8432 100644 --- a/blastmud_game/src/models/item.rs +++ b/blastmud_game/src/models/item.rs @@ -319,11 +319,25 @@ pub enum ItemFlag { DontListInLook, } +#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] +pub enum AttackMode { + NORMAL, + POWER, + FEINT, +} + +impl Default for AttackMode { + fn default() -> Self { + AttackMode::NORMAL + } +} + #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] #[serde(default)] pub struct ActiveCombat { pub attacking: Option, pub attacked_by: Vec, + pub attack_mode: AttackMode, } impl Default for ActiveCombat { @@ -331,6 +345,7 @@ impl Default for ActiveCombat { Self { attacking: None, attacked_by: vec![], + attack_mode: Default::default(), } } } @@ -456,46 +471,63 @@ pub struct FollowData { pub state: FollowState, } +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] +#[serde(default)] +pub struct TacticUse { + pub last_pow: Option>, + pub last_feint: Option>, +} + +impl Default for TacticUse { + fn default() -> Self { + Self { + last_pow: None, + last_feint: None, + } + } +} + #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, PartialOrd)] #[serde(default)] pub struct Item { - pub item_code: String, - pub item_type: String, - pub possession_type: Option, - pub display: String, - pub display_less_explicit: Option, - pub details: Option, - pub details_less_explicit: Option, - pub details_dyn_suffix: Option, - pub aliases: Vec, - pub location: String, // Item reference as item_type/item_code. pub action_type: LocationActionType, pub action_type_started: Option>, - pub presence_target: Option, // e.g. what are they sitting on. - pub is_static: bool, - pub death_data: Option, - pub species: SpeciesType, - pub health: u64, - pub total_xp: u64, - pub total_stats: BTreeMap, - pub total_skills: BTreeMap, - pub temporary_buffs: Vec, - pub pronouns: Pronouns, - pub flags: Vec, - pub sex: Option, - pub active_combat: Option, pub active_climb: Option, - pub weight: u64, - pub charges: u8, - pub special_data: Option, - pub dynamic_entrance: Option, - pub owner: Option, - pub door_states: Option>, - pub following: Option, - pub queue: VecDeque, - pub urges: Option, - pub liquid_details: Option, + pub active_combat: Option, pub active_effects: Vec<(EffectType, i64)>, + pub aliases: Vec, + pub charges: u8, + pub death_data: Option, + pub details: Option, + pub details_dyn_suffix: Option, + pub details_less_explicit: Option, + pub display: String, + pub display_less_explicit: Option, + pub door_states: Option>, + pub dynamic_entrance: Option, + pub flags: Vec, + pub following: Option, + pub health: u64, + pub is_static: bool, + pub item_code: String, + pub item_type: String, + pub liquid_details: Option, + pub location: String, // Item reference as item_type/item_code. + pub owner: Option, + pub possession_type: Option, + pub presence_target: Option, // e.g. what are they sitting on. + pub pronouns: Pronouns, + pub queue: VecDeque, + pub sex: Option, + pub special_data: Option, + pub species: SpeciesType, + pub tactic_use: TacticUse, + pub temporary_buffs: Vec, + pub total_skills: BTreeMap, + pub total_stats: BTreeMap, + pub total_xp: u64, + pub urges: Option, + pub weight: u64, } impl Item { @@ -583,43 +615,44 @@ impl Item { impl Default for Item { fn default() -> Self { Self { - item_code: "unset".to_owned(), - item_type: "unset".to_owned(), - possession_type: None, - display: "Item".to_owned(), - display_less_explicit: None, - details: None, - details_less_explicit: None, - details_dyn_suffix: None, - aliases: vec![], - location: "room/storage".to_owned(), action_type: LocationActionType::Normal, action_type_started: None, - presence_target: None, - is_static: false, - death_data: None, - species: SpeciesType::Human, - health: 24, - total_xp: 0, - total_stats: BTreeMap::new(), - total_skills: BTreeMap::new(), - temporary_buffs: Vec::new(), - pronouns: Pronouns::default_inanimate(), - flags: vec![], - sex: None, - active_combat: Some(Default::default()), active_climb: None, - weight: 0, - charges: 0, - special_data: None, - dynamic_entrance: None, - owner: None, - door_states: None, - following: None, - queue: VecDeque::new(), - urges: None, - liquid_details: None, + active_combat: Some(Default::default()), active_effects: vec![], + aliases: vec![], + charges: 0, + death_data: None, + details: None, + details_dyn_suffix: None, + details_less_explicit: None, + display: "Item".to_owned(), + display_less_explicit: None, + door_states: None, + dynamic_entrance: None, + flags: vec![], + following: None, + health: 24, + is_static: false, + item_code: "unset".to_owned(), + item_type: "unset".to_owned(), + liquid_details: None, + location: "room/storage".to_owned(), + owner: None, + possession_type: None, + presence_target: None, + pronouns: Pronouns::default_inanimate(), + queue: VecDeque::new(), + sex: None, + special_data: None, + species: SpeciesType::Human, + tactic_use: Default::default(), + temporary_buffs: Vec::new(), + total_skills: BTreeMap::new(), + total_stats: BTreeMap::new(), + total_xp: 0, + urges: None, + weight: 0, } } } diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 34348f07..e642eb8f 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -3,11 +3,11 @@ use crate::db::DBTrans; use crate::{ message_handler::user_commands::{ follow::cancel_follow_by_leader, stand::stand_if_needed, user_error, CommandHandlingError, - UResult, + UResult, VerbContext, }, models::{ corp::CorpCommType, - item::{DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack}, + item::{AttackMode, DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack}, journal::JournalType, task::{Task, TaskDetails, TaskMeta}, }, @@ -31,7 +31,7 @@ use crate::{ use ansi::ansi; use async_recursion::async_recursion; use async_trait::async_trait; -use chrono::Utc; +use chrono::{Duration, Utc}; use mockall_double::double; use rand::{prelude::IteratorRandom, Rng}; use rand_distr::{Distribution, Normal}; @@ -221,6 +221,10 @@ async fn process_attack( Some(&msg_nonexp), ) .await?; + match attacker_item.active_combat.as_mut() { + Some(ac) => ac.attack_mode = AttackMode::NORMAL, + None => {} + } ctx.trans.save_item_model(&attacker_item).await?; ctx.trans.save_item_model(&victim_item).await?; } else { @@ -244,6 +248,10 @@ async fn process_attack( .sample(&mut rand::thread_rng()) .floor() .max(1.0) as i64; + match attacker_item.active_combat.as_mut() { + Some(ac) => ac.attack_mode = AttackMode::NORMAL, + None => {} + } ctx.trans.save_item_model(&attacker_item).await?; let actual_damage = soak_damage( &ctx.trans, @@ -314,8 +322,14 @@ async fn process_attack( ctx.trans.save_item_model(victim_item).await?; } - let msg_exp = &(attack.start_message(&attacker_item, victim_item, true) + ".\n"); - let msg_nonexp = &(attack.start_message(&attacker_item, victim_item, false) + ".\n"); + let msg_exp = &(weapon + .normal_attack + .start_message(&attacker_item, victim_item, true) + + ".\n"); + let msg_nonexp = &(weapon + .normal_attack + .start_message(&attacker_item, victim_item, false) + + ".\n"); broadcast_to_room( ctx.trans, &attacker_item.location, @@ -373,7 +387,20 @@ impl TaskHandler for AttackTaskHandler { &mut attacker_item_mut, &weapon_it, &mut victim_item, - &weapon.normal_attack, + if attacker_item + .active_combat + .as_ref() + .map(|ac| ac.attack_mode == AttackMode::POWER) + .unwrap_or(false) + { + if let Some(pow) = weapon.power_attack.as_ref() { + pow + } else { + &weapon.normal_attack + } + } else { + &weapon.normal_attack + }, &weapon, ) .await?; @@ -747,8 +774,20 @@ async fn what_wielded( Ok((who.clone(), fist())) } -fn attack_speed(_who: &Item) -> time::Duration { - time::Duration::from_secs(5) +fn attack_speed(who: &Item) -> time::Duration { + let base_time = 5; + + let time_multiplier = who + .active_combat + .as_ref() + .map(|ac| match ac.attack_mode { + AttackMode::NORMAL => 1, + AttackMode::POWER => 2, + AttackMode::FEINT => 1, + }) + .unwrap_or(1); + + time::Duration::from_secs(base_time * time_multiplier) } #[async_recursion] @@ -828,10 +867,11 @@ pub async fn start_attack_mut( ) .await?; - by_whom + let ac = by_whom .active_combat - .get_or_insert_with(|| Default::default()) - .attacking = Some(format!("{}/{}", &to_whom.item_type, &to_whom.item_code)); + .get_or_insert_with(|| Default::default()); + ac.attacking = Some(format!("{}/{}", &to_whom.item_type, &to_whom.item_code)); + ac.attack_mode = AttackMode::NORMAL; by_whom.action_type = LocationActionType::Attacking(Subattack::Normal); to_whom .active_combat @@ -887,6 +927,72 @@ pub async fn corpsify_item(trans: &DBTrans, base_item: &Item) -> DResult { Ok(new_item) } +pub async fn switch_to_power_attack(ctx: &VerbContext<'_>, who: &Arc) -> UResult<()> { + let (wield_it, wielded) = what_wielded(&ctx.trans, who).await?; + let pow_att = match wielded.power_attack.as_ref() { + None => user_error(format!( + "{} is unsuitable for powerattacking", + wield_it.display_for_sentence(!ctx.session_dat.less_explicit_mode, 1, true) + ))?, + Some(v) => v, + }; + let (attacking_type, attacking_code) = match who + .active_combat + .as_ref() + .and_then(|ac| ac.attacking.as_ref()) + .and_then(|a| a.split_once("/")) + { + None => user_error("Calm down satan, you're not currently attacking anyone!".to_owned())?, + Some(v) => v, + }; + + let pow_delay: i64 = 30; + match who + .tactic_use + .last_pow + .map(|lp| (lp + Duration::seconds(pow_delay)) - Utc::now()) + { + None => {} + Some(d) if d < Duration::seconds(0) => {} + Some(d) => user_error(format!( + "You can't powerattack again for another {} seconds.", + d.num_seconds() + ))?, + } + + let to_whom = match ctx + .trans + .find_item_by_type_code(attacking_type, attacking_code) + .await? + { + None => user_error("They seem to be gone!".to_owned())?, + Some(v) => v, + }; + let msg_exp = pow_att.start_message(who, &to_whom, true) + ".\n"; + let msg_nonexp = pow_att.start_message(who, &to_whom, false) + ".\n"; + broadcast_to_room(ctx.trans, &who.location, None, &msg_exp, Some(&msg_nonexp)).await?; + + let mut who_mut = (**who).clone(); + who_mut.active_combat.as_mut().map(|ac| { + ac.attack_mode = AttackMode::POWER; + }); + who_mut.tactic_use.last_pow = Some(Utc::now()); + ctx.trans.save_item_model(&who_mut).await?; + ctx.trans + .upsert_task(&Task { + meta: TaskMeta { + task_code: format!("{}/{}", &who.item_type, &who.item_code), + next_scheduled: Utc::now() + + chrono::Duration::milliseconds(attack_speed(&who_mut).as_millis() as i64), + ..Default::default() + }, + details: TaskDetails::AttackTick, + }) + .await?; + + Ok(()) +} + pub struct NPCRecloneTaskHandler; #[async_trait] impl TaskHandler for NPCRecloneTaskHandler { diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 5d5d9095..9a123c1d 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -135,6 +135,7 @@ pub struct WeaponData { pub raw_min_to_learn: f64, pub raw_max_to_learn: f64, pub normal_attack: WeaponAttackData, + pub power_attack: Option, } impl Default for WeaponData { @@ -144,6 +145,7 @@ impl Default for WeaponData { raw_min_to_learn: 0.0, raw_max_to_learn: 15.0, normal_attack: Default::default(), + power_attack: None, } } } @@ -496,6 +498,26 @@ pub fn fist() -> &'static WeaponData { })], ..Default::default() }, + power_attack: Some(WeaponAttackData { + start_messages: vec![Box::new(|attacker, victim, exp| { + format!( + "{} tenses for a power fist swings at {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + ) + })], + success_messages: vec![Box::new(|attacker, victim, part, exp| { + format!( + "{}'s fists smash with great force into {}'s {}", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + })], + mean_damage: 3.0, + stdev_damage: 4.0, + ..Default::default() + }), ..Default::default() }) } diff --git a/blastmud_game/src/static_content/possession_type/whip.rs b/blastmud_game/src/static_content/possession_type/whip.rs index 226ca86e..8d3963ff 100644 --- a/blastmud_game/src/static_content/possession_type/whip.rs +++ b/blastmud_game/src/static_content/possession_type/whip.rs @@ -39,6 +39,30 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { stdev_damage: 3.0, ..Default::default() }, + power_attack: Some(WeaponAttackData { + start_messages: vec!( + Box::new(|attacker, victim, exp| + format!("{} rears back {} antenna whip for a power attack on {}", + &attacker.display_for_sentence(exp, 1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(exp, 1, false), + ) + ) + ), + success_messages: vec!( + Box::new(|attacker, victim, part, exp| + format!("{}'s antenna whip hits {}'s {} with great force", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + ) + ), + mean_damage: 9.0, + stdev_damage: 6.0, + crit_effects: vec![(0.05, EffectType::Bleed)], + ..Default::default() + }), ..Default::default() }), ..Default::default() @@ -80,6 +104,32 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> { other_damage_types: vec!((0.25, DamageType::Slash)), ..Default::default() }, + power_attack: Some(WeaponAttackData { + start_messages: vec!( + Box::new(|attacker, victim, exp| + format!("{} rears back {} leather whip for a power strike on {}", + &attacker.display_for_sentence(exp, 1, true), + &attacker.pronouns.possessive, + &victim.display_for_sentence(exp, 1, false), + ) + ) + ), + success_messages: vec!( + Box::new(|attacker, victim, part, exp| + format!("{}'s leather whip hits {}'s {} with great force", + &attacker.display_for_sentence(exp, 1, true), + &victim.display_for_sentence(exp, 1, false), + &part.display(victim.sex.clone()) + ) + ) + ), + mean_damage: 12.0, + stdev_damage: 4.0, + base_damage_type: DamageType::Beat, + crit_effects: vec![(0.3, EffectType::Bleed)], + other_damage_types: vec!((0.25, DamageType::Slash)), + ..Default::default() + }), ..Default::default() }), ..Default::default()