use super::{combat::change_health, comms::broadcast_to_room}; #[double] use crate::db::DBTrans; use crate::{ models::{ effect::{Effect, EffectParameter, EffectSet, EffectType}, item::Item, task::{Task, TaskDetails, TaskMeta}, }, regular_tasks::{TaskHandler, TaskRunContext}, static_content::species::SpeciesType, DResult, }; use ansi::ansi; use async_trait::async_trait; use chrono::Utc; use log::info; use mockall_double::double; use once_cell::sync::OnceCell; use serde::{Deserialize, Serialize}; use std::collections::{BTreeMap, VecDeque}; use std::time; #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct DelayedParameterEffect { magnitude: i64, delay: u64, parameter: EffectParameter, message: String, } #[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq, Ord, PartialOrd)] pub struct DelayedMessageEffect { delay: u64, message: String, is_direct: bool, } pub struct DelayedParameterTaskHandler; #[async_trait] impl TaskHandler for DelayedParameterTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let ref mut item_effect_series = match &mut ctx.task.details { TaskDetails::DelayedParameter { item, ref mut effect_series, } => (item, effect_series), _ => Err("Expected DelayedParameter type")?, }; let (item_type, item_code) = match item_effect_series.0.split_once("/") { None => { info!( "Invalid item {} to DelayedParameterTaskHandler", 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, }; if item.death_data.is_some() { return Ok(None); } match item_effect_series.1.pop_front() { None => Ok(None), Some(DelayedParameterEffect { magnitude, message, parameter, delay, .. }) => { let mut item_mut = (*item).clone(); match parameter { EffectParameter::Health => { change_health(ctx.trans, magnitude, &mut item_mut, &message).await?; () } EffectParameter::Raddamage => { item_mut.raddamage = (item_mut.raddamage as i64 + magnitude).max(0) as u64 } } ctx.trans.save_item_model(&item_mut).await?; Ok(item_effect_series .1 .front() .map(|it| time::Duration::from_secs(it.delay - delay))) } } } } pub static DELAYED_PARAMETER_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DelayedParameterTaskHandler; pub struct DelayedMessageTaskHandler; #[async_trait] impl TaskHandler for DelayedMessageTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let ref mut item_effect_series = match &mut ctx.task.details { TaskDetails::DelayedMessage { item, ref mut effect_series, } => (item, effect_series), _ => Err("Expected DelayedMessage type")?, }; let (item_type, item_code) = match item_effect_series.0.split_once("/") { None => { info!( "Invalid item {} to DelayedMessageTaskHandler", 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, }; if item.death_data.is_some() { return Ok(None); } match item_effect_series.1.pop_front() { None => Ok(None), Some(DelayedMessageEffect { message, is_direct, .. }) if is_direct => { match ctx.trans.find_session_for_player(&item_code).await? { None => {} Some((sess, _)) => { ctx.trans.queue_for_session(&sess, Some(&message)).await?; } } Ok(item_effect_series .1 .front() .map(|it| time::Duration::from_secs(it.delay))) } Some(DelayedMessageEffect { message, .. }) => { broadcast_to_room(&ctx.trans, &item.location, None, &message).await?; Ok(item_effect_series .1 .front() .map(|it| time::Duration::from_secs(it.delay))) } } } } pub static DELAYED_MESSAGE_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DelayedMessageTaskHandler; pub struct DispelEffectTaskHandler; #[async_trait] impl TaskHandler for DispelEffectTaskHandler { async fn do_task(&self, ctx: &mut TaskRunContext) -> DResult> { let (target_str, effect_type) = match ctx.task.details { TaskDetails::DispelEffect { target: ref t, effect_type: ref e, } => (t.as_str(), e), _ => Err("Expected DispelEffect type")?, }; let (target_type, target_code) = match target_str.split_once("/") { None => Err("Invalid DispelEffect target")?, Some(v) => v, }; let target = match ctx .trans .find_item_by_type_code(target_type, target_code) .await? { None => return Ok(None), Some(t) => t, }; let mut target_mut = (*target).clone(); target_mut .active_effects .iter() .position(|e| e.0 == *effect_type) .map(|p| target_mut.active_effects.remove(p)); ctx.trans.save_item_model(&target_mut).await?; Ok(None) } } pub static DISPEL_EFFECT_HANDLER: &'static (dyn TaskHandler + Sync + Send) = &DispelEffectTaskHandler; pub async fn run_effects( trans: &DBTrans, effects: &EffectSet, mut player: &mut Item, item: &Item, // None if target is player mut target: Option<&mut Item>, level: f64, ) -> DResult<()> { let mut target_health_series = BTreeMap::>::new(); let mut target_message_series = BTreeMap::>::new(); let mut dispel_time_secs: u64 = 0; for effect in &effects.effects { match effect { Effect::BroadcastMessage { delay_secs, messagef, } => { let msg = messagef( player, item, target.as_ref().map(|t| &**t).unwrap_or(player), ); if *delay_secs == 0 { broadcast_to_room(trans, &player.location, None, &msg).await?; } else { dispel_time_secs = dispel_time_secs.max(*delay_secs); let fx = DelayedMessageEffect { delay: *delay_secs, message: msg, is_direct: false, }; let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); target_message_series .entry(format!( "{}/{}", actual_target.item_type, actual_target.item_code )) .and_modify(|l| l.push_back(fx.clone())) .or_insert(VecDeque::from([fx])); } } Effect::DirectMessage { delay_secs, messagef, } => { let msg = messagef( player, item, target.as_ref().map(|t| &**t).unwrap_or(player), ); if *delay_secs == 0 { let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); match trans .find_session_for_player(&actual_target.item_code) .await? { None => {} Some((sess, _)) => { trans.queue_for_session(&sess, Some(&msg)).await?; } } } else { dispel_time_secs = dispel_time_secs.max(*delay_secs); let fx = DelayedMessageEffect { delay: *delay_secs, message: msg, is_direct: true, }; let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); target_message_series .entry(format!( "{}/{}", actual_target.item_type, actual_target.item_code )) .and_modify(|l| l.push_back(fx.clone())) .or_insert(VecDeque::from([fx])); } } Effect::ChangeTargetParameter { delay_secs, base_effect, skill_multiplier, max_effect, parameter, message, } => { let param_impact = *base_effect + ((skill_multiplier * level) as i64); let param_impact = if *max_effect >= 0 { param_impact.min(*max_effect) } else { param_impact.max(*max_effect) }; let msg = message(target.as_ref().map(|t| &**t).unwrap_or(player)); if *delay_secs == 0 { match parameter { EffectParameter::Health => { change_health( trans, param_impact, *target.as_mut().unwrap_or(&mut player), &msg, ) .await?; () } EffectParameter::Raddamage => { let eff_target = target.as_mut().unwrap_or(&mut player); eff_target.raddamage = (eff_target.raddamage as i64 + param_impact).max(0) as u64 } } } else { dispel_time_secs = dispel_time_secs.max(*delay_secs); let target_it = target.as_ref().map(|t| &**t).unwrap_or(player); let fx = DelayedParameterEffect { magnitude: param_impact, delay: *delay_secs, parameter: (*parameter).clone(), message: msg, }; 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])); } } } } let task_ref = trans.alloc_task_code().await?; if dispel_time_secs > 0 && effects.effect_type != EffectType::Ephemeral { let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player); actual_target .active_effects .push((effects.effect_type.clone(), task_ref.clone())); trans .upsert_task(&Task { meta: TaskMeta { task_code: format!("{}/{}", &actual_target.refstr(), task_ref), next_scheduled: Utc::now() + chrono::TimeDelta::try_seconds(dispel_time_secs as i64).unwrap(), ..Default::default() }, details: TaskDetails::DispelEffect { target: actual_target.refstr(), effect_type: effects.effect_type.clone(), }, }) .await?; } for (eff_item, l) in target_health_series.into_iter() { trans .upsert_task(&Task { meta: TaskMeta { task_code: format!("{}/{}", eff_item, trans.alloc_task_code().await?), next_scheduled: Utc::now() + chrono::TimeDelta::try_seconds(l[0].delay as i64).unwrap(), ..Default::default() }, details: TaskDetails::DelayedParameter { effect_series: l, item: eff_item, }, }) .await?; } for (eff_item, l) in target_message_series.into_iter() { trans .upsert_task(&Task { meta: TaskMeta { task_code: format!("{}/{}", eff_item, task_ref), next_scheduled: Utc::now() + chrono::TimeDelta::try_seconds(l[0].delay as i64).unwrap(), ..Default::default() }, details: TaskDetails::DelayedMessage { effect_series: l, item: eff_item, }, }) .await?; } Ok(()) } pub async fn cancel_effect( trans: &DBTrans, character: &Item, effect: &(EffectType, i64), ) -> DResult<()> { trans .delete_task( "DelayedMessage", &format!( "{}/{}/{}", &character.item_type, &character.item_code, effect.1 ), ) .await?; trans .delete_task( "DelayedParameter", &format!( "{}/{}/{}", &character.item_type, &character.item_code, effect.1 ), ) .await?; trans .delete_task( "DispelEffect", &format!( "{}/{}/{}", &character.item_type, &character.item_code, effect.1 ), ) .await?; Ok(()) } pub fn default_effects_for_type() -> &'static BTreeMap { static MAP: OnceCell> = OnceCell::new(); MAP.get_or_init(|| { vec![ EffectSet { effect_type: EffectType::Bleed, effects: vec![ Effect::BroadcastMessage { delay_secs: 0, messagef: Box::new(|_player, _item, target| format!(ansi!("You notice that {} has a fresh gaping wound that looks like it's about to {}!\n"), target.display_for_sentence(1, false), if target.species == SpeciesType::Robot { "leak coolant" } else { "bleed" } ) ) }, Effect::ChangeTargetParameter { delay_secs: 10, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} pulses from {}'s wound", if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" }, target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 20, base_effect: -10, skill_multiplier: 0.0, max_effect: -10, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} pulses from {}'s wound", if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" }, target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 30, base_effect: -8, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} pulses from {}'s wound", if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" }, target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 40, base_effect: -6, skill_multiplier: 0.0, max_effect: -6, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} pulses from {}'s wound", if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" }, target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 50, base_effect: -4, skill_multiplier: 0.0, max_effect: -4, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} pulses from {}'s wound", if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" }, target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 60, base_effect: -2, skill_multiplier: 0.0, max_effect: -2, parameter: EffectParameter::Health, message: Box::new(|target| format!("A final tiny drop of {} oozes from {}'s wound as it heals", if target.species == SpeciesType::Robot { "coolant" } else { "blood" }, target.display_for_sentence(1, false), ) ) }, ], }, EffectSet { effect_type: EffectType::Stunned, effects: vec![ Effect::BroadcastMessage { delay_secs: 0, messagef: Box::new(|_player, _item, target| format!(ansi!("{} is stunned!\n"), target.display_for_sentence(1, true), ) ) }, Effect::BroadcastMessage { delay_secs: 30, messagef: Box::new(|_player, _item, target| format!(ansi!("{} seems to have returned to {} senses!\n"), target.display_for_sentence(1, true), &target.pronouns.object, ) ) }, ] }, EffectSet { effect_type: EffectType::SnakePoisoned, effects: vec![ Effect::BroadcastMessage { delay_secs: 0, messagef: Box::new(|_player, _item, target| format!(ansi!("You notice that {} has a fresh bite wound with some kind of yellow liquid on the bite site!\n"), target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 60, base_effect: -6, skill_multiplier: 0.0, max_effect: -6, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks a bit unwell", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 120, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks pretty unwell", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 180, base_effect: -18, skill_multiplier: 0.0, max_effect: -18, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks very sick", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 240, base_effect: -18, skill_multiplier: 0.0, max_effect: -18, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks very sick", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 300, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks pretty unwell", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 360, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks pretty unwell", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 420, base_effect: -6, skill_multiplier: 0.0, max_effect: -6, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} looks a bit unwell", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 480, base_effect: -6, skill_multiplier: 0.0, max_effect: -6, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} feels the final effects of snake venom as it leaves {} body", target.display_for_sentence(1, false), target.pronouns.possessive, ) ) }, ], }, EffectSet { effect_type: EffectType::Stung, effects: vec![ Effect::ChangeTargetParameter { delay_secs: 0, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} howls out in pain from a sting", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 20, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} howls out in pain from a sting", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 40, base_effect: -12, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} howls out in pain from a sting", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 60, base_effect: -8, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} yelps in pain from the sting", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 80, base_effect: -8, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} yelps in pain from the sting", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 100, base_effect: -8, skill_multiplier: 0.0, max_effect: -12, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} yelps in pain from the sting", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 120, base_effect: -6, skill_multiplier: 0.0, max_effect: -10, parameter: EffectParameter::Health, message: Box::new(|target| format!("{}'s cries as the sting really hurts", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 140, base_effect: -6, skill_multiplier: 0.0, max_effect: -10, parameter: EffectParameter::Health, message: Box::new(|target| format!("{}'s cries as the sting really hurts", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 160, base_effect: -6, skill_multiplier: 0.0, max_effect: -10, parameter: EffectParameter::Health, message: Box::new(|target| format!("{}'s cries as the sting really hurts", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 170, base_effect: -4, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} sobs as the sting still hurts", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 180, base_effect: -4, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} sobs as the sting still hurts", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 190, base_effect: -4, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} sobs as the sting still hurts", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 200, base_effect: -2, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} whimpers as the sting starts to heal", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 220, base_effect: -2, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} whimpers as the sting is almost healed", target.display_for_sentence(1, false), ) ) }, Effect::ChangeTargetParameter { delay_secs: 230, base_effect: -2, skill_multiplier: 0.0, max_effect: -8, parameter: EffectParameter::Health, message: Box::new(|target| format!("{} whimpers as the sting is finally healed", target.display_for_sentence(1, false), ) ) }, ], }, ] .into_iter() .map(|et| (et.effect_type.clone(), et)) .collect() }) }