Allow for occassional 'power' attacks

They do more damage but take longer.
This commit is contained in:
Condorra 2023-10-18 22:25:08 +11:00
parent 6ac3f676be
commit aa4828469a
6 changed files with 319 additions and 77 deletions

View File

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

View File

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

View File

@ -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<String>,
pub attacked_by: Vec<String>,
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<DateTime<Utc>>,
pub last_feint: Option<DateTime<Utc>>,
}
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<PossessionType>,
pub display: String,
pub display_less_explicit: Option<String>,
pub details: Option<String>,
pub details_less_explicit: Option<String>,
pub details_dyn_suffix: Option<String>,
pub aliases: Vec<String>,
pub location: String, // Item reference as item_type/item_code.
pub action_type: LocationActionType,
pub action_type_started: Option<DateTime<Utc>>,
pub presence_target: Option<String>, // e.g. what are they sitting on.
pub is_static: bool,
pub death_data: Option<DeathData>,
pub species: SpeciesType,
pub health: u64,
pub total_xp: u64,
pub total_stats: BTreeMap<StatType, f64>,
pub total_skills: BTreeMap<SkillType, f64>,
pub temporary_buffs: Vec<Buff>,
pub pronouns: Pronouns,
pub flags: Vec<ItemFlag>,
pub sex: Option<Sex>,
pub active_combat: Option<ActiveCombat>,
pub active_climb: Option<ActiveClimb>,
pub weight: u64,
pub charges: u8,
pub special_data: Option<ItemSpecialData>,
pub dynamic_entrance: Option<DynamicEntrance>,
pub owner: Option<String>,
pub door_states: Option<BTreeMap<Direction, DoorState>>,
pub following: Option<FollowData>,
pub queue: VecDeque<QueueCommand>,
pub urges: Option<Urges>,
pub liquid_details: Option<LiquidDetails>,
pub active_combat: Option<ActiveCombat>,
pub active_effects: Vec<(EffectType, i64)>,
pub aliases: Vec<String>,
pub charges: u8,
pub death_data: Option<DeathData>,
pub details: Option<String>,
pub details_dyn_suffix: Option<String>,
pub details_less_explicit: Option<String>,
pub display: String,
pub display_less_explicit: Option<String>,
pub door_states: Option<BTreeMap<Direction, DoorState>>,
pub dynamic_entrance: Option<DynamicEntrance>,
pub flags: Vec<ItemFlag>,
pub following: Option<FollowData>,
pub health: u64,
pub is_static: bool,
pub item_code: String,
pub item_type: String,
pub liquid_details: Option<LiquidDetails>,
pub location: String, // Item reference as item_type/item_code.
pub owner: Option<String>,
pub possession_type: Option<PossessionType>,
pub presence_target: Option<String>, // e.g. what are they sitting on.
pub pronouns: Pronouns,
pub queue: VecDeque<QueueCommand>,
pub sex: Option<Sex>,
pub special_data: Option<ItemSpecialData>,
pub species: SpeciesType,
pub tactic_use: TacticUse,
pub temporary_buffs: Vec<Buff>,
pub total_skills: BTreeMap<SkillType, f64>,
pub total_stats: BTreeMap<StatType, f64>,
pub total_xp: u64,
pub urges: Option<Urges>,
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,
}
}
}

View File

@ -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<Item> {
Ok(new_item)
}
pub async fn switch_to_power_attack(ctx: &VerbContext<'_>, who: &Arc<Item>) -> 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 {

View File

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

View File

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