Implement use command for trauma kits.
This commit is contained in:
parent
70dae5b853
commit
385d2d1fd8
@ -30,6 +30,7 @@ pub mod parsing;
|
|||||||
mod quit;
|
mod quit;
|
||||||
mod register;
|
mod register;
|
||||||
pub mod say;
|
pub mod say;
|
||||||
|
pub mod use_cmd;
|
||||||
mod whisper;
|
mod whisper;
|
||||||
mod who;
|
mod who;
|
||||||
pub mod wield;
|
pub mod wield;
|
||||||
@ -126,6 +127,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! {
|
|||||||
|
|
||||||
"\'" => say::VERB,
|
"\'" => say::VERB,
|
||||||
"say" => say::VERB,
|
"say" => say::VERB,
|
||||||
|
|
||||||
|
"use" => use_cmd::VERB,
|
||||||
|
|
||||||
"-" => whisper::VERB,
|
"-" => whisper::VERB,
|
||||||
"whisper" => whisper::VERB,
|
"whisper" => whisper::VERB,
|
||||||
|
@ -77,6 +77,14 @@ pub fn parse_username(input: &str) -> Result<(&str, &str), &'static str> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn parse_on_or_default<'l>(input: &'l str, default_on: &'l str) -> (&'l str, &'l str) {
|
||||||
|
if let Some((a, b)) = input.split_once(" on ") {
|
||||||
|
(a, b)
|
||||||
|
} else {
|
||||||
|
(input, default_on)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
238
blastmud_game/src/message_handler/user_commands/use_cmd.rs
Normal file
238
blastmud_game/src/message_handler/user_commands/use_cmd.rs
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
use super::{
|
||||||
|
VerbContext,
|
||||||
|
UserVerb,
|
||||||
|
UserVerbRef,
|
||||||
|
UResult,
|
||||||
|
ItemSearchParams,
|
||||||
|
user_error,
|
||||||
|
get_player_item_or_fail,
|
||||||
|
search_item_for_user,
|
||||||
|
parsing,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
static_content::possession_type::{
|
||||||
|
possession_data,
|
||||||
|
},
|
||||||
|
regular_tasks::queued_command::{
|
||||||
|
QueueCommandHandler,
|
||||||
|
QueueCommand,
|
||||||
|
queue_command
|
||||||
|
},
|
||||||
|
models::item::{
|
||||||
|
SkillType,
|
||||||
|
},
|
||||||
|
services::{
|
||||||
|
broadcast_to_room,
|
||||||
|
skills::skill_check_and_grind,
|
||||||
|
effect::run_effects,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::time;
|
||||||
|
|
||||||
|
pub struct QueueHandler;
|
||||||
|
#[async_trait]
|
||||||
|
impl QueueCommandHandler for QueueHandler {
|
||||||
|
async fn start_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
|
||||||
|
-> UResult<time::Duration> {
|
||||||
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
|
if player_item.is_dead {
|
||||||
|
user_error("You try to use it, but your ghostly hands slip through it uselessly".to_owned())?;
|
||||||
|
}
|
||||||
|
let (item_id, target_type_code) = match command {
|
||||||
|
QueueCommand::Use { possession_id, target_id } => (possession_id, target_id),
|
||||||
|
_ => user_error("Unexpected command".to_owned())?
|
||||||
|
};
|
||||||
|
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? {
|
||||||
|
None => user_error("Item not found".to_owned())?,
|
||||||
|
Some(it) => it
|
||||||
|
};
|
||||||
|
if item.location != format!("player/{}", player_item.item_code) {
|
||||||
|
user_error(format!("You try to use {} but realise you no longer have it",
|
||||||
|
item.display_for_sentence(
|
||||||
|
!ctx.session_dat.less_explicit_mode,
|
||||||
|
1, false
|
||||||
|
)
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
let (target_type, target_code) = match target_type_code.split_once("/") {
|
||||||
|
None => user_error("Couldn't handle use command (invalid target)".to_owned())?,
|
||||||
|
Some(spl) => spl
|
||||||
|
};
|
||||||
|
let is_self_use = target_type == "player" && target_code == player_item.item_code;
|
||||||
|
let target =
|
||||||
|
if is_self_use {
|
||||||
|
player_item.clone()
|
||||||
|
} else {
|
||||||
|
match ctx.trans.find_item_by_type_code(&target_type, &target_code).await? {
|
||||||
|
None => user_error(format!("Couldn't handle use command (target {} missing)",
|
||||||
|
target_type_code))?,
|
||||||
|
Some(it) => it
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if !is_self_use && target.location != player_item.location &&
|
||||||
|
target.location != format!("player/{}", player_item.item_code) {
|
||||||
|
let target_name = target.display_for_sentence(!ctx.session_dat.less_explicit_mode,
|
||||||
|
1, false);
|
||||||
|
user_error(format!("You try to use {} on {}, but realise {} is no longer here",
|
||||||
|
item.display_for_sentence(
|
||||||
|
!ctx.session_dat.less_explicit_mode,
|
||||||
|
1, false),
|
||||||
|
target_name, target_name
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
let msg_exp = format!("{} prepares to use {} {} on {}\n",
|
||||||
|
&player_item.display_for_sentence(true, 1, true),
|
||||||
|
&player_item.pronouns.possessive,
|
||||||
|
&item.display_for_sentence(true, 1, false),
|
||||||
|
&if is_self_use { player_item.pronouns.intensive.clone() } else {
|
||||||
|
player_item.display_for_sentence(true, 1, false)
|
||||||
|
});
|
||||||
|
let msg_nonexp = format!("{} prepares to use {} {} on {}\n",
|
||||||
|
&player_item.display_for_sentence(false, 1, true),
|
||||||
|
&player_item.pronouns.possessive,
|
||||||
|
&item.display_for_sentence(false, 1, false),
|
||||||
|
&if is_self_use { player_item.pronouns.intensive.clone() } else {
|
||||||
|
player_item.display_for_sentence(true, 1, false)
|
||||||
|
});
|
||||||
|
broadcast_to_room(ctx.trans, &player_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||||
|
let mut draw_level: f64 = *player_item.total_skills.get(&SkillType::Quickdraw).to_owned().unwrap_or(&8.0);
|
||||||
|
let mut player_item_mut = (*player_item).clone();
|
||||||
|
|
||||||
|
let skill_result =
|
||||||
|
skill_check_and_grind(ctx.trans, &mut player_item_mut, &SkillType::Quickdraw, draw_level).await?;
|
||||||
|
if skill_result < -0.5 {
|
||||||
|
draw_level -= 2.0;
|
||||||
|
} else if skill_result < -0.25 {
|
||||||
|
draw_level -= 1.0;
|
||||||
|
} else if skill_result > 0.5 {
|
||||||
|
draw_level += 2.0;
|
||||||
|
} else if skill_result > 0.25 {
|
||||||
|
draw_level += 1.0;
|
||||||
|
}
|
||||||
|
ctx.trans.save_item_model(&player_item_mut).await?;
|
||||||
|
|
||||||
|
let wait_ticks = (12.0 - (draw_level / 2.0)).min(8.0).max(1.0);
|
||||||
|
Ok(time::Duration::from_millis((wait_ticks * 500.0).round() as u64))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unreachable_patterns)]
|
||||||
|
async fn finish_command(&self, ctx: &mut VerbContext<'_>, command: &QueueCommand)
|
||||||
|
-> UResult<()> {
|
||||||
|
let player_item = get_player_item_or_fail(ctx).await?;
|
||||||
|
if player_item.is_dead {
|
||||||
|
user_error("You try to use it, but your ghostly hands slip through it uselessly".to_owned())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (ref item_id, ref target_type_code) = match command {
|
||||||
|
QueueCommand::Use { possession_id, target_id } => (possession_id, target_id),
|
||||||
|
_ => user_error("Unexpected command".to_owned())?
|
||||||
|
};
|
||||||
|
let item = match ctx.trans.find_item_by_type_code("possession", &item_id).await? {
|
||||||
|
None => user_error("Item not found".to_owned())?,
|
||||||
|
Some(it) => it
|
||||||
|
};
|
||||||
|
if item.location != format!("player/{}", player_item.item_code) {
|
||||||
|
user_error(format!("You try to use {} but realise you no longer have it",
|
||||||
|
item.display_for_sentence(!ctx.session_dat.less_explicit_mode,
|
||||||
|
1, false)
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
let (ref target_type, ref target_code) = match target_type_code.split_once("/") {
|
||||||
|
None => user_error("Couldn't handle use command (invalid target)".to_owned())?,
|
||||||
|
Some(ref sp) => sp.clone()
|
||||||
|
};
|
||||||
|
let target = match ctx.trans.find_item_by_type_code(&target_type, &target_code).await? {
|
||||||
|
None => user_error("Couldn't handle use command (target missing)".to_owned())?,
|
||||||
|
Some(it) => it
|
||||||
|
};
|
||||||
|
if target.location != player_item.location &&
|
||||||
|
target.location != format!("player/{}", player_item.item_code) {
|
||||||
|
let target_name = target.display_for_sentence(!ctx.session_dat.less_explicit_mode,
|
||||||
|
1, false);
|
||||||
|
user_error(format!("You try to use {} on {}, but realise {} is no longer here",
|
||||||
|
item.display_for_sentence(
|
||||||
|
!ctx.session_dat.less_explicit_mode,
|
||||||
|
1, false),
|
||||||
|
target_name, target_name
|
||||||
|
))?
|
||||||
|
}
|
||||||
|
let use_data = match item.possession_type.as_ref()
|
||||||
|
.and_then(|poss_type| possession_data().get(&poss_type))
|
||||||
|
.and_then(|poss_data| poss_data.use_data.as_ref()) {
|
||||||
|
None => user_error("You can't use that!".to_owned())?,
|
||||||
|
Some(d) => d
|
||||||
|
};
|
||||||
|
if let Some(err) = (use_data.errorf)(&item, &target) {
|
||||||
|
user_error(err)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_self_use = target_type == &"player" && target_code == &player_item.item_code;
|
||||||
|
let mut player_mut = (*player_item).clone();
|
||||||
|
let skillcheck = skill_check_and_grind(&ctx.trans, &mut player_mut,
|
||||||
|
&use_data.uses_skill, use_data.diff_level).await?;
|
||||||
|
let (effects, skilllvl) = if skillcheck <= -0.5 {
|
||||||
|
// 0-1 how bad was the crit fail?
|
||||||
|
(&use_data.crit_fail_effects, (-0.5-skillcheck) * 2.0)
|
||||||
|
} else if skillcheck < 0.0 {
|
||||||
|
(&use_data.fail_effects, -skillcheck * 2.0)
|
||||||
|
} else {
|
||||||
|
(&use_data.success_effects, skillcheck)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut target_mut = if is_self_use { None } else { Some((*target).clone()) };
|
||||||
|
run_effects(ctx.trans, &effects, &mut player_mut, &item, &mut target_mut, skilllvl,
|
||||||
|
use_data.task_ref).await?;
|
||||||
|
if let Some(target_mut_save) = target_mut {
|
||||||
|
ctx.trans.save_item_model(&target_mut_save).await?;
|
||||||
|
}
|
||||||
|
ctx.trans.save_item_model(&player_mut).await?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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.is_dead {
|
||||||
|
user_error("You try to use it, but your ghostly hands slip through it uselessly".to_owned())?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (what_name, whom_name) = parsing::parse_on_or_default(remaining, "me");
|
||||||
|
|
||||||
|
let item = search_item_for_user(ctx, &ItemSearchParams {
|
||||||
|
include_contents: true,
|
||||||
|
item_type_only: Some("possession"),
|
||||||
|
limit: 1,
|
||||||
|
..ItemSearchParams::base(&player_item, &what_name)
|
||||||
|
}).await?;
|
||||||
|
|
||||||
|
let target = if whom_name == "me" || whom_name == "self" { player_item.clone() } else {
|
||||||
|
search_item_for_user(ctx, &ItemSearchParams {
|
||||||
|
include_contents: true,
|
||||||
|
include_loc_contents: true,
|
||||||
|
limit: 1,
|
||||||
|
..ItemSearchParams::base(&player_item, &whom_name)
|
||||||
|
}).await?
|
||||||
|
};
|
||||||
|
|
||||||
|
let use_data = match item.possession_type.as_ref()
|
||||||
|
.and_then(|poss_type| possession_data().get(&poss_type))
|
||||||
|
.and_then(|poss_data| poss_data.use_data.as_ref()) {
|
||||||
|
None => user_error("You can't use that!".to_owned())?,
|
||||||
|
Some(d) => d
|
||||||
|
};
|
||||||
|
if let Some(err) = (use_data.errorf)(&item, &target) {
|
||||||
|
user_error(err)?;
|
||||||
|
}
|
||||||
|
queue_command(ctx, &QueueCommand::Use {
|
||||||
|
possession_id: item.item_code.clone(),
|
||||||
|
target_id: format!("{}/{}", target.item_type, target.item_code)}).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
static VERB_INT: Verb = Verb;
|
||||||
|
pub static VERB: UserVerbRef = &VERB_INT as UserVerbRef;
|
@ -1,6 +1,8 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use crate::services::effect::DelayedHealthEffect;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
pub enum TaskRecurrence {
|
pub enum TaskRecurrence {
|
||||||
@ -28,6 +30,10 @@ pub enum TaskDetails {
|
|||||||
RotCorpse {
|
RotCorpse {
|
||||||
corpse_code: String
|
corpse_code: String
|
||||||
},
|
},
|
||||||
|
DelayedHealth {
|
||||||
|
item: String,
|
||||||
|
effect_series: VecDeque<DelayedHealthEffect>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
impl TaskDetails {
|
impl TaskDetails {
|
||||||
pub fn name(self: &Self) -> &'static str {
|
pub fn name(self: &Self) -> &'static str {
|
||||||
@ -40,6 +46,7 @@ impl TaskDetails {
|
|||||||
AttackTick => "AttackTick",
|
AttackTick => "AttackTick",
|
||||||
RecloneNPC { .. } => "RecloneNPC",
|
RecloneNPC { .. } => "RecloneNPC",
|
||||||
RotCorpse { .. } => "RotCorpse",
|
RotCorpse { .. } => "RotCorpse",
|
||||||
|
DelayedHealth { .. } => "DelayedHealth",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ use crate::{
|
|||||||
models::task::Task,
|
models::task::Task,
|
||||||
listener::{ListenerMap, ListenerSend},
|
listener::{ListenerMap, ListenerSend},
|
||||||
static_content::npc,
|
static_content::npc,
|
||||||
services::combat,
|
services::{combat, effect},
|
||||||
};
|
};
|
||||||
#[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence};
|
#[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence};
|
||||||
use mockall_double::double;
|
use mockall_double::double;
|
||||||
@ -42,6 +42,7 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task
|
|||||||
("AttackTick", combat::TASK_HANDLER.clone()),
|
("AttackTick", combat::TASK_HANDLER.clone()),
|
||||||
("RecloneNPC", npc::RECLONE_HANDLER.clone()),
|
("RecloneNPC", npc::RECLONE_HANDLER.clone()),
|
||||||
("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()),
|
("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()),
|
||||||
|
("DelayedHealth", effect::DELAYED_HEALTH_HANDLER.clone()),
|
||||||
).into_iter().collect()
|
).into_iter().collect()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -17,6 +17,7 @@ use crate::message_handler::user_commands::{
|
|||||||
get,
|
get,
|
||||||
drop,
|
drop,
|
||||||
movement,
|
movement,
|
||||||
|
use_cmd,
|
||||||
wield,
|
wield,
|
||||||
user_error,
|
user_error,
|
||||||
get_user_or_fail
|
get_user_or_fail
|
||||||
@ -28,6 +29,7 @@ use once_cell::sync::OnceCell;
|
|||||||
pub enum QueueCommand {
|
pub enum QueueCommand {
|
||||||
Movement { direction: Direction },
|
Movement { direction: Direction },
|
||||||
Wield { possession_id: String },
|
Wield { possession_id: String },
|
||||||
|
Use { possession_id: String, target_id: String },
|
||||||
Get { possession_id: String },
|
Get { possession_id: String },
|
||||||
Drop { possession_id: String },
|
Drop { possession_id: String },
|
||||||
}
|
}
|
||||||
@ -37,6 +39,7 @@ impl QueueCommand {
|
|||||||
match self {
|
match self {
|
||||||
Movement {..} => "Movement",
|
Movement {..} => "Movement",
|
||||||
Wield {..} => "Wield",
|
Wield {..} => "Wield",
|
||||||
|
Use {..} => "Use",
|
||||||
Get {..} => "Get",
|
Get {..} => "Get",
|
||||||
Drop {..} => "Drop",
|
Drop {..} => "Drop",
|
||||||
}
|
}
|
||||||
@ -55,7 +58,8 @@ fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn Que
|
|||||||
REGISTRY.get_or_init(|| vec!(
|
REGISTRY.get_or_init(|| vec!(
|
||||||
("Drop", &drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
("Drop", &drop::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
||||||
("Get", &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
("Get", &get::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
||||||
("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
("Movement", &movement::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
||||||
|
("Use", &use_cmd::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
||||||
("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
("Wield", &wield::QueueHandler as &(dyn QueueCommandHandler + Sync + Send)),
|
||||||
).into_iter().collect())
|
).into_iter().collect())
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,8 @@ use mockall_double::double;
|
|||||||
pub mod skills;
|
pub mod skills;
|
||||||
pub mod combat;
|
pub mod combat;
|
||||||
pub mod capacity;
|
pub mod capacity;
|
||||||
|
pub mod effect;
|
||||||
|
|
||||||
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>,
|
||||||
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> {
|
||||||
for item in trans.find_items_by_location(location).await? {
|
for item in trans.find_items_by_location(location).await? {
|
||||||
|
@ -80,6 +80,8 @@ impl TaskHandler for AttackTaskHandler {
|
|||||||
victim_item.display_for_sentence(false, 1, true),
|
victim_item.display_for_sentence(false, 1, true),
|
||||||
attacker_item.display_for_sentence(false, 1, false));
|
attacker_item.display_for_sentence(false, 1, false));
|
||||||
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||||
|
ctx.trans.save_item_model(&attacker_item).await?;
|
||||||
|
ctx.trans.save_item_model(&victim_item).await?;
|
||||||
} else {
|
} else {
|
||||||
// TODO: Parry system of some kind?
|
// TODO: Parry system of some kind?
|
||||||
|
|
||||||
@ -99,38 +101,60 @@ impl TaskHandler for AttackTaskHandler {
|
|||||||
|
|
||||||
let actual_damage = Normal::new(mean_damage,
|
let actual_damage = Normal::new(mean_damage,
|
||||||
weapon.normal_attack_stdev_damage)?
|
weapon.normal_attack_stdev_damage)?
|
||||||
.sample(&mut rand::thread_rng()).floor().max(1.0) as u64;
|
.sample(&mut rand::thread_rng()).floor().max(1.0) as i64;
|
||||||
let new_health = if actual_damage > victim_item.health { 0 } else { victim_item.health - actual_damage };
|
ctx.trans.save_item_model(&attacker_item).await?;
|
||||||
let msg_exp = format!(ansi!("[ <red>{}<reset> <bold>{}/{}<reset> ] {}.\n"),
|
let msg_exp = weapon.normal_attack_success_message(&attacker_item, &victim_item,
|
||||||
actual_damage,
|
&part, true);
|
||||||
new_health,
|
let msg_nonexp = weapon.normal_attack_success_message(&attacker_item, &victim_item,
|
||||||
max_health(&victim_item),
|
&part, false);
|
||||||
weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, true));
|
if change_health(ctx.trans, -actual_damage, &mut victim_item,
|
||||||
let msg_nonexp =
|
&msg_exp, &msg_nonexp).await? {
|
||||||
format!(ansi!("[ <red>{}<reset> <bold>{}/{}<reset> ] {}.\n"),
|
|
||||||
actual_damage,
|
|
||||||
new_health,
|
|
||||||
max_health(&victim_item),
|
|
||||||
weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, false));
|
|
||||||
broadcast_to_room(ctx.trans, &attacker_item.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
|
||||||
victim_item.health = new_health;
|
|
||||||
if new_health == 0 {
|
|
||||||
ctx.trans.save_item_model(&attacker_item).await?;
|
|
||||||
handle_death(ctx.trans, &mut victim_item).await?;
|
|
||||||
ctx.trans.save_item_model(&victim_item).await?;
|
ctx.trans.save_item_model(&victim_item).await?;
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
ctx.trans.save_item_model(&victim_item).await?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let msg_exp = &(weapon.normal_attack_start_message(&attacker_item, &victim_item, true) + ".\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");
|
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?;
|
broadcast_to_room(ctx.trans, &attacker_item.location, None, msg_exp, Some(msg_nonexp)).await?;
|
||||||
ctx.trans.save_item_model(&attacker_item).await?;
|
|
||||||
ctx.trans.save_item_model(&victim_item).await?;
|
|
||||||
Ok(Some(attack_speed(&attacker_item)))
|
Ok(Some(attack_speed(&attacker_item)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn change_health(trans: &DBTrans,
|
||||||
|
change: i64,
|
||||||
|
victim: &mut Item,
|
||||||
|
reason_exp: &str, reason_nonexp: &str) -> DResult<bool> {
|
||||||
|
let maxh = max_health(victim);
|
||||||
|
let new_health = ((victim.health as i64 + change).max(0) as u64).min(maxh);
|
||||||
|
if change >= 0 && new_health == victim.health {
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
let colour = if change > 0 { ansi!("<green>") } else { ansi!("<red>") };
|
||||||
|
let msg_exp = format!(ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
|
||||||
|
colour,
|
||||||
|
change,
|
||||||
|
new_health,
|
||||||
|
max_health(&victim),
|
||||||
|
reason_exp);
|
||||||
|
let msg_nonexp =
|
||||||
|
format!(ansi!("[ {}{}<reset> <bold>{}/{}<reset> ] {}.\n"),
|
||||||
|
colour,
|
||||||
|
change,
|
||||||
|
new_health,
|
||||||
|
maxh,
|
||||||
|
reason_nonexp);
|
||||||
|
broadcast_to_room(trans, &victim.location, None, &msg_exp, Some(&msg_nonexp)).await?;
|
||||||
|
victim.health = new_health;
|
||||||
|
if new_health == 0 {
|
||||||
|
handle_death(trans, victim).await?;
|
||||||
|
Ok(true)
|
||||||
|
} else {
|
||||||
|
Ok(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item: &Item) -> DResult<()> {
|
pub async fn consider_reward_for(trans: &DBTrans, by_item: &mut Item, for_item: &Item) -> DResult<()> {
|
||||||
if by_item.item_type != "player" {
|
if by_item.item_type != "player" {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
131
blastmud_game/src/services/effect.rs
Normal file
131
blastmud_game/src/services/effect.rs
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
use crate::{
|
||||||
|
db::DBTrans,
|
||||||
|
models::{
|
||||||
|
item::Item,
|
||||||
|
task::{
|
||||||
|
Task,
|
||||||
|
TaskMeta,
|
||||||
|
TaskDetails,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
DResult,
|
||||||
|
static_content::{
|
||||||
|
possession_type::UseEffect,
|
||||||
|
},
|
||||||
|
regular_tasks::{
|
||||||
|
TaskHandler,
|
||||||
|
TaskRunContext,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
use super::{
|
||||||
|
broadcast_to_room,
|
||||||
|
combat::change_health,
|
||||||
|
};
|
||||||
|
use async_trait::async_trait;
|
||||||
|
use std::time;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use std::collections::{BTreeMap, VecDeque};
|
||||||
|
use chrono::Utc;
|
||||||
|
use log::info;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
|
||||||
|
pub struct DelayedHealthEffect {
|
||||||
|
magnitude: i64,
|
||||||
|
delay: u64,
|
||||||
|
message: String,
|
||||||
|
message_nonexp: String
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DelayedHealthTaskHandler;
|
||||||
|
#[async_trait]
|
||||||
|
impl TaskHandler for DelayedHealthTaskHandler {
|
||||||
|
async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult<Option<time::Duration>> {
|
||||||
|
let ref mut item_effect_series = match &mut ctx.task.details {
|
||||||
|
TaskDetails::DelayedHealth { item, ref mut effect_series } => (item, effect_series),
|
||||||
|
_ => Err("Expected DelayedHealth type")?
|
||||||
|
};
|
||||||
|
let (item_type, item_code) = match item_effect_series.0.split_once("/") {
|
||||||
|
None => {
|
||||||
|
info!("Invalid item {} to DelayedHealthTaskHandler", item_effect_series.0);
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Some((item_type, item_code)) => (item_type, item_code)
|
||||||
|
};
|
||||||
|
let item = match ctx.trans.find_item_by_type_code(item_type, item_code).await? {
|
||||||
|
None => {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
Some(it) => it
|
||||||
|
};
|
||||||
|
match item_effect_series.1.pop_front() {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(DelayedHealthEffect { magnitude, message, message_nonexp, .. }) => {
|
||||||
|
let mut item_mut = (*item).clone();
|
||||||
|
change_health(ctx.trans, magnitude, &mut item_mut, &message, &message_nonexp).await?;
|
||||||
|
ctx.trans.save_item_model(&item_mut).await?;
|
||||||
|
Ok(item_effect_series.1.front().map(|it| time::Duration::from_secs(it.delay)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub static DELAYED_HEALTH_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DelayedHealthTaskHandler;
|
||||||
|
|
||||||
|
pub async fn run_effects(
|
||||||
|
trans: &DBTrans, effects: &Vec<UseEffect>,
|
||||||
|
player: &mut Item,
|
||||||
|
item: &Item,
|
||||||
|
// None if target is player
|
||||||
|
target: &mut Option<Item>,
|
||||||
|
level: f64,
|
||||||
|
task_ref: &str
|
||||||
|
) -> DResult<()> {
|
||||||
|
let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::new();
|
||||||
|
for effect in effects {
|
||||||
|
match effect {
|
||||||
|
UseEffect::BroadcastMessage { messagef } => {
|
||||||
|
let (msg_exp, msg_nonexp) = messagef(player, item, target.as_ref().unwrap_or(player));
|
||||||
|
broadcast_to_room(trans, &player.location, None, &msg_exp,
|
||||||
|
Some(&msg_nonexp)).await?;
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth { delay_secs, base_effect, skill_multiplier, max_effect,
|
||||||
|
message } => {
|
||||||
|
let health_impact =
|
||||||
|
(*base_effect + ((skill_multiplier * level) as i64).min(*max_effect)) as i64;
|
||||||
|
let (msg, msg_nonexp) = message(target.as_ref().unwrap_or(player));
|
||||||
|
if *delay_secs == 0 {
|
||||||
|
change_health(trans, health_impact, target.as_mut().unwrap_or(player), &msg,
|
||||||
|
&msg_nonexp).await?;
|
||||||
|
} else {
|
||||||
|
let target_it = target.as_ref().unwrap_or(player);
|
||||||
|
let fx = DelayedHealthEffect {
|
||||||
|
magnitude: health_impact,
|
||||||
|
delay: *delay_secs,
|
||||||
|
message: msg,
|
||||||
|
message_nonexp: msg_nonexp
|
||||||
|
};
|
||||||
|
target_health_series
|
||||||
|
.entry(format!("{}/{}", target_it.item_type, target_it.item_code))
|
||||||
|
.and_modify(|l| l.push_back(fx.clone()))
|
||||||
|
.or_insert(VecDeque::from([fx]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (eff_item, l) in target_health_series.into_iter() {
|
||||||
|
trans.upsert_task(&Task {
|
||||||
|
meta: TaskMeta {
|
||||||
|
task_code: format!("{}/{}", eff_item, task_ref),
|
||||||
|
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
details: TaskDetails::DelayedHealth {
|
||||||
|
effect_series: l,
|
||||||
|
item: eff_item,
|
||||||
|
}
|
||||||
|
}).await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
@ -69,6 +69,38 @@ impl Default for ChargeData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub enum UseEffect {
|
||||||
|
// messagef takes player, item used, target as the 3 parameters. Returns (explicit, non explicit) message.
|
||||||
|
BroadcastMessage { messagef: Box<dyn Fn(&Item, &Item, &Item) -> (String, String) + Sync + Send>},
|
||||||
|
// skill_multiplier is always positive - sign flipped for crit fails.
|
||||||
|
ChangeTargetHealth { delay_secs: u64, base_effect: i64, skill_multiplier: f64,
|
||||||
|
max_effect: i64, message: Box<dyn Fn(&Item) -> (String, String) + Sync + Send> }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct UseData {
|
||||||
|
pub uses_skill: SkillType,
|
||||||
|
pub diff_level: f64,
|
||||||
|
pub crit_fail_effects: Vec<UseEffect>,
|
||||||
|
pub fail_effects: Vec<UseEffect>,
|
||||||
|
pub success_effects: Vec<UseEffect>,
|
||||||
|
pub errorf: Box<dyn Fn(&Item, &Item) -> Option<String> + Sync + Send>,
|
||||||
|
pub task_ref: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UseData {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
uses_skill: SkillType::Medic,
|
||||||
|
diff_level: 10.0,
|
||||||
|
crit_fail_effects: vec!(),
|
||||||
|
fail_effects: vec!(),
|
||||||
|
success_effects: vec!(),
|
||||||
|
errorf: Box::new(|_it, _target| None),
|
||||||
|
task_ref: "set me",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct PossessionData {
|
pub struct PossessionData {
|
||||||
pub weapon_data: Option<WeaponData>,
|
pub weapon_data: Option<WeaponData>,
|
||||||
pub display: &'static str,
|
pub display: &'static str,
|
||||||
@ -78,6 +110,7 @@ pub struct PossessionData {
|
|||||||
pub aliases: Vec<&'static str>,
|
pub aliases: Vec<&'static str>,
|
||||||
pub max_health: u64,
|
pub max_health: u64,
|
||||||
pub charge_data: Option<ChargeData>,
|
pub charge_data: Option<ChargeData>,
|
||||||
|
pub use_data: Option<UseData>,
|
||||||
pub weight: u64,
|
pub weight: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,6 +126,7 @@ impl Default for PossessionData {
|
|||||||
max_health: 10,
|
max_health: 10,
|
||||||
weight: 100,
|
weight: 100,
|
||||||
charge_data: None,
|
charge_data: None,
|
||||||
|
use_data: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -258,6 +292,200 @@ pub fn possession_data() -> &'static BTreeMap<PossessionType, PossessionData> {
|
|||||||
charge_name_suffix: "worth of supplies",
|
charge_name_suffix: "worth of supplies",
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
|
use_data: Some(UseData {
|
||||||
|
uses_skill: SkillType::Medic,
|
||||||
|
diff_level: 10.0,
|
||||||
|
crit_fail_effects: vec!(
|
||||||
|
UseEffect::BroadcastMessage {
|
||||||
|
messagef: Box::new(|player, _item, target| (
|
||||||
|
format!(
|
||||||
|
"{} attempts to heal {} with a trauma kit, but fucks it up badly\n",
|
||||||
|
&player.display_for_sentence(true, 1, true),
|
||||||
|
&if target.item_type == player.item_type && target.item_code == player.item_code {
|
||||||
|
player.pronouns.intensive.clone()
|
||||||
|
} else {
|
||||||
|
target.display_for_sentence(true, 1, false)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
format!("{} attempts to heal {} with a trauma kit, but messes it up badly\n",
|
||||||
|
&player.display_for_sentence(false, 1, true),
|
||||||
|
&if target.item_type == player.item_type && target.item_code == player.item_code {
|
||||||
|
player.pronouns.intensive.clone()
|
||||||
|
} else {
|
||||||
|
target.display_for_sentence(false, 1, false)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 0, base_effect: -2, skill_multiplier: -3.0,
|
||||||
|
max_effect: -5,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"Fuck! The trauma kit makes {}'s condition worse",
|
||||||
|
target.display_for_sentence(true, 1, false)),
|
||||||
|
format!(
|
||||||
|
"The trauma kit makes {}'s condition worse",
|
||||||
|
target.display_for_sentence(false, 1, false)
|
||||||
|
)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
),
|
||||||
|
fail_effects: vec!(
|
||||||
|
UseEffect::BroadcastMessage {
|
||||||
|
messagef: Box::new(|player, _item, target| (
|
||||||
|
format!(
|
||||||
|
"{} attempts unsuccessfully to heal {} with a trauma kit\n",
|
||||||
|
&player.display_for_sentence(true, 1, true),
|
||||||
|
&if target.item_type == player.item_type && target.item_code == player.item_code {
|
||||||
|
player.pronouns.intensive.clone()
|
||||||
|
} else {
|
||||||
|
target.display_for_sentence(true, 1, false)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
format!("{} attempts unsuccessfully to heal {} with a trauma kit\n",
|
||||||
|
&player.display_for_sentence(false, 1, true),
|
||||||
|
&if target.item_type == player.item_type && target.item_code == player.item_code {
|
||||||
|
player.pronouns.intensive.clone()
|
||||||
|
} else {
|
||||||
|
target.display_for_sentence(false, 1, false)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
),
|
||||||
|
success_effects: vec!(
|
||||||
|
UseEffect::BroadcastMessage {
|
||||||
|
messagef: Box::new(|player, _item, target| (
|
||||||
|
format!(
|
||||||
|
"{} expertly heals {} with a trauma kit\n",
|
||||||
|
&player.display_for_sentence(true, 1, true),
|
||||||
|
&if target.item_type == player.item_type && target.item_code == player.item_code {
|
||||||
|
player.pronouns.intensive.clone()
|
||||||
|
} else {
|
||||||
|
target.display_for_sentence(true, 1, false)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
format!("{} expertly heals {} with a trauma kit\n",
|
||||||
|
&player.display_for_sentence(false, 1, true),
|
||||||
|
&if target.item_type == player.item_type && target.item_code == player.item_code {
|
||||||
|
player.pronouns.intensive.clone()
|
||||||
|
} else {
|
||||||
|
target.display_for_sentence(false, 1, false)
|
||||||
|
}
|
||||||
|
)))
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 0, base_effect: 2, skill_multiplier: 8.0,
|
||||||
|
max_effect: 10,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"FUUUCK! It hurts {}, but also starts to soothe {}",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
&target.pronouns.object
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"It hurts {}, but also starts to soothe {}",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
&target.pronouns.object
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 10, base_effect: 2, skill_multiplier: 7.0,
|
||||||
|
max_effect: 9,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"FUUUCK! It hurts {}, but also starts to soothe {}",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
&target.pronouns.object
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"It hurts {}, but also starts to soothe {}",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
&target.pronouns.object
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 20, base_effect: 1, skill_multiplier: 6.0,
|
||||||
|
max_effect: 7,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(false, 1, false),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 30, base_effect: 1, skill_multiplier: 5.0,
|
||||||
|
max_effect: 6,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(false, 1, false),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 40, base_effect: 0, skill_multiplier: 4.0,
|
||||||
|
max_effect: 4,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(false, 1, false),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 50, base_effect: 0, skill_multiplier: 3.0,
|
||||||
|
max_effect: 3,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(false, 1, false),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
UseEffect::ChangeTargetHealth {
|
||||||
|
delay_secs: 60, base_effect: 0, skill_multiplier: 2.0,
|
||||||
|
max_effect: 2,
|
||||||
|
message: Box::new(
|
||||||
|
|target|
|
||||||
|
(format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(true, 1, false),
|
||||||
|
),
|
||||||
|
format!(
|
||||||
|
"The bandages soothe {}'s wounds",
|
||||||
|
target.display_for_sentence(false, 1, false),
|
||||||
|
))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
..Default::default()
|
..Default::default()
|
||||||
}),
|
}),
|
||||||
).into_iter().collect()
|
).into_iter().collect()
|
||||||
|
Loading…
Reference in New Issue
Block a user