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
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"))]

View file

@ -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<CurrentTarget>, Without<Current>),
>,
(armour_q, damage_q, mut mitigation_q): (
(armour_q, damage_q, mut mitigation_q, bypass_q): (
Query<&ArmourBodyPart>,
Query<(Entity, &DeferredDamage)>,
Query<Option<&mut DamageMitigationBonus>>,
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));
}

View file

@ -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:?}"),
}
}

View file

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