refactor: split up core attacking system

This commit is contained in:
TotallyNot 2025-11-04 19:15:08 +01:00
parent 1c7a5d1e1b
commit 9b720facf8
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15

View file

@ -1,6 +1,6 @@
use bevy_ecs::prelude::*; use bevy_ecs::{prelude::*, system::SystemParam};
use proxisim_models::bundle::{ use proxisim_models::bundle::{
armour::{ArmourBodyPart, ArmourBodyParts}, armour::{ArmourBodyPart, ArmourBodyParts, BodyPartCoverage},
bonus::{ArmourBypassBonus, DamageMitigationBonus}, bonus::{ArmourBypassBonus, DamageMitigationBonus},
player::{ player::{
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender, Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender,
@ -146,55 +146,217 @@ impl FromWorld for DamageSpread {
} }
} }
// NOTE: unfortunately this function can't really be split into smaller parts due to the existence #[derive(SystemParam)]
// of multi turn bonuses struct SharedParams<'w, 's> {
commands: Commands<'w, 's>,
rng: ResMut<'w, Rng>,
effects: Effects<'w, 's>,
logger: Logger<'w>,
metrics: Res<'w, Metrics>,
}
#[derive(SystemParam)]
struct BonusMitigationParams<'w, 's> {
bypass_q: Query<'w, 's, &'static ArmourBypassBonus>,
mitigation_q: Query<'w, 's, Option<&'static mut DamageMitigationBonus>>,
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn use_damaging_weapon( #[inline(always)]
mut rng: ResMut<Rng>, fn check_bonus_mitigation(
params: &mut BonusMitigationParams,
shared: &mut SharedParams,
w_children: &Children,
armour_mitigation: &mut f32,
piece: &mut Option<&BodyPartCoverage>,
slot: WeaponSlot,
rounds: &Option<u16>,
health: &Health,
max_health: &SimpleStatEffective<MaxHealth>,
) -> f32 {
for bypass in params.bypass_q.iter_many(w_children) {
match bypass {
ArmourBypassBonus::Penetrate { mitigation } => {
*armour_mitigation *= 1.0 - mitigation;
}
ArmourBypassBonus::Puncture { chance } => {
if *chance >= 1.0 || shared.rng.random_bool(*chance as f64) {
*armour_mitigation = 0.0;
*piece = None;
return 0.0;
}
}
}
}
if let Some(piece) = piece
&& let Some(mut mitigation) = params.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 / max_health.value as f32 <= 0.25 {
*mitigation
} else {
0.0
}
}
DamageMitigationBonus::Impassable { chance } => {
if *chance >= 1.0 || shared.rng.random_bool(*chance as f64) {
1.0
} else {
0.0
}
}
DamageMitigationBonus::Kinetokinesis { mitigation } => {
shared.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 {
shared.commands.entity(piece.armour).insert(
DamageMitigationBonus::Kinetokinesis {
mitigation: *mitigation,
},
);
}
*mitigation
}
_ => 0.0,
}
} else {
0.0
}
}
#[derive(SystemParam)]
struct DeferredDamageParams<'w, 's> {
damage_q: Query<'w, 's, (Entity, &'static DeferredDamage)>,
}
/// Checks and applied deferred damage.
/// Returns `true` if the attacking player was defeated
#[inline(always)]
fn check_deferred_damage(
deferred: &DeferredDamageParams,
shared: &mut SharedParams,
player: Entity,
player_children: &Children,
player_health: &mut Health,
is_attacker: bool,
target: Entity,
) -> bool {
for (instance, damage) in deferred.damage_q.iter_many(player_children) {
let health_before = player_health.0;
player_health.0 = player_health.0.saturating_sub(damage.amount);
log!(shared.logger, "deferred_damage", {
health_before: health_before,
health_after: player_health.0,
amount: damage.amount,
label: damage.label,
target: player,
});
shared.commands.entity(instance).despawn();
if player_health.0 == 0 {
shared.commands.entity(player).insert(Defeated);
log!(shared.logger, "fight_end", {
actor: target,
recipient: player,
fight_end_type: %if is_attacker {
FightEndType::Loss
} else {
FightEndType::Victory
},
});
shared.metrics.increment_counter(Some(target), "victory", 1);
return true;
}
}
false
}
#[derive(SystemParam)]
struct EntityParams<'w, 's> {
/// The weapon and its related stats
weapon_q: Query< weapon_q: Query<
'w,
's,
( (
Entity, Entity,
&DamageStat, &'static DamageStat,
&SimpleStatEffective<WeaponAccuracy>, &'static SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>, &'static SimpleStatEffective<DamageBonus>,
&SimpleStatEffective<CritRate>, &'static SimpleStatEffective<CritRate>,
&Children, &'static Children,
&WeaponSlot, &'static WeaponSlot,
Has<NonTargeted>, Has<NonTargeted>,
), ),
(With<Weapon>, With<Current>, Without<NeedsReload>), (With<Weapon>, With<Current>, Without<NeedsReload>),
>, >,
mut player_q: Query<
/// The player who's using the weapon and their related stats
player_q: Query<
'w,
's,
( (
Entity, Entity,
&EffectiveStat<Speed>, &'static EffectiveStat<Speed>,
&EffectiveStat<Strength>, &'static EffectiveStat<Strength>,
&SimpleStatEffective<CritRate>, &'static SimpleStatEffective<CritRate>,
&SimpleStatEffective<WeaponAccuracy>, &'static SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>, &'static SimpleStatEffective<DamageBonus>,
&Children, &'static Children,
&mut Health, &'static mut Health,
Has<Attacker>, Has<Attacker>,
), ),
(With<Player>, With<Current>), (With<Player>, With<Current>),
>, >,
mut target_q: Query<
/// The player who is targeted with the weapon
target_q: Query<
'w,
's,
( (
Entity, Entity,
&EffectiveStat<Dexterity>, &'static EffectiveStat<Dexterity>,
&EffectiveStat<Defence>, &'static EffectiveStat<Defence>,
&SimpleStatEffective<MaxHealth>, &'static SimpleStatEffective<MaxHealth>,
&ArmourBodyParts, &'static ArmourBodyParts,
&mut Health, &'static mut Health,
), ),
(With<CurrentTarget>, Without<Current>), (With<CurrentTarget>, Without<Current>),
>, >,
(armour_q, damage_q, mut mitigation_q, bypass_q): ( }
Query<&ArmourBodyPart>,
Query<(Entity, &DeferredDamage)>, // NOTE: unfortunately this function can't really be split into smaller parts due to the existence
Query<Option<&mut DamageMitigationBonus>>, // of multi turn bonuses
Query<&ArmourBypassBonus>, #[allow(clippy::too_many_arguments)]
fn use_damaging_weapon(
(mut shared, mut bonus_mitigation, deferred): (
SharedParams,
BonusMitigationParams,
DeferredDamageParams,
), ),
mut entities: EntityParams,
armour_q: Query<&ArmourBodyPart>,
(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): (
Query<( Query<(
@ -206,15 +368,10 @@ pub fn use_damaging_weapon(
)>, )>,
Query<&mut Uses>, Query<&mut Uses>,
), ),
(mut logger, mut commands, dmg_spread, metrics, mut effects): ( dmg_spread: Local<DamageSpread>,
Logger,
Commands,
Local<DamageSpread>,
Res<Metrics>,
Effects,
),
) { ) {
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, slot, non_targeted)) = weapon_q.single() let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, slot, non_targeted)) =
entities.weapon_q.single()
else { else {
return; return;
}; };
@ -228,44 +385,26 @@ pub fn use_damaging_weapon(
p_children, p_children,
mut p_health, mut p_health,
attacker, attacker,
) = player_q.single_mut().unwrap(); ) = entities.player_q.single_mut().unwrap();
let (target, target_dex, target_def, target_max_health, armour_parts, mut health) = let (target, target_dex, target_def, target_max_health, armour_parts, mut health) =
target_q.single_mut().unwrap(); entities.target_q.single_mut().unwrap();
for (instance, damage) in damage_q.iter_many(p_children) { if check_deferred_damage(
let health_before = p_health.0; &deferred,
p_health.0 = p_health.0.saturating_sub(damage.amount); &mut shared,
player,
log!(logger, "deferred_damage", { p_children,
health_before: health_before, &mut p_health,
health_after: p_health.0, attacker,
amount: damage.amount, target,
label: damage.label, ) {
target: player,
});
commands.entity(instance).despawn();
if p_health.0 == 0 {
commands.entity(player).insert(Defeated);
log!(logger, "fight_end", {
actor: target,
recipient: player,
fight_end_type: %if attacker {
FightEndType::Loss
} else {
FightEndType::Victory
},
});
metrics.increment_counter(Some(target), "victory", 1);
return; return;
} }
}
if let Ok(mut uses) = temp_q.get_mut(weapon) { if let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1; uses.0 -= 1;
if uses.0 == 0 { if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>(); shared.commands.entity(weapon).remove::<Usable>();
} }
} }
@ -314,15 +453,19 @@ pub fn use_damaging_weapon(
loop { loop {
let rounds = ammo.as_mut().map(|(ammo, clips, rof, _)| { let rounds = ammo.as_mut().map(|(ammo, clips, rof, _)| {
let rounds = (rng.random_range(rof.clone()).round() as u16).clamp(1, ammo.0); let rounds = (shared.rng.random_range(rof.clone()).round() as u16).clamp(1, ammo.0);
metrics.increment_counter(Some(player), "rounds_fired", rounds.into()); shared
metrics.increment_counter(Some(weapon), "rounds_fired", rounds.into()); .metrics
.increment_counter(Some(player), "rounds_fired", rounds.into());
shared
.metrics
.increment_counter(Some(weapon), "rounds_fired", rounds.into());
ammo.0 -= rounds; ammo.0 -= rounds;
if ammo.0 == 0 { if ammo.0 == 0 {
if clips.value == 0 { if clips.value == 0 {
commands.entity(weapon).remove::<Usable>(); shared.commands.entity(weapon).remove::<Usable>();
} else { } else {
commands.entity(weapon).insert(NeedsReload); shared.commands.entity(weapon).insert(NeedsReload);
} }
} }
rounds rounds
@ -334,31 +477,31 @@ pub fn use_damaging_weapon(
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance) base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
}; };
if hit_chance <= 1.0 && !rng.random_bool(hit_chance as f64) { if hit_chance <= 1.0 && !shared.rng.random_bool(hit_chance as f64) {
log!(logger, "miss_target", { log!(shared.logger, "miss_target", {
weapon: weapon, weapon: weapon,
actor: player, actor: player,
recipient: target, recipient: target,
rounds: rounds, rounds: rounds,
hit_chance: hit_chance, hit_chance: hit_chance,
}); });
metrics.increment_counter(Some(player), "miss", 1); shared.metrics.increment_counter(Some(player), "miss", 1);
metrics.increment_counter(Some(weapon), "miss", 1); shared.metrics.increment_counter(Some(weapon), "miss", 1);
if multi_attack_proc.is_none() { if multi_attack_proc.is_none() {
return; return;
}; };
} else { } else {
let body_part = if !non_targeted { let body_part = if !non_targeted {
rng.sample(crit) shared.rng.sample(crit)
} else { } else {
BodyPart::Stomach BodyPart::Stomach
}; };
let mult = match body_part { let mult = match body_part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => { BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
metrics.increment_counter(Some(player), "crit", 1); shared.metrics.increment_counter(Some(player), "crit", 1);
metrics.increment_counter(Some(weapon), "crit", 1); shared.metrics.increment_counter(Some(weapon), "crit", 1);
1.0 1.0
} }
BodyPart::LeftHand BodyPart::LeftHand
@ -371,17 +514,17 @@ pub fn use_damaging_weapon(
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75, BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
}; };
metrics.increment_counter(Some(player), "hit", 1); shared.metrics.increment_counter(Some(player), "hit", 1);
metrics.increment_counter(Some(weapon), "hit", 1); shared.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 mut piece = rng.sample(armour_parts); let mut piece = shared.rng.sample(armour_parts);
let mut 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.
// It might be better to revivisit this detail later down the line and run more tests. // It might be better to revivisit this detail later down the line and run more tests.
let dmg_spread = rng.sample(dmg_spread.0) / 10.0 + 1.0; let dmg_spread = shared.rng.sample(dmg_spread.0) / 10.0 + 1.0;
let mut dmg_bonus = dmg_bonus + p_dmg_bonus; let mut dmg_bonus = dmg_bonus + p_dmg_bonus;
@ -391,84 +534,17 @@ pub fn use_damaging_weapon(
} }
} }
let bonus_mitigation = 'block: { let bonus_mitigation = check_bonus_mitigation(
match piece { &mut bonus_mitigation,
Some(part) => { &mut shared,
for bypass in bypass_q.iter_many(children) { children,
match bypass { &mut armour_mitigation,
ArmourBypassBonus::Penetrate { mitigation } => { &mut piece,
armour_mitigation *= 1.0 - mitigation; *slot,
} &rounds,
ArmourBypassBonus::Puncture { chance } => { &health,
if *chance >= 1.0 || rng.random_bool(*chance as f64) { target_max_health,
armour_mitigation = 0.0;
piece = None;
break 'block 0.0;
}
}
}
}
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;
if *remaining_turns == 0 {
commands.entity(part.armour).insert(
DamageMitigationBonus::Kinetokinesis {
mitigation: *mitigation,
},
);
}
*mitigation
}
_ => 0.0,
}
} else {
0.0
}
}
None => 0.0,
}
};
let spammo_bonus = ammo let spammo_bonus = ammo
.as_ref() .as_ref()
@ -498,8 +574,8 @@ pub fn use_damaging_weapon(
* dmg_spread; * dmg_spread;
let mut dmg_i = dmg.round() as u32; let mut dmg_i = dmg.round() as u32;
metrics.record_histogram(Some(player), "dmg", dmg_i); shared.metrics.record_histogram(Some(player), "dmg", dmg_i);
metrics.record_histogram(Some(weapon), "dmg", dmg_i); shared.metrics.record_histogram(Some(weapon), "dmg", dmg_i);
if dmg_i > 0 { if dmg_i > 0 {
for effect in damage_proc_q.iter_many(children) { for effect in damage_proc_q.iter_many(children) {
@ -509,7 +585,7 @@ pub fn use_damaging_weapon(
continue; continue;
} }
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 || shared.rng.random_bool(chance) {
match bonus { match bonus {
MultiTurnBonus::Blindfire => { MultiTurnBonus::Blindfire => {
multi_attack_proc = Some(MultiAttack::Blindfire) multi_attack_proc = Some(MultiAttack::Blindfire)
@ -520,43 +596,53 @@ pub fn use_damaging_weapon(
} }
MultiTurnBonus::Rage => { MultiTurnBonus::Rage => {
multi_attack_proc = multi_attack_proc =
Some(MultiAttack::Rage(rng.random_range(2..=8))) Some(MultiAttack::Rage(shared.rng.random_range(2..=8)))
} }
MultiTurnBonus::DoubleTap => { MultiTurnBonus::DoubleTap => {
multi_attack_proc = multi_attack_proc =
Some(MultiAttack::DoubleTap { first_shot: true }) Some(MultiAttack::DoubleTap { first_shot: true })
} }
}; };
metrics.increment_counter(Some(player), bonus.counter_label(), 1); shared.metrics.increment_counter(
metrics.increment_counter(Some(weapon), bonus.counter_label(), 1); Some(player),
bonus.counter_label(),
1,
);
shared.metrics.increment_counter(
Some(weapon),
bonus.counter_label(),
1,
);
} }
} }
DamageProcEffect::SelfEffect { value, bonus } => { DamageProcEffect::SelfEffect { value, bonus } => {
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 || shared.rng.random_bool(chance) {
bonus.spawn(player, &mut effects); bonus.spawn(player, &mut shared.effects);
} }
} }
DamageProcEffect::OpponentEffect { value, bonus } => { DamageProcEffect::OpponentEffect { value, bonus } => {
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 || shared.rng.random_bool(chance) {
bonus.spawn(target, &mut effects, &mut rng.0); bonus.spawn(target, &mut shared.effects, &mut shared.rng.0);
} }
} }
DamageProcEffect::DamageOverTime { 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 || shared.rng.random_bool(chance) {
match kind { match kind {
DamageOverTimeType::Bleed => { DamageOverTimeType::Bleed => {
commands shared.commands.entity(target).insert(DamageOverTime::<
.entity(target) Bleed,
.insert(DamageOverTime::<Bleed>::new(dmg_i)); >::new(
dmg_i
));
} }
} }
} }
} }
DamageProcEffect::Deadly { chance } => { DamageProcEffect::Deadly { chance } => {
if chance >= 1.0 || rng.random_bool(chance as f64) { if chance >= 1.0 || shared.rng.random_bool(chance as f64) {
dmg = dmg_intrinsic dmg = dmg_intrinsic
* w_dmg.0 * w_dmg.0
* (1.0 + dmg_bonus.value + spammo_bonus + 5.0) * (1.0 + dmg_bonus.value + spammo_bonus + 5.0)
@ -570,15 +656,15 @@ pub fn use_damaging_weapon(
} }
DamageProcEffect::Execute { cutoff } => { DamageProcEffect::Execute { cutoff } => {
if health.0 as f32 / target_max_health.value as f32 <= cutoff { if health.0 as f32 / target_max_health.value as f32 <= cutoff {
commands.entity(target).insert(Defeated); shared.commands.entity(target).insert(Defeated);
let health_before = health.0; let health_before = health.0;
health.0 = 0; health.0 = 0;
log!(logger, "executed", { log!(shared.logger, "executed", {
actor: player, actor: player,
recipient: target, recipient: target,
health_before, health_before,
}); });
log!(logger, "fight_end", { log!(shared.logger, "fight_end", {
actor: player, actor: player,
recipient: target, recipient: target,
fight_end_type: %if attacker { fight_end_type: %if attacker {
@ -587,7 +673,7 @@ pub fn use_damaging_weapon(
FightEndType::Loss FightEndType::Loss
}, },
}); });
metrics.increment_counter(Some(player), "victory", 1); shared.metrics.increment_counter(Some(player), "victory", 1);
return; return;
} }
} }
@ -599,7 +685,7 @@ pub fn use_damaging_weapon(
health.0 = health.0.saturating_sub(dmg_i as u16); health.0 = health.0.saturating_sub(dmg_i as u16);
log!(logger, "hit_target", { log!(shared.logger, "hit_target", {
actor: player, actor: player,
acc: acc_eff.value, acc: acc_eff.value,
recipient: target, recipient: target,
@ -624,8 +710,8 @@ pub fn use_damaging_weapon(
if health.0 == 0 && !defeated { if health.0 == 0 && !defeated {
defeated = true; defeated = true;
commands.entity(target).insert(Defeated); shared.commands.entity(target).insert(Defeated);
log!(logger, "fight_end", { log!(shared.logger, "fight_end", {
actor: player, actor: player,
recipient: target, recipient: target,
fight_end_type: %if attacker { fight_end_type: %if attacker {
@ -634,7 +720,7 @@ pub fn use_damaging_weapon(
FightEndType::Loss FightEndType::Loss
}, },
}); });
metrics.increment_counter(Some(player), "victory", 1); shared.metrics.increment_counter(Some(player), "victory", 1);
} }
} }