blastmud/blastmud_game/src/services/effect.rs
Condorra 8aff296e03 Add the start of Ronald's House
And minor bugfixes on stability of item indexes, making stings less
aggressive.
2024-05-22 22:57:36 +10:00

847 lines
34 KiB
Rust

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<Option<time::Duration>> {
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<Option<time::Duration>> {
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<Option<time::Duration>> {
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::<String, VecDeque<DelayedParameterEffect>>::new();
let mut target_message_series = BTreeMap::<String, VecDeque<DelayedMessageEffect>>::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<EffectType, EffectSet> {
static MAP: OnceCell<BTreeMap<EffectType, EffectSet>> = 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!("<red>You notice that {} has a fresh gaping wound that looks like it's about to {}!<reset>\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!("<blue>{} is stunned!<reset>\n"),
target.display_for_sentence(1, true),
)
)
},
Effect::BroadcastMessage {
delay_secs: 30,
messagef: Box::new(|_player, _item, target|
format!(ansi!("<blue>{} seems to have returned to {} senses!<reset>\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!("<red>You notice that {} has a fresh bite wound with some kind of yellow liquid on the bite site!<reset>\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()
})
}