Allow for occassional 'power' attacks
They do more damage but take longer.
This commit is contained in:
parent
6ac3f676be
commit
aa4828469a
@ -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,
|
||||
|
25
blastmud_game/src/message_handler/user_commands/pow.rs
Normal file
25
blastmud_game/src/message_handler/user_commands/pow.rs
Normal 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;
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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()
|
||||
})
|
||||
}
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user