diff --git a/src/player/mod.rs b/src/player/mod.rs index b994d1e..563d1a5 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,6 +1,6 @@ -use bevy_ecs::prelude::*; +use bevy_ecs::{prelude::*, system::SystemParam}; use proxisim_models::bundle::{ - armour::{ArmourBodyPart, ArmourBodyParts}, + armour::{ArmourBodyPart, ArmourBodyParts, BodyPartCoverage}, bonus::{ArmourBypassBonus, DamageMitigationBonus}, player::{ 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 -// of multi turn bonuses +#[derive(SystemParam)] +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)] -pub fn use_damaging_weapon( - mut rng: ResMut, +#[inline(always)] +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, + health: &Health, + max_health: &SimpleStatEffective, +) -> 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< + 'w, + 's, ( Entity, - &DamageStat, - &SimpleStatEffective, - &SimpleStatEffective, - &SimpleStatEffective, - &Children, - &WeaponSlot, + &'static DamageStat, + &'static SimpleStatEffective, + &'static SimpleStatEffective, + &'static SimpleStatEffective, + &'static Children, + &'static WeaponSlot, Has, ), (With, With, Without), >, - mut player_q: Query< + + /// The player who's using the weapon and their related stats + player_q: Query< + 'w, + 's, ( Entity, - &EffectiveStat, - &EffectiveStat, - &SimpleStatEffective, - &SimpleStatEffective, - &SimpleStatEffective, - &Children, - &mut Health, + &'static EffectiveStat, + &'static EffectiveStat, + &'static SimpleStatEffective, + &'static SimpleStatEffective, + &'static SimpleStatEffective, + &'static Children, + &'static mut Health, Has, ), (With, With), >, - mut target_q: Query< + + /// The player who is targeted with the weapon + target_q: Query< + 'w, + 's, ( Entity, - &EffectiveStat, - &EffectiveStat, - &SimpleStatEffective, - &ArmourBodyParts, - &mut Health, + &'static EffectiveStat, + &'static EffectiveStat, + &'static SimpleStatEffective, + &'static ArmourBodyParts, + &'static mut Health, ), (With, Without), >, - (armour_q, damage_q, mut mitigation_q, bypass_q): ( - Query<&ArmourBodyPart>, - Query<(Entity, &DeferredDamage)>, - Query>, - Query<&ArmourBypassBonus>, +} + +// NOTE: unfortunately this function can't really be split into smaller parts due to the existence +// of multi turn bonuses +#[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>), (mut ammo_q, mut temp_q): ( Query<( @@ -206,15 +368,10 @@ pub fn use_damaging_weapon( )>, Query<&mut Uses>, ), - (mut logger, mut commands, dmg_spread, metrics, mut effects): ( - Logger, - Commands, - Local, - Res, - Effects, - ), + dmg_spread: Local, ) { - 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 { return; }; @@ -228,44 +385,26 @@ pub fn use_damaging_weapon( p_children, mut p_health, 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) = - target_q.single_mut().unwrap(); + entities.target_q.single_mut().unwrap(); - for (instance, damage) in damage_q.iter_many(p_children) { - let health_before = p_health.0; - p_health.0 = p_health.0.saturating_sub(damage.amount); - - log!(logger, "deferred_damage", { - health_before: health_before, - health_after: p_health.0, - amount: damage.amount, - 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; - } + if check_deferred_damage( + &deferred, + &mut shared, + player, + p_children, + &mut p_health, + attacker, + target, + ) { + return; } if let Ok(mut uses) = temp_q.get_mut(weapon) { uses.0 -= 1; if uses.0 == 0 { - commands.entity(weapon).remove::(); + shared.commands.entity(weapon).remove::(); } } @@ -314,15 +453,19 @@ pub fn use_damaging_weapon( loop { let rounds = ammo.as_mut().map(|(ammo, clips, rof, _)| { - let rounds = (rng.random_range(rof.clone()).round() as u16).clamp(1, ammo.0); - metrics.increment_counter(Some(player), "rounds_fired", rounds.into()); - metrics.increment_counter(Some(weapon), "rounds_fired", rounds.into()); + let rounds = (shared.rng.random_range(rof.clone()).round() as u16).clamp(1, ammo.0); + shared + .metrics + .increment_counter(Some(player), "rounds_fired", rounds.into()); + shared + .metrics + .increment_counter(Some(weapon), "rounds_fired", rounds.into()); ammo.0 -= rounds; if ammo.0 == 0 { if clips.value == 0 { - commands.entity(weapon).remove::(); + shared.commands.entity(weapon).remove::(); } else { - commands.entity(weapon).insert(NeedsReload); + shared.commands.entity(weapon).insert(NeedsReload); } } rounds @@ -334,31 +477,31 @@ pub fn use_damaging_weapon( base_hit_chance + acc_eff.value * (1.0 - base_hit_chance) }; - if hit_chance <= 1.0 && !rng.random_bool(hit_chance as f64) { - log!(logger, "miss_target", { + if hit_chance <= 1.0 && !shared.rng.random_bool(hit_chance as f64) { + log!(shared.logger, "miss_target", { weapon: weapon, actor: player, recipient: target, rounds: rounds, hit_chance: hit_chance, }); - metrics.increment_counter(Some(player), "miss", 1); - metrics.increment_counter(Some(weapon), "miss", 1); + shared.metrics.increment_counter(Some(player), "miss", 1); + shared.metrics.increment_counter(Some(weapon), "miss", 1); if multi_attack_proc.is_none() { return; }; } else { let body_part = if !non_targeted { - rng.sample(crit) + shared.rng.sample(crit) } else { BodyPart::Stomach }; let mult = match body_part { BodyPart::Head | BodyPart::Heart | BodyPart::Throat => { - metrics.increment_counter(Some(player), "crit", 1); - metrics.increment_counter(Some(weapon), "crit", 1); + shared.metrics.increment_counter(Some(player), "crit", 1); + shared.metrics.increment_counter(Some(weapon), "crit", 1); 1.0 } BodyPart::LeftHand @@ -371,17 +514,17 @@ pub fn use_damaging_weapon( BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75, }; - metrics.increment_counter(Some(player), "hit", 1); - metrics.increment_counter(Some(weapon), "hit", 1); + shared.metrics.increment_counter(Some(player), "hit", 1); + shared.metrics.increment_counter(Some(weapon), "hit", 1); 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); // 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. // 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; @@ -391,84 +534,17 @@ pub fn use_damaging_weapon( } } - 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; - } - } - } - } - - 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 bonus_mitigation = check_bonus_mitigation( + &mut bonus_mitigation, + &mut shared, + children, + &mut armour_mitigation, + &mut piece, + *slot, + &rounds, + &health, + target_max_health, + ); let spammo_bonus = ammo .as_ref() @@ -498,8 +574,8 @@ pub fn use_damaging_weapon( * dmg_spread; let mut dmg_i = dmg.round() as u32; - metrics.record_histogram(Some(player), "dmg", dmg_i); - metrics.record_histogram(Some(weapon), "dmg", dmg_i); + shared.metrics.record_histogram(Some(player), "dmg", dmg_i); + shared.metrics.record_histogram(Some(weapon), "dmg", dmg_i); if dmg_i > 0 { for effect in damage_proc_q.iter_many(children) { @@ -509,7 +585,7 @@ pub fn use_damaging_weapon( continue; } 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 { MultiTurnBonus::Blindfire => { multi_attack_proc = Some(MultiAttack::Blindfire) @@ -520,43 +596,53 @@ pub fn use_damaging_weapon( } MultiTurnBonus::Rage => { multi_attack_proc = - Some(MultiAttack::Rage(rng.random_range(2..=8))) + Some(MultiAttack::Rage(shared.rng.random_range(2..=8))) } MultiTurnBonus::DoubleTap => { multi_attack_proc = Some(MultiAttack::DoubleTap { first_shot: true }) } }; - metrics.increment_counter(Some(player), bonus.counter_label(), 1); - metrics.increment_counter(Some(weapon), bonus.counter_label(), 1); + shared.metrics.increment_counter( + Some(player), + bonus.counter_label(), + 1, + ); + shared.metrics.increment_counter( + Some(weapon), + bonus.counter_label(), + 1, + ); } } DamageProcEffect::SelfEffect { value, bonus } => { let chance = (value / 100.0) as f64; - if chance > 1.0 || rng.random_bool(chance) { - bonus.spawn(player, &mut effects); + if chance > 1.0 || shared.rng.random_bool(chance) { + bonus.spawn(player, &mut shared.effects); } } DamageProcEffect::OpponentEffect { value, bonus } => { let chance = (value / 100.0) as f64; - if chance > 1.0 || rng.random_bool(chance) { - bonus.spawn(target, &mut effects, &mut rng.0); + if chance > 1.0 || shared.rng.random_bool(chance) { + bonus.spawn(target, &mut shared.effects, &mut shared.rng.0); } } DamageProcEffect::DamageOverTime { value, kind } => { 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 { DamageOverTimeType::Bleed => { - commands - .entity(target) - .insert(DamageOverTime::::new(dmg_i)); + shared.commands.entity(target).insert(DamageOverTime::< + Bleed, + >::new( + dmg_i + )); } } } } 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 * w_dmg.0 * (1.0 + dmg_bonus.value + spammo_bonus + 5.0) @@ -570,15 +656,15 @@ pub fn use_damaging_weapon( } DamageProcEffect::Execute { 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; health.0 = 0; - log!(logger, "executed", { + log!(shared.logger, "executed", { actor: player, recipient: target, health_before, }); - log!(logger, "fight_end", { + log!(shared.logger, "fight_end", { actor: player, recipient: target, fight_end_type: %if attacker { @@ -587,7 +673,7 @@ pub fn use_damaging_weapon( FightEndType::Loss }, }); - metrics.increment_counter(Some(player), "victory", 1); + shared.metrics.increment_counter(Some(player), "victory", 1); return; } } @@ -599,7 +685,7 @@ pub fn use_damaging_weapon( health.0 = health.0.saturating_sub(dmg_i as u16); - log!(logger, "hit_target", { + log!(shared.logger, "hit_target", { actor: player, acc: acc_eff.value, recipient: target, @@ -624,8 +710,8 @@ pub fn use_damaging_weapon( if health.0 == 0 && !defeated { defeated = true; - commands.entity(target).insert(Defeated); - log!(logger, "fight_end", { + shared.commands.entity(target).insert(Defeated); + log!(shared.logger, "fight_end", { actor: player, recipient: target, fight_end_type: %if attacker { @@ -634,7 +720,7 @@ pub fn use_damaging_weapon( FightEndType::Loss }, }); - metrics.increment_counter(Some(player), "victory", 1); + shared.metrics.increment_counter(Some(player), "victory", 1); } }