feat: added armour passives

This commit is contained in:
TotallyNot 2025-11-04 13:04:56 +01:00
parent cfe2631578
commit 451efd2bb7
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
17 changed files with 654 additions and 318 deletions

View file

@ -1,14 +1,14 @@
use bevy_ecs::prelude::*;
use proxisim_models::bundle::{
armour::{ArmourBodyPart, ArmourBodyParts},
passive::{FactionUpgrades, Merits},
bonus::DamageMitigationBonus,
player::{
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender,
FightEndType, Level, MaxHealth, PartDamageBonus, Player, PlayerStrategy, Weapons,
FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons,
},
stat::{
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health,
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth,
SimpleStatEffective, Speed, Strength, WeaponAccuracy,
},
weapon::{
Ammo, DamageStat, NeedsReload, NonTargeted, RateOfFire, Usable, Uses, Weapon, WeaponSlot,
@ -22,6 +22,7 @@ use crate::{
log,
log::Logger,
metrics::Metrics,
player::status_effect::{Bleed, DamageOverTime, DamageOverTimeType, DeferredDamage},
weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus},
};
@ -52,28 +53,6 @@ fn select_weapon<'a>(
}
}
fn derive_max_health(
level_query: Query<(Entity, &Level, &Merits, &FactionUpgrades)>,
mut cmd: Commands,
) {
for (entity, level, merits, faction) in level_query.iter() {
let base_life = match level.0 {
1..=8 => 100 + (level.0 - 1) * 25,
9..=95 => 275 + (level.0 - 8) * 50,
96.. => 4625 + (level.0 - 95) * 75,
0 => unreachable!(),
};
let max_health =
((base_life as f32) * (1.0 + ((merits.life * 5 + faction.life) as f32) / 100.0)) as u16;
cmd.entity(entity).insert((
MaxHealth(max_health),
SimpleStatBundle::<Health>::new(max_health),
));
}
}
fn designate_first(
attacker_q: Query<Entity, With<Attacker>>,
defender_q: Query<Entity, With<Defender>>,
@ -179,11 +158,12 @@ pub fn use_damaging_weapon(
&SimpleStatEffective<DamageBonus>,
&SimpleStatEffective<CritRate>,
&Children,
&WeaponSlot,
Has<NonTargeted>,
),
(With<Weapon>, With<Current>, Without<NeedsReload>),
>,
player_q: Query<
mut player_q: Query<
(
Entity,
&EffectiveStat<Speed>,
@ -191,6 +171,8 @@ pub fn use_damaging_weapon(
&SimpleStatEffective<CritRate>,
&SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>,
&Children,
&mut Health,
Has<Attacker>,
),
(With<Player>, With<Current>),
@ -200,12 +182,17 @@ pub fn use_damaging_weapon(
Entity,
&EffectiveStat<Dexterity>,
&EffectiveStat<Defence>,
&SimpleStatEffective<MaxHealth>,
&ArmourBodyParts,
&mut SimpleStatEffective<Health>,
&mut Health,
),
With<CurrentTarget>,
(With<CurrentTarget>, Without<Current>),
>,
armour_q: Query<&ArmourBodyPart>,
(armour_q, damage_q, mut mitigation_q): (
Query<&ArmourBodyPart>,
Query<(Entity, &DeferredDamage)>,
Query<Option<&mut DamageMitigationBonus>>,
),
(damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>),
(mut ammo_q, mut temp_q): (
Query<(
@ -224,13 +211,52 @@ pub fn use_damaging_weapon(
Effects,
),
) {
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.single()
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, slot, non_targeted)) = weapon_q.single()
else {
return;
};
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) =
player_q.single().unwrap();
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut().unwrap();
let (
player,
player_spd,
player_str,
player_crit,
acc_bonus,
p_dmg_bonus,
p_children,
mut p_health,
attacker,
) = player_q.single_mut().unwrap();
let (target, target_dex, target_def, target_max_health, armour_parts, mut health) =
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 {
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 let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1;
@ -253,7 +279,7 @@ pub fn use_damaging_weapon(
.ok()
.map(|(ammo, clips, rof, ammo_ctrl)| {
let ammo_ctrl = 1.0 - (ammo_ctrl).value;
let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..((rof.0[1] as f32) * ammo_ctrl);
let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..=((rof.0[1] as f32) * ammo_ctrl);
(ammo, clips, rof_eff)
});
@ -358,6 +384,68 @@ 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
}
}
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 {
mitigation: *mitigation,
},
);
}
*mitigation
}
_ => 0.0,
}
} else {
0.0
}
}
None => 0.0,
};
// TODO: special ammo
let dmg = dmg_intrinsic
@ -365,6 +453,7 @@ pub fn use_damaging_weapon(
* (1.0 + dmg_bonus.value)
* (1.0 - armour_mitigation)
* (1.0 - def_mitigation)
* (1.0 - bonus_mitigation)
* mult
* dmg_spread;
let dmg = dmg.round() as u32;
@ -414,13 +503,25 @@ pub fn use_damaging_weapon(
bonus.spawn(target, &mut effects, &mut rng.0);
}
}
DamageProcEffect::DamageOverTimer { value, kind } => {
let chance = (value / 100.0) as f64;
if chance > 1.0 || rng.random_bool(chance) {
match kind {
DamageOverTimeType::Bleed => {
commands
.entity(target)
.insert(DamageOverTime::<Bleed>::new(dmg));
}
}
}
}
}
}
}
let health_before = health.value;
let health_before = health.0;
health.value = health.value.saturating_sub(dmg as u16);
health.0 = health.0.saturating_sub(dmg as u16);
log!(logger, "hit_target", {
actor: player,
@ -431,19 +532,20 @@ pub fn use_damaging_weapon(
part_mult: mult,
rounds,
health_before: health_before,
health_after: health.value,
health_after: health.0,
dmg,
dmg_spread,
dmg_intrinsic,
dmg_weapon: w_dmg.0,
armour_mitigation,
def_mitigation,
bonus_mitigation,
bonus_dmg: dmg_bonus.value,
hit_chance: hit_chance,
crit_rate: crit.value,
});
if health.value == 0 && !defeated {
if health.0 == 0 && !defeated {
defeated = true;
commands.entity(target).insert(Defeated);
@ -539,12 +641,23 @@ pub fn restore_initial_state(
}
}
fn record_post_fight_stats(
player_q: Query<(Entity, &SimpleStatEffective<Health>)>,
metrics: Res<Metrics>,
) {
fn record_post_fight_stats(player_q: Query<(Entity, &Health)>, metrics: Res<Metrics>) {
for (player, health) in player_q.iter() {
metrics.record_histogram(Some(player), "rem_health", health.value as u32);
metrics.record_histogram(Some(player), "rem_health", health.0 as u32);
}
}
fn restore_health(
health_q: Query<(Entity, &SimpleStatEffective<MaxHealth>)>,
mut commands: Commands,
mut logger: Logger,
) {
for (player, max_health) in health_q {
log!(logger, "initial_health", {
target: player,
max_health: max_health.value,
});
commands.entity(player).insert(Health(max_health.value));
}
}
@ -554,7 +667,7 @@ pub(crate) fn configure(stages: &mut Stages) {
stages.add_event::<ChooseWeapon>();
stages.equip.add_systems(designate_first);
stages.pre_fight.add_systems(derive_max_health);
stages.pre_fight.add_systems(restore_health);
stages.pre_turn.add_systems(pick_action);
stages.turn.add_systems(use_damaging_weapon);
stages