diff --git a/blastmud_game/src/message_handler/user_commands.rs b/blastmud_game/src/message_handler/user_commands.rs index 85ebfca..5a3417e 100644 --- a/blastmud_game/src/message_handler/user_commands.rs +++ b/blastmud_game/src/message_handler/user_commands.rs @@ -30,6 +30,7 @@ pub mod parsing; mod quit; mod register; pub mod say; +pub mod use_cmd; mod whisper; mod who; pub mod wield; @@ -126,6 +127,8 @@ static REGISTERED_COMMANDS: UserVerbRegistry = phf_map! { "\'" => say::VERB, "say" => say::VERB, + + "use" => use_cmd::VERB, "-" => whisper::VERB, "whisper" => whisper::VERB, diff --git a/blastmud_game/src/message_handler/user_commands/parsing.rs b/blastmud_game/src/message_handler/user_commands/parsing.rs index fc57de0..5b475fb 100644 --- a/blastmud_game/src/message_handler/user_commands/parsing.rs +++ b/blastmud_game/src/message_handler/user_commands/parsing.rs @@ -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)] mod tests { use super::*; diff --git a/blastmud_game/src/message_handler/user_commands/use_cmd.rs b/blastmud_game/src/message_handler/user_commands/use_cmd.rs new file mode 100644 index 0000000..24c3e82 --- /dev/null +++ b/blastmud_game/src/message_handler/user_commands/use_cmd.rs @@ -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 { + 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; diff --git a/blastmud_game/src/models/task.rs b/blastmud_game/src/models/task.rs index acaf127..1a2b3f3 100644 --- a/blastmud_game/src/models/task.rs +++ b/blastmud_game/src/models/task.rs @@ -1,6 +1,8 @@ use serde::{Serialize, Deserialize}; use serde_json::Value; use chrono::{DateTime, Utc}; +use crate::services::effect::DelayedHealthEffect; +use std::collections::VecDeque; #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub enum TaskRecurrence { @@ -28,6 +30,10 @@ pub enum TaskDetails { RotCorpse { corpse_code: String }, + DelayedHealth { + item: String, + effect_series: VecDeque + } } impl TaskDetails { pub fn name(self: &Self) -> &'static str { @@ -40,6 +46,7 @@ impl TaskDetails { AttackTick => "AttackTick", RecloneNPC { .. } => "RecloneNPC", RotCorpse { .. } => "RotCorpse", + DelayedHealth { .. } => "DelayedHealth", } } } diff --git a/blastmud_game/src/regular_tasks.rs b/blastmud_game/src/regular_tasks.rs index 7d7d9d1..894325a 100644 --- a/blastmud_game/src/regular_tasks.rs +++ b/blastmud_game/src/regular_tasks.rs @@ -6,7 +6,7 @@ use crate::{ models::task::Task, listener::{ListenerMap, ListenerSend}, static_content::npc, - services::combat, + services::{combat, effect}, }; #[cfg(not(test))] use crate::models::task::{TaskParse, TaskRecurrence}; use mockall_double::double; @@ -42,6 +42,7 @@ fn task_handler_registry() -> &'static BTreeMap<&'static str, &'static (dyn Task ("AttackTick", combat::TASK_HANDLER.clone()), ("RecloneNPC", npc::RECLONE_HANDLER.clone()), ("RotCorpse", combat::ROT_CORPSE_HANDLER.clone()), + ("DelayedHealth", effect::DELAYED_HEALTH_HANDLER.clone()), ).into_iter().collect() ) } diff --git a/blastmud_game/src/regular_tasks/queued_command.rs b/blastmud_game/src/regular_tasks/queued_command.rs index a91ee08..f2e3550 100644 --- a/blastmud_game/src/regular_tasks/queued_command.rs +++ b/blastmud_game/src/regular_tasks/queued_command.rs @@ -17,6 +17,7 @@ use crate::message_handler::user_commands::{ get, drop, movement, + use_cmd, wield, user_error, get_user_or_fail @@ -28,6 +29,7 @@ use once_cell::sync::OnceCell; pub enum QueueCommand { Movement { direction: Direction }, Wield { possession_id: String }, + Use { possession_id: String, target_id: String }, Get { possession_id: String }, Drop { possession_id: String }, } @@ -37,6 +39,7 @@ impl QueueCommand { match self { Movement {..} => "Movement", Wield {..} => "Wield", + Use {..} => "Use", Get {..} => "Get", Drop {..} => "Drop", } @@ -55,7 +58,8 @@ fn queue_command_registry() -> &'static BTreeMap<&'static str, &'static (dyn Que REGISTRY.get_or_init(|| vec!( ("Drop", &drop::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)), ).into_iter().collect()) } diff --git a/blastmud_game/src/services.rs b/blastmud_game/src/services.rs index dfdcbd0..e22c83f 100644 --- a/blastmud_game/src/services.rs +++ b/blastmud_game/src/services.rs @@ -8,6 +8,8 @@ use mockall_double::double; pub mod skills; pub mod combat; pub mod capacity; +pub mod effect; + pub async fn broadcast_to_room(trans: &DBTrans, location: &str, from_item: Option<&Item>, message_explicit_ok: &str, message_nonexplicit: Option<&str>) -> DResult<()> { for item in trans.find_items_by_location(location).await? { diff --git a/blastmud_game/src/services/combat.rs b/blastmud_game/src/services/combat.rs index 7929e19..821597b 100644 --- a/blastmud_game/src/services/combat.rs +++ b/blastmud_game/src/services/combat.rs @@ -80,6 +80,8 @@ impl TaskHandler for AttackTaskHandler { victim_item.display_for_sentence(false, 1, true), attacker_item.display_for_sentence(false, 1, false)); 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 { // TODO: Parry system of some kind? @@ -99,38 +101,60 @@ impl TaskHandler for AttackTaskHandler { let actual_damage = Normal::new(mean_damage, weapon.normal_attack_stdev_damage)? - .sample(&mut rand::thread_rng()).floor().max(1.0) as u64; - let new_health = if actual_damage > victim_item.health { 0 } else { victim_item.health - actual_damage }; - let msg_exp = format!(ansi!("[ {} {}/{} ] {}.\n"), - actual_damage, - new_health, - max_health(&victim_item), - weapon.normal_attack_success_message(&attacker_item, &victim_item, &part, true)); - let msg_nonexp = - format!(ansi!("[ {} {}/{} ] {}.\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?; + .sample(&mut rand::thread_rng()).floor().max(1.0) as i64; + ctx.trans.save_item_model(&attacker_item).await?; + let msg_exp = weapon.normal_attack_success_message(&attacker_item, &victim_item, + &part, true); + let msg_nonexp = weapon.normal_attack_success_message(&attacker_item, &victim_item, + &part, false); + if change_health(ctx.trans, -actual_damage, &mut victim_item, + &msg_exp, &msg_nonexp).await? { ctx.trans.save_item_model(&victim_item).await?; 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_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?; - ctx.trans.save_item_model(&attacker_item).await?; - ctx.trans.save_item_model(&victim_item).await?; 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 { + 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!("") } else { ansi!("") }; + let msg_exp = format!(ansi!("[ {}{} {}/{} ] {}.\n"), + colour, + change, + new_health, + max_health(&victim), + reason_exp); + let msg_nonexp = + format!(ansi!("[ {}{} {}/{} ] {}.\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<()> { if by_item.item_type != "player" { return Ok(()); diff --git a/blastmud_game/src/services/effect.rs b/blastmud_game/src/services/effect.rs new file mode 100644 index 0000000..b3179f0 --- /dev/null +++ b/blastmud_game/src/services/effect.rs @@ -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> { + 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, + player: &mut Item, + item: &Item, + // None if target is player + target: &mut Option, + level: f64, + task_ref: &str +) -> DResult<()> { + let mut target_health_series = BTreeMap::>::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(()) +} diff --git a/blastmud_game/src/static_content/possession_type.rs b/blastmud_game/src/static_content/possession_type.rs index 27dcfb8..7b40560 100644 --- a/blastmud_game/src/static_content/possession_type.rs +++ b/blastmud_game/src/static_content/possession_type.rs @@ -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 (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 (String, String) + Sync + Send> } +} + +pub struct UseData { + pub uses_skill: SkillType, + pub diff_level: f64, + pub crit_fail_effects: Vec, + pub fail_effects: Vec, + pub success_effects: Vec, + pub errorf: Box Option + 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 weapon_data: Option, pub display: &'static str, @@ -78,6 +110,7 @@ pub struct PossessionData { pub aliases: Vec<&'static str>, pub max_health: u64, pub charge_data: Option, + pub use_data: Option, pub weight: u64, } @@ -93,6 +126,7 @@ impl Default for PossessionData { max_health: 10, weight: 100, charge_data: None, + use_data: None, } } } @@ -258,6 +292,200 @@ pub fn possession_data() -> &'static BTreeMap { charge_name_suffix: "worth of supplies", ..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() }), ).into_iter().collect()