From 76b2874077ec716f2b689ffae57fffecf59efdfd Mon Sep 17 00:00:00 2001 From: Condorra Date: Fri, 20 Oct 2023 23:24:20 +1100 Subject: [PATCH] Allow feinting during battle --- .../src/message_handler/user_commands.rs | 2 + .../message_handler/user_commands/attack.rs | 9 +- .../message_handler/user_commands/feint.rs | 37 ++ .../message_handler/user_commands/movement.rs | 11 + .../src/message_handler/user_commands/pow.rs | 10 +- blastmud_game/src/models/effect.rs | 1 + blastmud_game/src/services/combat.rs | 392 ++++++++++++++++-- blastmud_game/src/services/effect.rs | 73 +++- 8 files changed, 488 insertions(+), 47 deletions(-) create mode 100644 blastmud_game/src/message_handler/user_commands/feint.rs diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 261809d..3f39b3a 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -31,6 +31,7 @@ mod describe; pub mod drink; pub mod drop; pub mod eat; +mod feint; pub mod fill; mod fire; pub mod follow; @@ -197,6 +198,7 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "look" => look::VERB, "read" => look::VERB, + "feint" => feint::VERB, "list" => list::VERB, "load" => load::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/attack.rs b/blastmud_game/src/message_handler/user_commands/attack.rs index 1a1567f..cd68cae 100644 --- a/blastmud_game/src/message_handler/user_commands/attack.rs +++ b/blastmud_game/src/message_handler/user_commands/attack.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{ db::ItemSearchParams, - models::{consent::ConsentType, item::ItemFlag}, + models::{consent::ConsentType, effect::EffectType, item::ItemFlag}, services::{check_consent, combat::start_attack}, }; use ansi::ansi; @@ -24,6 +24,13 @@ impl UserVerb for Verb { if player_item.death_data.is_some() { user_error("It doesn't really seem fair, but you realise you won't be able to attack anyone while you're dead!".to_string())?; } + if player_item + .active_effects + .iter() + .any(|v| v.0 == EffectType::Stunned) + { + user_error("You're too stunned to attack.".to_owned())?; + } let attack_whom = search_item_for_user( ctx, diff --git a/blastmud_game/src/message_handler/user_commands/feint.rs b/blastmud_game/src/message_handler/user_commands/feint.rs new file mode 100644 index 0000000..13a0e72 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/feint.rs @@ -0,0 +1,37 @@ +use crate::{models::effect::EffectType, services::combat::switch_to_feint}; + +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("Feint while dead? You can't even do a regular attack.".to_owned())?; + } + + if player_item + .active_effects + .iter() + .any(|v| v.0 == EffectType::Stunned) + { + user_error( + "You stay still like a stunned mullet, unable to gain the composure to feint." + .to_owned(), + )?; + } + + switch_to_feint(ctx, &player_item).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/movement.rs b/blastmud_game/src/message_handler/user_commands/movement.rs index 404a5c9..f653d4b 100644 --- a/blastmud_game/src/message_handler/user_commands/movement.rs +++ b/blastmud_game/src/message_handler/user_commands/movement.rs @@ -14,6 +14,7 @@ use crate::{ language, models::{ consent::ConsentType, + effect::EffectType, item::{ActiveClimb, DoorState, Item, ItemSpecialData, LocationActionType, SkillType}, }, regular_tasks::queued_command::{ @@ -354,6 +355,16 @@ async fn attempt_move_immediate( ctx.item.location.clone() }; + if ctx + .item + .active_effects + .iter() + .any(|v| v.0 == EffectType::Stunned) + && !ctx.item.death_data.is_some() + { + user_error("You're too stunned to move.".to_owned())?; + } + let session = ctx.get_session().await?; match is_door_in_direction(ctx.trans, direction, &use_location).await? { diff --git a/blastmud_game/src/message_handler/user_commands/pow.rs b/blastmud_game/src/message_handler/user_commands/pow.rs index f423850..d39fb30 100644 --- a/blastmud_game/src/message_handler/user_commands/pow.rs +++ b/blastmud_game/src/message_handler/user_commands/pow.rs @@ -1,4 +1,4 @@ -use crate::services::combat::switch_to_power_attack; +use crate::{models::effect::EffectType, services::combat::switch_to_power_attack}; use super::{get_player_item_or_fail, user_error, UResult, UserVerb, UserVerbRef, VerbContext}; use async_trait::async_trait; @@ -16,6 +16,14 @@ impl UserVerb for Verb { if player_item.death_data.is_some() { user_error("Power attack while dead? You can't even do a regular attack.".to_owned())?; } + if player_item + .active_effects + .iter() + .any(|v| v.0 == EffectType::Stunned) + { + user_error("You stay still like a stunned mullet, unable to gain the composure to powerattack.".to_owned())?; + } + switch_to_power_attack(ctx, &player_item).await?; Ok(()) diff --git a/blastmud_game/src/models/effect.rs b/blastmud_game/src/models/effect.rs index bfcd20b..a382c90 100644 --- a/blastmud_game/src/models/effect.rs +++ b/blastmud_game/src/models/effect.rs @@ -6,6 +6,7 @@ pub enum EffectType { Ephemeral, // i.e. no enduring impact to show in status. Bandages, Bleed, + Stunned, } pub struct EffectSet { diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index e642eb8..8e68014 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -7,7 +7,11 @@ use crate::{ }, models::{ corp::CorpCommType, - item::{AttackMode, DeathData, Item, ItemFlag, LocationActionType, SkillType, Subattack}, + effect::EffectType, + item::{ + AttackMode, DeathData, Item, ItemFlag, LocationActionType, SkillType, StatType, + Subattack, + }, journal::JournalType, task::{Task, TaskDetails, TaskMeta}, }, @@ -24,7 +28,7 @@ use crate::{ possession_type::{ fist, possession_data, DamageDistribution, DamageType, WeaponAttackData, WeaponData, }, - species::{species_info_map, BodyPart}, + species::{species_info_map, BodyPart, SpeciesType}, }, DResult, }; @@ -33,11 +37,11 @@ use async_recursion::async_recursion; use async_trait::async_trait; use chrono::{Duration, Utc}; use mockall_double::double; -use rand::{prelude::IteratorRandom, Rng}; +use rand::{prelude::IteratorRandom, thread_rng, Rng}; use rand_distr::{Distribution, Normal}; use std::{sync::Arc, time}; -use super::effect::{default_effects_for_type, run_effects}; +use super::effect::{cancel_effect, default_effects_for_type, run_effects}; pub async fn soak_damage( trans: &DBTrans, @@ -128,6 +132,31 @@ pub async fn soak_damage( Ok(total_damage) } +async fn start_next_attack( + ctx: &mut TaskRunContext<'_>, + attacker_item: &Item, + victim_item: &Item, + weapon: &WeaponData, +) -> DResult<()> { + 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, + None, + msg_exp, + Some(msg_nonexp), + ) + .await?; + Ok(()) +} + async fn process_attack( ctx: &mut TaskRunContext<'_>, attacker_item: &mut Item, @@ -163,6 +192,14 @@ async fn process_attack( Some(&msg_nonexp), ) .await?; + + match attacker_item.active_combat.as_mut() { + Some(ac) if ac.attack_mode != AttackMode::NORMAL => { + ac.attack_mode = AttackMode::NORMAL; + ctx.trans.save_item_model(&attacker_item).await?; + } + _ => {} + } return Ok(false); } @@ -322,22 +359,154 @@ async fn process_attack( ctx.trans.save_item_model(victim_item).await?; } - 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, - None, - msg_exp, - Some(msg_nonexp), - ) - .await?; + start_next_attack(ctx, &attacker_item, victim_item, weapon).await?; + Ok(false) +} + +async fn process_feint( + ctx: &mut TaskRunContext<'_>, + attacker_item: &mut Item, + victim_item: &mut Item, +) -> DResult { + if attacker_item + .urges + .as_ref() + .map(|u| u.stress.value) + .unwrap_or(0) + > 8000 + { + let msg_exp = format!( + "{} looks like {} wanted to feint {}, but was too tired and stressed to do it.\n", + attacker_item.display_for_sentence(true, 1, true), + attacker_item.pronouns.subject, + victim_item.display_for_sentence(true, 1, false), + ); + let msg_nonexp = format!( + "{} looks like {} wanted to feint {}, but was too tired and stressed to do it.\n", + attacker_item.display_for_sentence(false, 1, true), + attacker_item.pronouns.subject, + victim_item.display_for_sentence(false, 1, false), + ); + broadcast_to_room( + ctx.trans, + &attacker_item.location, + None, + &msg_exp, + Some(&msg_nonexp), + ) + .await?; + match attacker_item.active_combat.as_mut() { + Some(ac) => { + ac.attack_mode = AttackMode::NORMAL; + ctx.trans.save_item_model(&attacker_item).await?; + } + _ => {} + } + return Ok(false); + } + + let attacker_brn = *attacker_item + .total_stats + .get(&StatType::Brains) + .unwrap_or(&8.0); + let victim_brn = *victim_item + .total_stats + .get(&StatType::Brains) + .unwrap_or(&8.0); + let fuzzed_diff = Normal::new(attacker_brn - victim_brn, 2.0)?.sample(&mut rand::thread_rng()); + + if fuzzed_diff <= -1.0 { + broadcast_to_room( + &ctx.trans, + &attacker_item.location, + None, + &format!( + "{} seems to have pulled off the feint so poorly {} confused {}!\n", + &attacker_item.display_for_sentence(true, 1, true), + &attacker_item.pronouns.object, + &attacker_item.pronouns.intensive, + ), + Some(&format!( + "{} seems to have pulled off the feint so poorly {} confused {}!\n", + &attacker_item.display_for_sentence(false, 1, true), + &attacker_item.pronouns.object, + &attacker_item.pronouns.intensive, + )), + ) + .await?; + run_effects( + &ctx.trans, + default_effects_for_type() + .get(&EffectType::Stunned) + .unwrap(), + attacker_item, + &attacker_item.clone(), + None, + 0.0, + ) + .await?; + } else if fuzzed_diff >= 1.0 + && !victim_item + .active_effects + .iter() + .any(|v| v.0 == EffectType::Stunned) + { + broadcast_to_room( + &ctx.trans, + &attacker_item.location, + None, + &format!( + "{} is confused by {}'s antics!\n", + &victim_item.display_for_sentence(true, 1, true), + &attacker_item.display_for_sentence(true, 1, false), + ), + Some(&format!( + "{} is confused by {}'s antics!\n", + &victim_item.display_for_sentence(false, 1, true), + &attacker_item.display_for_sentence(false, 1, false), + )), + ) + .await?; + run_effects( + &ctx.trans, + default_effects_for_type() + .get(&EffectType::Stunned) + .unwrap(), + victim_item, + attacker_item, + None, + 0.0, + ) + .await?; + } else { + broadcast_to_room( + &ctx.trans, + &attacker_item.location, + None, + &format!( + "{} doesn't seem to have fallen for {}'s unenlightened attempt at a feint.\n", + &victim_item.display_for_sentence(true, 1, true), + &attacker_item.display_for_sentence(true, 1, false) + ), + Some(&format!( + "{} doesn't seem to have fallen for {}'s unenlightened attempt at a feint.\n", + &victim_item.display_for_sentence(false, 1, true), + &attacker_item.display_for_sentence(false, 1, false) + )), + ) + .await?; + return Ok(false); + } + + match attacker_item.active_combat.as_mut() { + Some(ac) => { + ac.attack_mode = AttackMode::NORMAL; + } + _ => {} + } + ctx.trans.save_item_model(&attacker_item).await?; + ctx.trans.save_item_model(&victim_item).await?; + Ok(false) } @@ -382,28 +551,51 @@ impl TaskHandler for AttackTaskHandler { let (weapon_it, weapon) = what_wielded(ctx.trans, &attacker_item).await?; let mut attacker_item_mut = (*attacker_item).clone(); - process_attack( - ctx, - &mut attacker_item_mut, - &weapon_it, - &mut victim_item, - 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 + + if attacker_item_mut + .active_effects + .iter() + .any(|v| v.0 == EffectType::Stunned) + { + match attacker_item_mut.active_combat.as_mut() { + Some(ac) if ac.attack_mode != AttackMode::NORMAL => { + ac.attack_mode = AttackMode::NORMAL; + ctx.trans.save_item_model(&attacker_item_mut).await?; + } + _ => {} + } + return Ok(Some(attack_speed(&attacker_item_mut))); + } + + let mode = attacker_item + .active_combat + .as_ref() + .map(|ac| ac.attack_mode.clone()) + .unwrap_or(AttackMode::NORMAL); + + if mode == AttackMode::FEINT { + if process_feint(ctx, &mut attacker_item_mut, &mut victim_item).await? { + start_next_attack(ctx, &mut attacker_item_mut, &mut victim_item, &weapon).await?; + } + } else { + process_attack( + ctx, + &mut attacker_item_mut, + &weapon_it, + &mut victim_item, + if mode == AttackMode::POWER { + if let Some(pow) = weapon.power_attack.as_ref() { + pow + } else { + &weapon.normal_attack + } } else { &weapon.normal_attack - } - } else { - &weapon.normal_attack - }, - &weapon, - ) - .await?; + }, + &weapon, + ) + .await?; + } // We re-check this on the next tick, rather than going off if the victim // died. That prevents a bug when re-focusing where we re-schedule and then @@ -652,6 +844,10 @@ pub async fn handle_resurrect(trans: &DBTrans, player: &mut Item) -> DResult, who: &Arc) -> U Ok(()) } +pub async fn switch_to_feint(ctx: &VerbContext<'_>, who: &Arc) -> UResult<()> { + 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 feint_delay: i64 = 30; + match who + .tactic_use + .last_feint + .map(|lp| (lp + Duration::seconds(feint_delay)) - Utc::now()) + { + None => {} + Some(d) if d < Duration::seconds(0) => {} + Some(d) => user_error(format!( + "You can't feint 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, + }; + + if to_whom.species != SpeciesType::Human && to_whom.species != SpeciesType::Robot { + user_error(format!( + "You don't think {} will pay any attention to a feint.", + to_whom.display_for_session(&ctx.session_dat) + ))?; + } + + let feints = vec![ + |p: &str, v: &str| { + format!( + "{} flicks sand in {}'s eyes and shouts \"pocket sand\".\n", + &p, &v + ) + }, + |p: &str, v: &str| { + format!( + "{} waves a piece of string at {} and asks \"how long is a piece of string?\".\n", + &p, &v + ) + }, + |p: &str, v: &str| format!("{} points out the futility of violence to {}.\n", &p, &v), + |p: &str, v: &str| format!("{} references {}'s mother in a pejorative way.\n", &p, &v), + |p: &str, v: &str| { + format!( + "{} asks if {} suffers from pneumonoultramicroscopicsilicovolcanoconiosis.\n", + &p, &v + ) + }, + |p: &str, v: &str| { + format!( + "{} notes that consciousness is an illusion, so {} might as well not bother.\n", + &p, &v + ) + }, + ]; + let feint = feints.iter().choose(&mut thread_rng()).unwrap(); + let msg_exp = feint( + &who.display_for_sentence(true, 1, true), + &to_whom.display_for_sentence(true, 1, false), + ); + let msg_nonexp = feint( + &who.display_for_sentence(false, 1, true), + &to_whom.display_for_sentence(false, 1, false), + ); + 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::FEINT; + }); + who_mut.tactic_use.last_feint = 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 { @@ -1062,3 +1359,18 @@ impl TaskHandler for RotCorpseTaskHandler { } } pub static ROT_CORPSE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &RotCorpseTaskHandler; + +#[cfg(test)] +mod Tests { + use crate::{models::effect::EffectType, services::effect::default_effects_for_type}; + + #[test] + fn default_for_stunned() { + assert_eq!( + default_effects_for_type() + .get(&EffectType::Stunned) + .is_some(), + true + ) + } +} diff --git a/blastmud_game/src/services/effect.rs b/blastmud_game/src/services/effect.rs index 5f343da..848248d 100644 --- a/blastmud_game/src/services/effect.rs +++ b/blastmud_game/src/services/effect.rs @@ -332,11 +332,45 @@ pub async fn run_effects( Ok(()) } +pub async fn cancel_effect( + trans: &DBTrans, + character: &Item, + effect: &(EffectType, i64), +) -> DResult<()> { + trans + .delete_task( + "DelayedMessage", + &format!( + "{}/{}/{}", + &character.item_type, &character.item_code, effect.1 + ), + ) + .await?; + trans + .delete_task( + "DelayedHealth", + &format!( + "{}/{}/{}", + &character.item_type, &character.item_code, effect.1 + ), + ) + .await?; + trans + .delete_task( + "DispelEffect", + &format!( + "{}/{}/{}", + &character.item_type, &character.item_code, effect.1 + ), + ) + .await?; + Ok(()) +} + pub fn default_effects_for_type() -> &'static BTreeMap { static MAP: OnceCell> = OnceCell::new(); MAP.get_or_init(|| { - vec![( - EffectType::Bleed, + vec![ EffectSet { effect_type: EffectType::Bleed, effects: vec![ @@ -444,8 +478,37 @@ pub fn default_effects_for_type() -> &'static BTreeMap { }, ], }, - )] - .into_iter() - .collect() + EffectSet { + effect_type: EffectType::Stunned, + effects: vec![ + Effect::BroadcastMessage { + delay_secs: 0, + messagef: Box::new(|_player, _item, target| ( + format!(ansi!("{} is stunned!\n"), + target.display_for_sentence(true, 1, true), + ), + format!(ansi!("{} is stunned!\n"), + target.display_for_sentence(false, 1, true)) + )) + }, + Effect::BroadcastMessage { + delay_secs: 30, + messagef: Box::new(|_player, _item, target| ( + format!(ansi!("{} seems to have returned to {} senses!\n"), + target.display_for_sentence(true, 1, true), + &target.pronouns.object, + ), + format!(ansi!("{} seems to have returned to {} senses!\n"), + target.display_for_sentence(false, 1, true), + &target.pronouns.object, + ) + )) + }, + ] + } + ] + .into_iter() + .map(|et| (et.effect_type.clone(), et)) + .collect() }) }