diff --git a/models/src/bundle/bonus.rs b/models/src/bundle/bonus.rs index 1d3f344..a7793e3 100644 --- a/models/src/bundle/bonus.rs +++ b/models/src/bundle/bonus.rs @@ -62,6 +62,10 @@ pub enum WeaponBonusType { // Attack nullification types Homerun, Parry, + + // Armour mitigation + Puncture, + Penetrate, } #[derive(Component)] @@ -183,6 +187,12 @@ impl BonusPartDamageBonus { } } +#[derive(Debug, Clone, Component)] +pub enum ArmourBypassBonus { + Puncture { chance: f32 }, + Penetrate { mitigation: f32 }, +} + #[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/src/player/mod.rs b/src/player/mod.rs index b7e46ea..a6d8bba 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,7 +1,7 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ armour::{ArmourBodyPart, ArmourBodyParts}, - bonus::DamageMitigationBonus, + bonus::{ArmourBypassBonus, DamageMitigationBonus}, player::{ Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender, FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons, @@ -188,10 +188,11 @@ pub fn use_damaging_weapon( ), (With, Without), >, - (armour_q, damage_q, mut mitigation_q): ( + (armour_q, damage_q, mut mitigation_q, bypass_q): ( Query<&ArmourBodyPart>, Query<(Entity, &DeferredDamage)>, Query>, + Query<(&ArmourBypassBonus)>, ), (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), (mut ammo_q, mut temp_q): ( @@ -368,8 +369,8 @@ pub fn use_damaging_weapon( metrics.increment_counter(Some(weapon), "hit", 1); let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap(); - let piece = rng.sample(armour_parts); - let armour_mitigation = piece.map_or(0.0, |p| p.armour_value); + let mut piece = rng.sample(armour_parts); + let mut armour_mitigation = piece.map_or(0.0, |p| p.armour_value); // NOTE: Proxima's simulator seems to have the damage spread be between 95% and 105%, // but from my brief tests it seems that 100% to 110% lines up better, at least for h2h. @@ -384,66 +385,83 @@ pub fn use_damaging_weapon( } } - let bonus_mitigation = match piece { - Some(piece) => { - if let Some(mut mitigation) = mitigation_q.get_mut(piece.armour).unwrap() { - match mitigation.as_mut() { - DamageMitigationBonus::Impregnable { mitigation } - if *slot == WeaponSlot::Melee => - { - *mitigation - } - DamageMitigationBonus::Impenetrable { mitigation } - if rounds.is_some() => - { - *mitigation - } - DamageMitigationBonus::Insurmountable { mitigation } => { - if health.0 as f32 / target_max_health.value as f32 <= 0.25 { - *mitigation - } else { - 0.0 + let bonus_mitigation = 'block: { + match piece { + Some(part) => { + for bypass in bypass_q.iter_many(children) { + match bypass { + ArmourBypassBonus::Penetrate { mitigation } => { + armour_mitigation *= 1.0 - mitigation; + } + ArmourBypassBonus::Puncture { chance } => { + if *chance >= 1.0 || rng.random_bool(*chance as f64) { + armour_mitigation = 0.0; + piece = None; + break 'block 0.0; + } } } - DamageMitigationBonus::Impassable { chance } => { - if *chance >= 1.0 || rng.random_bool(*chance as f64) { - 1.0 - } else { - 0.0 - } - } - DamageMitigationBonus::Kinetokinesis { mitigation } => { - commands.entity(piece.armour).insert( - DamageMitigationBonus::ActiveKinetokinesis { - mitigation: *mitigation, - remaining_turns: 10, - }, - ); - 0.0 - } - DamageMitigationBonus::ActiveKinetokinesis { - mitigation, - remaining_turns, - } => { - *remaining_turns -= 1; + } - if *remaining_turns == 0 { - commands.entity(piece.armour).insert( - DamageMitigationBonus::Kinetokinesis { + if let Some(mut mitigation) = mitigation_q.get_mut(part.armour).unwrap() { + match mitigation.as_mut() { + DamageMitigationBonus::Impregnable { mitigation } + if *slot == WeaponSlot::Melee => + { + *mitigation + } + DamageMitigationBonus::Impenetrable { mitigation } + if rounds.is_some() => + { + *mitigation + } + DamageMitigationBonus::Insurmountable { mitigation } => { + if health.0 as f32 / target_max_health.value as f32 <= 0.25 { + *mitigation + } else { + 0.0 + } + } + DamageMitigationBonus::Impassable { chance } => { + if *chance >= 1.0 || rng.random_bool(*chance as f64) { + 1.0 + } else { + 0.0 + } + } + DamageMitigationBonus::Kinetokinesis { mitigation } => { + commands.entity(part.armour).insert( + DamageMitigationBonus::ActiveKinetokinesis { mitigation: *mitigation, + remaining_turns: 10, }, ); + 0.0 } + DamageMitigationBonus::ActiveKinetokinesis { + mitigation, + remaining_turns, + } => { + *remaining_turns -= 1; - *mitigation + if *remaining_turns == 0 { + commands.entity(part.armour).insert( + DamageMitigationBonus::Kinetokinesis { + mitigation: *mitigation, + }, + ); + } + + *mitigation + } + _ => 0.0, } - _ => 0.0, + } else { + 0.0 } - } else { - 0.0 } + None => 0.0, } - None => 0.0, }; // TODO: special ammo @@ -503,7 +521,7 @@ pub fn use_damaging_weapon( bonus.spawn(target, &mut effects, &mut rng.0); } } - DamageProcEffect::DamageOverTimer { value, kind } => { + DamageProcEffect::DamageOverTime { value, kind } => { let chance = (value / 100.0) as f64; if chance > 1.0 || rng.random_bool(chance) { match kind { @@ -562,8 +580,8 @@ pub fn use_damaging_weapon( } } - // Technically only douple tap and blindfire have this condition, but we can run into with - // invalid bonus/weapon combinations without checking this for all bonuses + // Technically only douple tap and blindfire have this condition, but we can run into + // panics with invalid bonus/weapon combinations without checking this for all bonuses if ammo.as_ref().is_some_and(|(a, _, _)| a.0 == 0) { break; } @@ -679,5 +697,7 @@ pub(crate) fn configure(stages: &mut Stages) { .before(change_roles), ); stages.post_fight.add_systems(record_post_fight_stats); - stages.restore.add_systems(restore_initial_state); + stages + .restore + .add_systems((restore_initial_state, restore_health)); } diff --git a/src/weapon/bonus.rs b/src/weapon/bonus.rs index 5d3642f..d4a4361 100644 --- a/src/weapon/bonus.rs +++ b/src/weapon/bonus.rs @@ -1,6 +1,6 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ - bonus::{BonusPartDamageBonus, BonusValue, WeaponBonusType}, + bonus::{ArmourBypassBonus, BonusPartDamageBonus, BonusValue, WeaponBonusType}, player::PartDamageBonus, stat::{ AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, @@ -431,12 +431,27 @@ pub(crate) fn prepare_bonuses( WeaponBonusType::Bleed => { commands .entity(weapon.parent()) - .with_child(DamageProcEffect::DamageOverTimer { + .with_child(DamageProcEffect::DamageOverTime { value: value.0, kind: DamageOverTimeType::Bleed, }); } + WeaponBonusType::Puncture => { + commands + .entity(weapon.parent()) + .with_child(ArmourBypassBonus::Puncture { + chance: value.0 / 100.0, + }); + } + WeaponBonusType::Penetrate => { + commands + .entity(weapon.parent()) + .with_child(ArmourBypassBonus::Penetrate { + mitigation: value.0 / 100.0, + }); + } + val => unimplemented!("{val:?}"), } } diff --git a/src/weapon/mod.rs b/src/weapon/mod.rs index 66a85cf..6b2665e 100644 --- a/src/weapon/mod.rs +++ b/src/weapon/mod.rs @@ -153,7 +153,7 @@ pub enum DamageProcEffect { value: f32, bonus: SelfStatusEffect, }, - DamageOverTimer { + DamageOverTime { value: f32, kind: DamageOverTimeType, },