Allow feinting during battle

This commit is contained in:
Condorra 2023-10-20 23:24:20 +11:00
parent aa4828469a
commit 76b2874077
8 changed files with 488 additions and 47 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ pub enum EffectType {
Ephemeral, // i.e. no enduring impact to show in status.
Bandages,
Bleed,
Stunned,
}
pub struct EffectSet {

View File

@ -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<DamageDist: DamageDistribution>(
trans: &DBTrans,
@ -128,6 +132,31 @@ pub async fn soak_damage<DamageDist: DamageDistribution>(
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<bool> {
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<boo
user.experience.xp_change_for_this_reroll -= lost_xp as i64;
player.temporary_buffs = vec![];
player.flags.push(ItemFlag::HasUrges);
for effect in &player.active_effects {
cancel_effect(trans, player, effect).await?;
}
player.active_effects = vec![];
calculate_total_stats_skills_for_user(player, &user);
recalculate_urge_growth(trans, player).await?;
@ -993,6 +1189,107 @@ pub async fn switch_to_power_attack(ctx: &VerbContext<'_>, who: &Arc<Item>) -> U
Ok(())
}
pub async fn switch_to_feint(ctx: &VerbContext<'_>, who: &Arc<Item>) -> 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
)
}
}

View File

@ -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<EffectType, EffectSet> {
static MAP: OnceCell<BTreeMap<EffectType, EffectSet>> = 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<EffectType, EffectSet> {
},
],
},
)]
.into_iter()
.collect()
EffectSet {
effect_type: EffectType::Stunned,
effects: vec![
Effect::BroadcastMessage {
delay_secs: 0,
messagef: Box::new(|_player, _item, target| (
format!(ansi!("<blue>{} is stunned!<reset>\n"),
target.display_for_sentence(true, 1, true),
),
format!(ansi!("<blue>{} is stunned!<reset>\n"),
target.display_for_sentence(false, 1, true))
))
},
Effect::BroadcastMessage {
delay_secs: 30,
messagef: Box::new(|_player, _item, target| (
format!(ansi!("<blue>{} seems to have returned to {} senses!<reset>\n"),
target.display_for_sentence(true, 1, true),
&target.pronouns.object,
),
format!(ansi!("<blue>{} seems to have returned to {} senses!<reset>\n"),
target.display_for_sentence(false, 1, true),
&target.pronouns.object,
)
))
},
]
}
]
.into_iter()
.map(|et| (et.effect_type.clone(), et))
.collect()
})
}