diff --git a/models/src/bundle/bonus.rs b/models/src/bundle/bonus.rs index 8a290ae..86678d9 100644 --- a/models/src/bundle/bonus.rs +++ b/models/src/bundle/bonus.rs @@ -70,6 +70,10 @@ pub enum WeaponBonusType { // Misc Execute, Deadly, + Bloodlust, + DoubleEdged, + Blindside, + Comeback, } #[derive(Component)] @@ -197,6 +201,14 @@ pub enum ArmourBypassBonus { Penetrate { mitigation: f32 }, } +#[derive(Debug, Clone, Component)] +pub enum MiscBonus { + Blindside { bonus: f32 }, + Comeback { bonus: f32 }, + Deadly { chance: f64 }, + DoubleEdged { chance: f64 }, +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Component, Display)] #[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "json", serde(rename_all = "snake_case"))] diff --git a/models/src/bundle/player.rs b/models/src/bundle/player.rs index 871d8b2..47f41e2 100644 --- a/models/src/bundle/player.rs +++ b/models/src/bundle/player.rs @@ -132,10 +132,11 @@ pub enum PartDamageBonus { } #[derive(Message)] -pub enum HealthRestore { +pub enum HealthChange { Cauterise, - Serotonin { extra_effectiveness: f32 }, + Serotonin, Bloodlust { dmg: u32, value: f32 }, + DoubleEdged { dmg: u32 }, } impl PartDamageBonus { diff --git a/src/player/mod.rs b/src/player/mod.rs index 5a32fdf..976202b 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,10 +1,10 @@ use bevy_ecs::{prelude::*, system::SystemParam}; use proxisim_models::bundle::{ armour::{ArmourBodyPart, ArmourBodyParts, BodyPartCoverage}, - bonus::{ArmourBypassBonus, DamageMitigationBonus}, + bonus::{ArmourBypassBonus, DamageMitigationBonus, MiscBonus}, player::{ Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender, - FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons, + FightEndType, Health, HealthChange, PartDamageBonus, Player, PlayerStrategy, Weapons, }, stat::{ AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth, @@ -23,7 +23,10 @@ use crate::{ log, log::Logger, metrics::Metrics, - player::status_effect::{Bleed, DamageOverTime, DamageOverTimeType, DeferredDamage}, + player::status_effect::{ + Bleed, DamageOverTime, DamageOverTimeType, DeferredDamage, ExtraStatusEffectEffectiveness, + Hardened, + }, weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus}, }; @@ -243,6 +246,57 @@ fn check_bonus_mitigation( } } +#[derive(SystemParam)] +struct DamageBonusParams<'w, 's> { + misc_bonus_q: Query<'w, 's, &'static MiscBonus>, + part_bonus_q: Query<'w, 's, &'static PartDamageBonus>, +} + +/// Process miscellaneous bonuses that did not fall in any of the other categories +#[allow(clippy::too_many_arguments)] +#[inline(always)] +fn check_damage_bonuses( + queries: &DamageBonusParams, + shared: &mut SharedParams, + p_children: &Children, + t_health: &Health, + p_health: &Health, + t_max_health: &SimpleStatEffective, + p_max_health: &SimpleStatEffective, + part: BodyPart, + dmg_bonus: &mut SimpleStatEffective, + double_edged: &mut bool, +) { + for bonus in queries.misc_bonus_q.iter_many(p_children) { + match bonus { + MiscBonus::Blindside { bonus } if t_health.0 == t_max_health.value => { + dmg_bonus.value += bonus; + } + MiscBonus::Comeback { bonus } + if p_health.0 as f32 / p_max_health.value as f32 <= 0.25 => + { + dmg_bonus.value += bonus; + } + MiscBonus::Deadly { chance } if *chance >= 1.0 || shared.rng.random_bool(*chance) => { + dmg_bonus.value += 5.0; + } + MiscBonus::DoubleEdged { chance } + if *chance >= 1.0 || shared.rng.random_bool(*chance) => + { + dmg_bonus.value += 1.0; + *double_edged = true; + } + _ => (), + } + } + + for bonus in queries.part_bonus_q.iter_many(p_children) { + if let Some(bonus) = bonus.dmg_bonus(part) { + dmg_bonus.value += bonus; + } + } +} + #[derive(SystemParam)] struct DeferredDamageParams<'w, 's> { damage_q: Query<'w, 's, (Entity, &'static DeferredDamage)>, @@ -323,6 +377,7 @@ struct EntityParams<'w, 's> { &'static SimpleStatEffective, &'static SimpleStatEffective, &'static SimpleStatEffective, + &'static SimpleStatEffective, &'static Children, &'static mut Health, Has, @@ -350,14 +405,15 @@ struct EntityParams<'w, 's> { // of multi turn bonuses #[allow(clippy::too_many_arguments)] fn use_damaging_weapon( - (mut shared, mut bonus_mitigation, deferred): ( + (mut shared, mut bonus_mitigation, deferred, misc_bonus): ( SharedParams, BonusMitigationParams, DeferredDamageParams, + DamageBonusParams, ), mut entities: EntityParams, armour_q: Query<&ArmourBodyPart>, - (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), + damage_proc_q: Query<&DamageProcEffect>, (mut ammo_q, mut temp_q): ( Query<( &mut Ammo, @@ -382,6 +438,7 @@ fn use_damaging_weapon( player_crit, acc_bonus, p_dmg_bonus, + p_max_health, p_children, mut p_health, attacker, @@ -528,12 +585,6 @@ fn use_damaging_weapon( let mut dmg_bonus = dmg_bonus + p_dmg_bonus; - for part_bonus in part_bonus_q.iter_many(children) { - if let Some(bonus) = part_bonus.dmg_bonus(body_part) { - dmg_bonus.value += bonus; - } - } - let bonus_mitigation = check_bonus_mitigation( &mut bonus_mitigation, &mut shared, @@ -546,32 +597,46 @@ fn use_damaging_weapon( target_max_health, ); - let spammo_bonus = ammo - .as_ref() - .map(|(_, _, _, kind)| match kind { - AmmoType::HollowPoint if piece.is_none() => { - armour_mitigation *= 1.5; - 0.0 - } - AmmoType::HollowPoint => 0.50, - AmmoType::Incindiary => 0.40, - AmmoType::Piercer => { - armour_mitigation *= 0.5; - 0.0 - } - _ => 0.0, - }) - .unwrap_or_default(); + if let Some((_, _, _, kind)) = ammo { + match kind { + AmmoType::HollowPoint if piece.is_none() => armour_mitigation *= 1.5, + AmmoType::HollowPoint => dmg_bonus.value += 0.50, + AmmoType::Incindiary => dmg_bonus.value += 0.40, + AmmoType::Piercer => armour_mitigation *= 0.5, + _ => (), + } + } - let mut dmg = dmg_intrinsic + let mut double_edged = false; + + check_damage_bonuses( + &misc_bonus, + &mut shared, + children, + &health, + &p_health, + target_max_health, + p_max_health, + body_part, + &mut dmg_bonus, + &mut double_edged, + ); + + let dmg = dmg_intrinsic * w_dmg.0 - * (1.0 + dmg_bonus.value + spammo_bonus) + * (1.0 + dmg_bonus.value) * (1.0 - armour_mitigation) * (1.0 - def_mitigation) * (1.0 - bonus_mitigation) * mult * dmg_spread; - let mut dmg_i = dmg.round() as u32; + let dmg_i = dmg.round() as u32; + + if double_edged { + shared + .commands + .write_message(HealthChange::DoubleEdged { dmg: dmg_i }); + } shared.metrics.record_histogram(Some(player), "dmg", dmg_i); shared.metrics.record_histogram(Some(weapon), "dmg", dmg_i); @@ -584,7 +649,7 @@ fn use_damaging_weapon( continue; } let chance = (value / 100.0) as f64; - if chance > 1.0 || shared.rng.random_bool(chance) { + if chance >= 1.0 || shared.rng.random_bool(chance) { match bonus { MultiTurnBonus::Blindfire => { multi_attack_proc = Some(MultiAttack::Blindfire) @@ -640,19 +705,6 @@ fn use_damaging_weapon( } } } - DamageProcEffect::Deadly { chance } => { - if chance >= 1.0 || shared.rng.random_bool(chance as f64) { - dmg = dmg_intrinsic - * w_dmg.0 - * (1.0 + dmg_bonus.value + spammo_bonus + 5.0) - * (1.0 - armour_mitigation) - * (1.0 - def_mitigation) - * (1.0 - bonus_mitigation) - * mult - * dmg_spread; - dmg_i = dmg.round() as u32; - } - } DamageProcEffect::Execute { cutoff } => { if health.0 as f32 / target_max_health.value as f32 <= cutoff { shared.commands.entity(target).insert(Defeated); @@ -676,6 +728,12 @@ fn use_damaging_weapon( return; } } + DamageProcEffect::Bloodlust { ratio } => { + shared.commands.write_message(HealthChange::Bloodlust { + dmg: dmg_i, + value: ratio, + }); + } } } } @@ -822,18 +880,69 @@ fn restore_health( } } +fn process_health_changes( + mut messages: MessageReader, + mut logger: Logger, + mut p_query: Query< + ( + Entity, + &SimpleStatEffective, + &mut Health, + Option<&ExtraStatusEffectEffectiveness<1, Hardened>>, + ), + (With, With), + >, +) { + for message in messages.read() { + let (player, max_health, mut health, extra_effectiveness) = p_query.single_mut().unwrap(); + let (source, delta) = match message { + HealthChange::Cauterise => { + let delta = (max_health.value as f32 * 0.2) as i16; + ("Cauterise", delta) + } + HealthChange::Serotonin => { + let delta = (max_health.value as f32 + * 0.25 + * extra_effectiveness.map_or(1.0, |e| e.factor)) + as i16; + ("Serotonin", delta) + } + HealthChange::Bloodlust { dmg, value } => { + let delta = (*dmg as f32 * *value) as i16; + ("Bloodlust", delta) + } + HealthChange::DoubleEdged { dmg } => { + let delta = -(*dmg as f32 * 0.25) as i16; + ("Double-edged", delta) + } + }; + + let health_before = health.0; + health.0 = (health.0 as i16 + delta).clamp(1, max_health.value as i16) as u16; + + log!(logger, "health_change", { + recipient: player, + source, + delta, + health_before, + health_after: health.0 + }) + } +} + pub(crate) fn configure(stages: &mut Stages) { stats::configure(stages); status_effect::configure(stages); stages.add_event::(); + stages.add_event::(); stages.equip.add_systems(designate_first); stages.pre_fight.add_systems(restore_health); stages.pre_turn.add_systems(pick_action); stages.turn.add_systems(use_damaging_weapon); stages .post_turn - .add_systems((check_term_condition, change_roles)) + .add_systems((check_term_condition, change_roles, process_health_changes)) .add_systems( check_stalemate .after(check_term_condition) diff --git a/src/player/status_effect.rs b/src/player/status_effect.rs index 52d3b16..1f240de 100644 --- a/src/player/status_effect.rs +++ b/src/player/status_effect.rs @@ -154,8 +154,6 @@ pub trait AdditiveStatusEffectMarker: Send + Sync + 'static { fn duration() -> f32; } -// TODO: instead of tracking it in the status effect itself, add generic -// `StatusEffectEffectiveness` and `StatusEffectExtraDuration` components #[derive(Component)] pub struct AdditiveStatusEffect where diff --git a/src/weapon/bonus.rs b/src/weapon/bonus.rs index 6a466ce..c7d2bec 100644 --- a/src/weapon/bonus.rs +++ b/src/weapon/bonus.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ - bonus::{ArmourBypassBonus, BonusPartDamageBonus, BonusValue, WeaponBonusType}, + bonus::{ArmourBypassBonus, BonusPartDamageBonus, BonusValue, MiscBonus, WeaponBonusType}, player::PartDamageBonus, stat::{ AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, @@ -455,8 +455,8 @@ pub(crate) fn prepare_bonuses( WeaponBonusType::Deadly => { commands .entity(weapon.parent()) - .with_child(DamageProcEffect::Deadly { - chance: value.0 / 100.0, + .with_child(MiscBonus::Deadly { + chance: (value.0 / 100.0) as f64, }); } WeaponBonusType::Execute => { @@ -467,6 +467,34 @@ pub(crate) fn prepare_bonuses( }); } + WeaponBonusType::Bloodlust => { + commands + .entity(weapon.parent()) + .with_child(DamageProcEffect::Bloodlust { + ratio: value.0 / 100.0, + }); + } + WeaponBonusType::Comeback => { + commands + .entity(weapon.parent()) + .with_child(MiscBonus::Comeback { + bonus: value.0 / 100.0, + }); + } + WeaponBonusType::Blindside => { + commands + .entity(weapon.parent()) + .with_child(MiscBonus::Blindside { + bonus: value.0 / 100.0, + }); + } + WeaponBonusType::DoubleEdged => { + commands + .entity(weapon.parent()) + .with_child(MiscBonus::DoubleEdged { + chance: value.0 as f64 / 100.0, + }); + } val => unimplemented!("{val:?}"), } } diff --git a/src/weapon/mod.rs b/src/weapon/mod.rs index 7530881..28ef2ce 100644 --- a/src/weapon/mod.rs +++ b/src/weapon/mod.rs @@ -162,8 +162,8 @@ pub enum DamageProcEffect { Execute { cutoff: f32, }, - Deadly { - chance: f32, + Bloodlust { + ratio: f32, }, } diff --git a/src/weapon/temp.rs b/src/weapon/temp.rs index 3ad7ace..011996e 100644 --- a/src/weapon/temp.rs +++ b/src/weapon/temp.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ - player::{Current, CurrentTarget, Player}, + player::{Current, CurrentTarget, HealthChange, Player}, weapon::{BuffingTemp, DebuffingTemp, Usable, Uses}, }; @@ -76,11 +76,14 @@ fn use_buffing_temp( let current = current_q.single().unwrap(); match temp { - BuffingTemp::Serotonin => effects.spawn_and_insert( - AdditiveStatusEffect::<1, Hardened>::default(), - current, - AssociatedWeapon(weapon), - ), + BuffingTemp::Serotonin => { + commands.write_message(HealthChange::Serotonin); + effects.spawn_and_insert( + AdditiveStatusEffect::<1, Hardened>::default(), + current, + AssociatedWeapon(weapon), + ) + } BuffingTemp::Tyrosine => effects.spawn_and_insert( AdditiveStatusEffect::<1, Sharpened>::default(), current,