Give some weapons a chance at causing crit status effects.

This commit is contained in:
Condorra 2023-10-16 22:18:03 +11:00
parent 2350e22f5f
commit 6ac3f676be
12 changed files with 214 additions and 42 deletions

View File

@ -1122,6 +1122,14 @@ impl DBTrans {
.get(0))
}
pub async fn alloc_task_code(&self) -> DResult<i64> {
Ok(self
.pg_trans()?
.query_one("SELECT NEXTVAL('task_seq')", &[])
.await?
.get(0))
}
pub async fn get_online_info(&self) -> DResult<Vec<OnlineInfo>> {
Ok(self
.pg_trans()?

View File

@ -228,7 +228,11 @@ pub async fn describe_normal_item(
));
}
if item.active_effects.contains(&EffectType::Bandages) {
if item
.active_effects
.iter()
.any(|e| e.0 == EffectType::Bandages)
{
contents_desc.push_str(&format!(
"{} is wrapped up in bandages.\n",
&language::caps_first(&item.pronouns.subject)

View File

@ -252,9 +252,8 @@ impl QueueCommandHandler for QueueHandler {
&actual_effects,
ctx.item,
&item,
&mut target_mut,
target_mut.as_mut(),
skilllvl,
use_data.task_ref,
)
.await?;
}

View File

@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize};
pub enum EffectType {
Ephemeral, // i.e. no enduring impact to show in status.
Bandages,
Bleed,
}
pub struct EffectSet {

View File

@ -495,7 +495,7 @@ pub struct Item {
pub queue: VecDeque<QueueCommand>,
pub urges: Option<Urges>,
pub liquid_details: Option<LiquidDetails>,
pub active_effects: Vec<EffectType>,
pub active_effects: Vec<(EffectType, i64)>,
}
impl Item {

View File

@ -35,7 +35,9 @@ use chrono::Utc;
use mockall_double::double;
use rand::{prelude::IteratorRandom, Rng};
use rand_distr::{Distribution, Normal};
use std::time;
use std::{sync::Arc, time};
use super::effect::{default_effects_for_type, run_effects};
pub async fn soak_damage<DamageDist: DamageDistribution>(
trans: &DBTrans,
@ -129,6 +131,7 @@ pub async fn soak_damage<DamageDist: DamageDistribution>(
async fn process_attack(
ctx: &mut TaskRunContext<'_>,
attacker_item: &mut Item,
weapon_item: &Item,
victim_item: &mut Item,
attack: &WeaponAttackData,
weapon: &WeaponData,
@ -226,8 +229,6 @@ async fn process_attack(
// Determine body part...
let part = victim_item.species.sample_body_part();
// TODO: Armour / soaks
let mut mean_damage: f64 = attack.mean_damage;
for scaling in attack.skill_scaling.iter() {
let skill = *attacker_item
@ -287,6 +288,29 @@ async fn process_attack(
ctx.trans.save_item_model(victim_item).await?;
return Ok(true);
}
// Consider applying a crit effect...
let mut crit_rand = rand::thread_rng().gen::<f64>();
for (p, eff_type) in &attack.crit_effects {
if victim_item.active_effects.iter().any(|e| e.0 == *eff_type) {
continue;
}
if *p >= crit_rand {
if let Some(effect_set) = default_effects_for_type().get(eff_type) {
run_effects(
ctx.trans,
&effect_set,
attacker_item,
weapon_item,
Some(victim_item),
0.0,
)
.await?;
}
} else {
crit_rand -= *p;
}
}
ctx.trans.save_item_model(victim_item).await?;
}
@ -314,11 +338,11 @@ impl TaskHandler for AttackTaskHandler {
.task_code
.split_once("/")
.ok_or("Invalid AttackTick task code")?;
let mut attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? {
let attacker_item = match ctx.trans.find_item_by_type_code(ctype, ccode).await? {
None => {
return Ok(None);
} // Player is gone
Some(item) => (*item).clone(),
Some(item) => item,
};
let (vtype, vcode) = match attacker_item
@ -341,11 +365,13 @@ impl TaskHandler for AttackTaskHandler {
return Ok(None);
}
let weapon = what_wielded(ctx.trans, &attacker_item).await?;
let (weapon_it, weapon) = what_wielded(ctx.trans, &attacker_item).await?;
let mut attacker_item_mut = (*attacker_item).clone();
process_attack(
ctx,
&mut attacker_item,
&mut attacker_item_mut,
&weapon_it,
&mut victim_item,
&weapon.normal_attack,
&weapon,
@ -355,7 +381,7 @@ impl TaskHandler for AttackTaskHandler {
// We re-check this on the next tick, rather than going off if the victim
// died. That prevents a bug when re-focusing where we re-schedule and then
// re-delete the task.
Ok(Some(attack_speed(&attacker_item)))
Ok(Some(attack_speed(&attacker_item_mut)))
}
}
@ -685,7 +711,10 @@ pub async fn stop_attacking(trans: &DBTrans, by_whom: &Item, to_whom: &Item) ->
Ok(())
}
async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponData> {
async fn what_wielded(
trans: &DBTrans,
who: &Arc<Item>,
) -> DResult<(Arc<Item>, &'static WeaponData)> {
if let Some(item) = trans
.find_by_action_and_location(&who.refstr(), &LocationActionType::Wielded)
.await?
@ -697,7 +726,7 @@ async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponDat
.and_then(|pt| possession_data().get(&pt))
.and_then(|pd| pd.weapon_data.as_ref())
{
return Ok(dat);
return Ok((item.clone(), dat));
}
}
@ -711,11 +740,11 @@ async fn what_wielded(trans: &DBTrans, who: &Item) -> DResult<&'static WeaponDat
.get(intrinsic)
.and_then(|p| p.weapon_data.as_ref())
{
return Ok(weapon);
return Ok((who.clone(), weapon));
}
}
}
Ok(fist())
Ok((who.clone(), fist()))
}
fn attack_speed(_who: &Item) -> time::Duration {
@ -786,7 +815,7 @@ pub async fn start_attack_mut(
&to_whom.display_for_sentence(false, 1, false)
));
let wielded = what_wielded(trans, by_whom).await?;
let (_, wielded) = what_wielded(trans, &Arc::new(by_whom.clone())).await?;
msg_exp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, true) + ".\n"));
msg_nonexp.push_str(&(wielded.normal_attack.start_message(by_whom, to_whom, false) + ".\n"));

View File

@ -8,12 +8,15 @@ use crate::{
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;
@ -183,7 +186,7 @@ impl TaskHandler for DispelEffectTaskHandler {
target_mut
.active_effects
.iter()
.position(|e| e == effect_type)
.position(|e| e.0 == *effect_type)
.map(|p| target_mut.active_effects.remove(p));
ctx.trans.save_item_model(&target_mut).await?;
Ok(None)
@ -195,37 +198,43 @@ pub static DISPEL_EFFECT_HANDLER: &'static (dyn TaskHandler + Sync + Send) =
pub async fn run_effects(
trans: &DBTrans,
effects: &EffectSet,
player: &mut Item,
mut player: &mut Item,
item: &Item,
// None if target is player
target: &mut Option<Item>,
mut target: Option<&mut Item>,
level: f64,
task_ref: &str,
) -> DResult<()> {
let mut target_health_series = BTreeMap::<String, VecDeque<DelayedHealthEffect>>::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_exp, msg_nonexp) =
messagef(player, item, target.as_ref().unwrap_or(player));
let (msg_exp, msg_nonexp) = messagef(
player,
item,
target.as_ref().map(|t| &**t).unwrap_or(player),
);
if *delay_secs == 0 {
broadcast_to_room(trans, &player.location, None, &msg_exp, Some(&msg_nonexp))
.await?;
} else {
dispel_time_secs = dispel_time_secs.max(*delay_secs);
let target_it = target.as_ref().unwrap_or(player);
let fx = DelayedMessageEffect {
delay: *delay_secs,
message: msg_exp,
message_nonexp: msg_nonexp,
};
let actual_target: &mut Item = *target.as_mut().unwrap_or(&mut player);
target_message_series
.entry(format!("{}/{}", target_it.item_type, target_it.item_code))
.entry(format!(
"{}/{}",
actual_target.item_type, actual_target.item_code
))
.and_modify(|l| l.push_back(fx.clone()))
.or_insert(VecDeque::from([fx]));
}
@ -238,20 +247,20 @@ pub async fn run_effects(
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));
(*base_effect + ((skill_multiplier * level) as i64)).min(*max_effect) as i64;
let (msg, msg_nonexp) = message(target.as_ref().map(|t| &**t).unwrap_or(player));
if *delay_secs == 0 {
change_health(
trans,
health_impact,
target.as_mut().unwrap_or(player),
*target.as_mut().unwrap_or(&mut player),
&msg,
&msg_nonexp,
)
.await?;
} else {
dispel_time_secs = dispel_time_secs.max(*delay_secs);
let target_it = target.as_ref().unwrap_or(player);
let target_it = target.as_ref().map(|t| &**t).unwrap_or(player);
let fx = DelayedHealthEffect {
magnitude: health_impact,
delay: *delay_secs,
@ -267,19 +276,22 @@ pub async fn run_effects(
}
}
let task_ref = trans.alloc_task_code().await?;
if dispel_time_secs > 0 && effects.effect_type != EffectType::Ephemeral {
let target_item = target.as_mut().unwrap_or(player);
target_item.active_effects.push(effects.effect_type.clone());
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!("{}/{}", &target_item.refstr(), task_ref),
task_code: format!("{}/{}", &actual_target.refstr(), task_ref),
next_scheduled: Utc::now() + chrono::Duration::seconds(dispel_time_secs as i64),
..Default::default()
},
details: TaskDetails::DispelEffect {
target: target_item.refstr(),
target: actual_target.refstr(),
effect_type: effects.effect_type.clone(),
},
})
@ -290,7 +302,7 @@ pub async fn run_effects(
trans
.upsert_task(&Task {
meta: TaskMeta {
task_code: format!("{}/{}", eff_item, task_ref),
task_code: format!("{}/{}", eff_item, trans.alloc_task_code().await?),
next_scheduled: Utc::now() + chrono::Duration::seconds(l[0].delay as i64),
..Default::default()
},
@ -319,3 +331,121 @@ pub async fn run_effects(
Ok(())
}
pub fn default_effects_for_type() -> &'static BTreeMap<EffectType, EffectSet> {
static MAP: OnceCell<BTreeMap<EffectType, EffectSet>> = OnceCell::new();
MAP.get_or_init(|| {
vec![(
EffectType::Bleed,
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(true, 1, false),
if target.species == SpeciesType::Robot { "leak coolant" } else { "bleed" }
),
format!(ansi!("<red>You notice that {} has a fresh gaping wound that looks like it's about to {}!<reset>\n"),
target.display_for_sentence(false, 1, false),
if target.species == SpeciesType::Robot { "leak coolant" } else { "bleed" }
)))
},
Effect::ChangeTargetHealth {
delay_secs: 10,
base_effect: -12,
skill_multiplier: 0.0,
max_effect: -12,
message: Box::new(|target| (
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(true, 1, false),
),
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(false, 1, false),
)))
},
Effect::ChangeTargetHealth {
delay_secs: 20,
base_effect: -10,
skill_multiplier: 0.0,
max_effect: -10,
message: Box::new(|target| (
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(true, 1, false),
),
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(false, 1, false),
)))
},
Effect::ChangeTargetHealth {
delay_secs: 30,
base_effect: -8,
skill_multiplier: 0.0,
max_effect: -8,
message: Box::new(|target| (
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(true, 1, false),
),
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(false, 1, false),
)))
},
Effect::ChangeTargetHealth {
delay_secs: 40,
base_effect: -6,
skill_multiplier: 0.0,
max_effect: -6,
message: Box::new(|target| (
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(true, 1, false),
),
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(false, 1, false),
)))
},
Effect::ChangeTargetHealth {
delay_secs: 50,
base_effect: -4,
skill_multiplier: 0.0,
max_effect: -4,
message: Box::new(|target| (
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(true, 1, false),
),
format!("{} pulses from {}'s wound",
if target.species == SpeciesType::Robot { "Coolant" } else { "Blood" },
target.display_for_sentence(false, 1, false),
)))
},
Effect::ChangeTargetHealth {
delay_secs: 60,
base_effect: -2,
skill_multiplier: 0.0,
max_effect: -2,
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(true, 1, false),
),
format!("A final tiny drop of {} oozes from {}'s wound as it clots",
if target.species == SpeciesType::Robot { "coolant" } else { "blood" },
target.display_for_sentence(false, 1, false),
)))
},
],
},
)]
.into_iter()
.collect()
})
}

View File

@ -4,7 +4,7 @@ use crate::{
message_handler::user_commands::{UResult, VerbContext},
models::{
consent::ConsentType,
effect::EffectSet,
effect::{EffectSet, EffectType},
item::{Item, ItemFlag, LiquidType, Pronouns, SkillType},
},
regular_tasks::queued_command::QueuedCommandContext,
@ -82,6 +82,7 @@ pub struct WeaponAttackData {
pub stdev_damage: f64,
pub base_damage_type: DamageType,
pub other_damage_types: Vec<(f64, DamageType)>, // Allocation fractions.
pub crit_effects: Vec<(f64, EffectType)>, // Probability, add up to <1 unless one guaranteed.
pub skill_scaling: Vec<SkillScaling>,
}
@ -108,6 +109,7 @@ impl Default for WeaponAttackData {
stdev_damage: 2.0,
base_damage_type: DamageType::Slash,
other_damage_types: vec![],
crit_effects: vec![],
skill_scaling: vec![],
}
}
@ -170,7 +172,6 @@ pub struct UseData {
pub fail_effects: Option<EffectSet>,
pub success_effects: Option<EffectSet>,
pub errorf: Box<dyn Fn(&Item, &Item) -> Option<String> + Sync + Send>,
pub task_ref: &'static str,
pub needs_consent_check: Option<ConsentType>,
}
@ -183,7 +184,6 @@ impl Default for UseData {
fail_effects: None,
success_effects: None,
errorf: Box::new(|_it, _target| None),
task_ref: "set me",
needs_consent_check: None,
}
}

View File

@ -238,14 +238,13 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
}
),
}),
task_ref: "bandage",
errorf: Box::new(
|item, target|
if target.death_data.is_some() {
Some(format!("It is too late, {}'s dead", target.pronouns.subject))
} else if target.item_type != "player" && target.item_type != "npc" {
Some("It only works on animals.".to_owned())
} else if target.active_effects.contains(&EffectType::Bandages) {
} else if target.active_effects.iter().any(|e| e.0 == EffectType::Bandages) {
Some(format!(
"You see no reason to use {} on {}",
item.display_for_sentence(false, 1, false),

View File

@ -1,5 +1,5 @@
use super::{DamageType, PossessionData, PossessionType, WeaponAttackData, WeaponData};
use crate::models::item::SkillType;
use crate::models::{effect::EffectType, item::SkillType};
use once_cell::sync::OnceCell;
pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
@ -76,6 +76,7 @@ pub fn data() -> &'static Vec<(PossessionType, PossessionData)> {
mean_damage: 4.0,
stdev_damage: 2.0,
base_damage_type: DamageType::Beat,
crit_effects: vec![(0.05, EffectType::Bleed)],
other_damage_types: vec!((0.25, DamageType::Slash)),
..Default::default()
},

View File

@ -168,9 +168,8 @@ impl TaskHandler for SeePatientTaskHandler {
},
&mut who_mut,
&who,
&mut None,
None,
0.0,
"bandages",
)
.await?;
ctx.trans.save_item_model(&who_mut).await?;

View File

@ -70,6 +70,8 @@ CREATE UNIQUE INDEX tasks_by_code_type ON tasks((details->>'task_code'), (detail
CREATE INDEX tasks_by_static ON tasks((cast(details->>'is_static' as boolean)));
CREATE INDEX tasks_by_scheduled ON tasks((details->>'next_scheduled'));
CREATE SEQUENCE task_seq;
CREATE TABLE corps (
corp_id BIGSERIAL NOT NULL PRIMARY KEY,
details JSONB NOT NULL