feat: added puncture and penetrate

This commit is contained in:
TotallyNot 2025-11-04 16:14:49 +01:00
parent 451efd2bb7
commit 6c3c50689a
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
4 changed files with 104 additions and 59 deletions

View file

@ -62,6 +62,10 @@ pub enum WeaponBonusType {
// Attack nullification types // Attack nullification types
Homerun, Homerun,
Parry, Parry,
// Armour mitigation
Puncture,
Penetrate,
} }
#[derive(Component)] #[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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Component, Display)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]

View file

@ -1,7 +1,7 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
armour::{ArmourBodyPart, ArmourBodyParts}, armour::{ArmourBodyPart, ArmourBodyParts},
bonus::DamageMitigationBonus, bonus::{ArmourBypassBonus, DamageMitigationBonus},
player::{ player::{
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender, Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender,
FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons, FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons,
@ -188,10 +188,11 @@ pub fn use_damaging_weapon(
), ),
(With<CurrentTarget>, Without<Current>), (With<CurrentTarget>, Without<Current>),
>, >,
(armour_q, damage_q, mut mitigation_q): ( (armour_q, damage_q, mut mitigation_q, bypass_q): (
Query<&ArmourBodyPart>, Query<&ArmourBodyPart>,
Query<(Entity, &DeferredDamage)>, Query<(Entity, &DeferredDamage)>,
Query<Option<&mut DamageMitigationBonus>>, Query<Option<&mut DamageMitigationBonus>>,
Query<(&ArmourBypassBonus)>,
), ),
(damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>),
(mut ammo_q, mut temp_q): ( (mut ammo_q, mut temp_q): (
@ -368,8 +369,8 @@ pub fn use_damaging_weapon(
metrics.increment_counter(Some(weapon), "hit", 1); metrics.increment_counter(Some(weapon), "hit", 1);
let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap(); let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap();
let piece = rng.sample(armour_parts); let mut piece = rng.sample(armour_parts);
let armour_mitigation = piece.map_or(0.0, |p| p.armour_value); 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%, // 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. // 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 { let bonus_mitigation = 'block: {
Some(piece) => { match piece {
if let Some(mut mitigation) = mitigation_q.get_mut(piece.armour).unwrap() { Some(part) => {
match mitigation.as_mut() { for bypass in bypass_q.iter_many(children) {
DamageMitigationBonus::Impregnable { mitigation } match bypass {
if *slot == WeaponSlot::Melee => ArmourBypassBonus::Penetrate { mitigation } => {
{ armour_mitigation *= 1.0 - mitigation;
*mitigation }
} ArmourBypassBonus::Puncture { chance } => {
DamageMitigationBonus::Impenetrable { mitigation } if *chance >= 1.0 || rng.random_bool(*chance as f64) {
if rounds.is_some() => armour_mitigation = 0.0;
{ piece = None;
*mitigation break 'block 0.0;
} }
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(piece.armour).insert(
DamageMitigationBonus::ActiveKinetokinesis {
mitigation: *mitigation,
remaining_turns: 10,
},
);
0.0
}
DamageMitigationBonus::ActiveKinetokinesis {
mitigation,
remaining_turns,
} => {
*remaining_turns -= 1;
if *remaining_turns == 0 { if let Some(mut mitigation) = mitigation_q.get_mut(part.armour).unwrap() {
commands.entity(piece.armour).insert( match mitigation.as_mut() {
DamageMitigationBonus::Kinetokinesis { 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, 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 // TODO: special ammo
@ -503,7 +521,7 @@ pub fn use_damaging_weapon(
bonus.spawn(target, &mut effects, &mut rng.0); bonus.spawn(target, &mut effects, &mut rng.0);
} }
} }
DamageProcEffect::DamageOverTimer { value, kind } => { DamageProcEffect::DamageOverTime { value, kind } => {
let chance = (value / 100.0) as f64; let chance = (value / 100.0) as f64;
if chance > 1.0 || rng.random_bool(chance) { if chance > 1.0 || rng.random_bool(chance) {
match kind { 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 // Technically only douple tap and blindfire have this condition, but we can run into
// invalid bonus/weapon combinations without checking this for all bonuses // panics with invalid bonus/weapon combinations without checking this for all bonuses
if ammo.as_ref().is_some_and(|(a, _, _)| a.0 == 0) { if ammo.as_ref().is_some_and(|(a, _, _)| a.0 == 0) {
break; break;
} }
@ -679,5 +697,7 @@ pub(crate) fn configure(stages: &mut Stages) {
.before(change_roles), .before(change_roles),
); );
stages.post_fight.add_systems(record_post_fight_stats); 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));
} }

View file

@ -1,6 +1,6 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
bonus::{BonusPartDamageBonus, BonusValue, WeaponBonusType}, bonus::{ArmourBypassBonus, BonusPartDamageBonus, BonusValue, WeaponBonusType},
player::PartDamageBonus, player::PartDamageBonus,
stat::{ stat::{
AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus,
@ -431,12 +431,27 @@ pub(crate) fn prepare_bonuses(
WeaponBonusType::Bleed => { WeaponBonusType::Bleed => {
commands commands
.entity(weapon.parent()) .entity(weapon.parent())
.with_child(DamageProcEffect::DamageOverTimer { .with_child(DamageProcEffect::DamageOverTime {
value: value.0, value: value.0,
kind: DamageOverTimeType::Bleed, 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:?}"), val => unimplemented!("{val:?}"),
} }
} }

View file

@ -153,7 +153,7 @@ pub enum DamageProcEffect {
value: f32, value: f32,
bonus: SelfStatusEffect, bonus: SelfStatusEffect,
}, },
DamageOverTimer { DamageOverTime {
value: f32, value: f32,
kind: DamageOverTimeType, kind: DamageOverTimeType,
}, },