initial commit

This commit is contained in:
TotallyNot 2023-12-31 21:26:43 +01:00
commit 86f9333aec
21 changed files with 6449 additions and 0 deletions

916
src/player/mod.rs Normal file
View file

@ -0,0 +1,916 @@
use bevy_ecs::prelude::*;
use macros::LogMessage;
use rand::Rng as _;
use strum::Display;
use crate::{
armour,
effect::Effects,
hierarchy::Children,
log::Logger,
metrics::Metrics,
passives::{Education, FactionUpgrades, Merits},
weapon::{
bonus::MultiTurnBonus,
temp::{NonTargeted, Uses},
Ammo, DamageProcEffect, DamageStat, NeedsReload, RateOfFire, TurnTriggeredEffect, Usable,
Weapon, WeaponSlot,
},
FightStatus, Id, Name, Rng, Stages,
};
use self::stats::{
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health,
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
};
pub mod stats;
pub mod status_effect;
#[derive(Component)]
pub struct Attacker;
#[derive(Component)]
pub struct Defender;
#[derive(Component)]
pub struct Defeated;
#[derive(Component)]
pub struct Current;
#[derive(Component)]
pub struct CurrentTarget;
#[derive(Component, Default)]
pub struct Player;
#[derive(Component)]
pub struct Level(pub u16);
impl Default for Level {
fn default() -> Self {
Self(1)
}
}
#[derive(Component, Debug)]
pub struct MaxHealth(pub u16);
impl Default for MaxHealth {
fn default() -> Self {
Self(100)
}
}
#[derive(Component, Debug, Default)]
pub struct CombatTurns(pub u16);
#[derive(Component, Default, Debug)]
pub struct Weapons {
pub primary: Option<Entity>,
pub secondary: Option<Entity>,
pub melee: Option<Entity>,
pub temporary: Option<Entity>,
pub fists: Option<Entity>,
pub kick: Option<Entity>,
}
impl Weapons {
fn select(
&self,
slot: WeaponSlot,
reload: bool,
usable_q: &Query<(Has<NeedsReload>, &Children), With<Usable>>,
) -> Option<(Entity, Children)> {
let id = match slot {
WeaponSlot::Primary => self.primary?,
WeaponSlot::Secondary => self.secondary?,
WeaponSlot::Melee => self.melee?,
WeaponSlot::Temporary => self.temporary?,
WeaponSlot::Fists => self.fists?,
WeaponSlot::Kick => self.kick?,
};
let (needs_reload, children) = usable_q.get(id).ok()?;
if !reload && needs_reload {
None
} else {
Some((id, children.clone()))
}
}
}
#[derive(Component, Debug)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
pub enum PlayerStrategy {
AlwaysFists,
AlwaysKicks,
PrimaryMelee {
reload: bool,
},
InOrder {
order: Vec<WeaponSlot>,
reload: bool,
},
}
impl Default for PlayerStrategy {
fn default() -> Self {
Self::AlwaysFists
}
}
#[derive(Event)]
pub struct ChooseWeapon(pub Entity);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BodyPart {
LeftHand,
RightHand,
LeftArm,
RightArm,
LeftFoot,
RightFoot,
LeftLeg,
RightLeg,
Stomach,
Chest,
Groin,
Head,
Throat,
Heart,
}
impl std::fmt::Display for BodyPart {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::LeftHand => write!(f, "Left hand"),
Self::RightHand => write!(f, "Right hand"),
Self::LeftArm => write!(f, "Left arm"),
Self::RightArm => write!(f, "Right arm"),
Self::LeftFoot => write!(f, "Left foot"),
Self::RightFoot => write!(f, "Right foot"),
Self::LeftLeg => write!(f, "Left leg"),
Self::RightLeg => write!(f, "Right leg"),
Self::Stomach => write!(f, "Stomach"),
Self::Chest => write!(f, "Chest"),
Self::Groin => write!(f, "Groin"),
Self::Head => write!(f, "Head"),
Self::Throat => write!(f, "Throat"),
Self::Heart => write!(f, "Heart"),
}
}
}
#[derive(Event)]
pub struct InitiateHit {
pub body_part: BodyPart,
pub weapon: Entity,
pub rounds: Option<u16>,
pub dmg: f32,
pub dmg_bonus_weapon: f32,
pub dmg_bonus_player: f32,
pub hit_chance: f32,
pub crit_rate: u16,
}
#[derive(Clone, Copy, PartialEq, Eq, Display)]
pub enum FightEndType {
Victory,
Stalemate,
Loss,
}
#[derive(LogMessage)]
struct FightEnd {
#[log(player)]
actor: Entity,
#[log(player)]
recipient: Entity,
#[log(display)]
fight_end_type: FightEndType,
}
#[derive(Bundle)]
pub struct PlayerBundle {
pub name: Name,
pub id: Id,
pub player: Player,
pub level: Level,
pub crit_rate: SimpleStatBundle<CritRate>,
// TODO: since these two need to be tracked here anyways it might be preferable to shift all
// player specific passives here instead of tracking them on the weapons
pub acc_bonus: SimpleStatBundle<WeaponAccuracy>,
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
pub strategy: PlayerStrategy,
pub combat_turns: CombatTurns,
}
impl PlayerBundle {
pub fn new(name: impl ToString, id: usize, level: u16, strategy: PlayerStrategy) -> Self {
Self {
name: Name(name.to_string()),
id: Id(id),
player: Player,
level: Level(level),
crit_rate: SimpleStatBundle::new(24),
acc_bonus: SimpleStatBundle::new(0.0),
dmg_bonus: SimpleStatBundle::new(0.0),
strategy,
combat_turns: Default::default(),
}
}
}
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>>,
mut commands: Commands,
) {
let attacker = attacker_q.iter().next().unwrap();
let defender = defender_q.single();
commands.entity(attacker).insert(Current);
commands.entity(defender).insert(CurrentTarget);
}
fn change_roles(
current_q: Query<Entity, (With<Current>, With<Player>)>,
target_q: Query<Entity, With<CurrentTarget>>,
mut commands: Commands,
) {
let current = current_q.single();
let target = target_q.single();
// TODO: Group fights
commands
.entity(current)
.remove::<Current>()
.insert(CurrentTarget);
// TODO: Distraction
commands
.entity(target)
.insert(Current)
.remove::<CurrentTarget>();
}
fn check_term_condition(
mut state: ResMut<FightStatus>,
defender_q: Query<(), (With<Defender>, Without<Defeated>)>,
attacker_q: Query<(), (With<Attacker>, Without<Defeated>)>,
) {
if defender_q.is_empty() || attacker_q.is_empty() {
*state = FightStatus::Over;
}
}
pub fn pick_action(
mut p_query: Query<
(Entity, &Weapons, &PlayerStrategy, &mut CombatTurns),
(With<Current>, With<Player>),
>,
target_q: Query<Entity, With<CurrentTarget>>,
usable_q: Query<(Has<NeedsReload>, &Children), With<Usable>>,
weapon_trigger_q: Query<&TurnTriggeredEffect>,
mut commands: Commands,
mut effects: Effects,
metrics: Res<Metrics>,
) {
let (current, weapons, strat, mut turns) = p_query.single_mut();
let (weapon, children) = match strat {
PlayerStrategy::AlwaysFists => (weapons.fists.unwrap(), Default::default()),
PlayerStrategy::AlwaysKicks => weapons
.select(WeaponSlot::Kick, true, &usable_q)
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
PlayerStrategy::PrimaryMelee { reload } => weapons
.select(WeaponSlot::Primary, *reload, &usable_q)
.or_else(|| weapons.select(WeaponSlot::Melee, true, &usable_q))
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
PlayerStrategy::InOrder { order, reload } => order
.iter()
.find_map(|slot| weapons.select(*slot, *reload, &usable_q))
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
};
metrics.increment_counter(current, "turn", 1);
metrics.increment_counter(weapon, "turn", 1);
commands.entity(weapon).insert(Current);
let target = target_q.single();
for effect in weapon_trigger_q.iter_many(children.get()) {
effect.trigger(&mut effects, current, target);
}
turns.0 += 1;
}
pub struct DamageSpread(rand_distr::Beta<f32>);
impl FromWorld for DamageSpread {
fn from_world(_world: &mut World) -> Self {
Self(rand_distr::Beta::new(3.0, 3.0).unwrap())
}
}
fn receive_hit(
(mut rng, spread): (ResMut<crate::Rng>, Local<DamageSpread>),
mut hit_init_events: EventReader<InitiateHit>,
current_q: Query<
(
Entity,
&Education,
Option<&Attacker>,
&EffectiveStat<Strength>,
),
(With<Current>, With<Player>),
>,
mut target_q: Query<
(
Entity,
&mut SimpleStatEffective<Health>,
&armour::ArmourBodyParts,
&EffectiveStat<Defence>,
),
With<CurrentTarget>,
>,
armour_q: Query<&armour::ArmourBodyPart>,
(mut commands, mut logger): (Commands, Logger),
metrics: Res<Metrics>,
) {
#[derive(LogMessage)]
struct HitTarget {
#[log(player)]
actor: Entity,
#[log(player)]
recipient: Entity,
#[log(weapon)]
weapon: Entity,
#[log(display)]
part: BodyPart,
part_mult: f32,
rounds: Option<u16>,
dmg: u32,
health_before: u16,
health_after: u16,
dmg_intrinsic: f32,
dmg_spread: f32,
armour_mitigation: f32,
def_mitigation: f32,
weapon_dmg: f32,
bonus_dmg: f32,
hit_chance: f32,
crit_rate: u16,
}
if hit_init_events.is_empty() {
return;
}
let (target, mut health, body_parts, target_def) = target_q.single_mut();
let (current, edu, attacker, current_str) = current_q.single();
let def_str_ratio = (target_def.value / current_str.value).clamp(1.0 / 32.0, 14.0);
let def_mitigation = if def_str_ratio < 1.0 {
0.5 * def_str_ratio.log(32.0) + 0.5
} else {
0.5 * def_str_ratio.log(14.0) + 0.5
};
let dmg_intrinsic = 7.0 * (current_str.value / 10.0).log10().powi(2)
+ 27.0 * (current_str.value / 10.0).log10()
+ 30.0;
for event in hit_init_events.read() {
let mult = match event.body_part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
metrics.increment_counter(current, "crit", 1);
metrics.increment_counter(event.weapon, "crit", 1);
1.0
}
BodyPart::LeftHand | BodyPart::RightHand | BodyPart::LeftFoot | BodyPart::RightFoot => {
0.2
}
BodyPart::LeftArm | BodyPart::RightArm | BodyPart::LeftLeg | BodyPart::RightLeg => {
1.0 / 3.5
}
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
};
metrics.increment_counter(current, "hit", 1);
metrics.increment_counter(event.weapon, "hit", 1);
let armour_parts = armour_q.get(body_parts.0[event.body_part.into()]).unwrap();
let piece = rng.sample(armour_parts);
let armour_mitigation = piece.map_or(0.0, |p| p.armour_value);
// NOTE: The beta distribution is defined on [0,1], so we rescale here
let dmg_spread = rng.sample(spread.0) / 10.0 + 1.0;
let mut dmg_bonus = event.dmg_bonus_weapon + event.dmg_bonus_player;
if edu.bio2380 && event.body_part == BodyPart::Throat {
dmg_bonus += 0.10;
}
let dmg = dmg_intrinsic
* event.dmg
* dmg_bonus
* (1.0 - armour_mitigation)
* (1.0 - def_mitigation)
* mult
* dmg_spread;
let dmg = dmg.round() as u32;
metrics.record_histogram(current, "dmg", dmg);
metrics.record_histogram(event.weapon, "dmg", dmg);
let health_before = health.value;
health.value = health.value.saturating_sub(dmg as u16);
logger.log(|| HitTarget {
actor: current,
recipient: target,
weapon: event.weapon,
part: event.body_part,
part_mult: mult,
rounds: event.rounds,
dmg,
health_before,
health_after: health.value,
dmg_spread,
dmg_intrinsic,
armour_mitigation,
def_mitigation,
weapon_dmg: event.dmg,
bonus_dmg: dmg_bonus,
hit_chance: event.hit_chance,
crit_rate: event.crit_rate,
});
if health.value == 0 {
commands.entity(target).insert(Defeated);
logger.log(|| FightEnd {
actor: current,
recipient: target,
fight_end_type: if attacker.is_some() {
FightEndType::Victory
} else {
FightEndType::Loss
},
});
metrics.increment_counter(current, "victory", 1);
}
}
}
// 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)]
pub fn use_damaging_weapon(
mut rng: ResMut<Rng>,
weapon_q: Query<
(
Entity,
&DamageStat,
&SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>,
&SimpleStatEffective<CritRate>,
&Children,
Has<NonTargeted>,
),
(With<Weapon>, With<Current>, Without<NeedsReload>),
>,
player_q: Query<
(
Entity,
&EffectiveStat<Speed>,
&EffectiveStat<Strength>,
&SimpleStatEffective<CritRate>,
&SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>,
&Education,
Has<Attacker>,
),
(With<Player>, With<Current>),
>,
mut target_q: Query<
(
Entity,
&EffectiveStat<Dexterity>,
&EffectiveStat<Defence>,
&armour::ArmourBodyParts,
&mut SimpleStatEffective<Health>,
),
With<CurrentTarget>,
>,
armour_q: Query<&armour::ArmourBodyPart>,
damage_proc_q: Query<&DamageProcEffect>,
(mut ammo_q, mut temp_q): (
Query<(
&mut Ammo,
&SimpleStatEffective<Clips>,
&RateOfFire,
&SimpleStatEffective<AmmoControl>,
)>,
Query<&mut Uses>,
),
(mut logger, mut commands, dmg_spread, metrics): (
Logger,
Commands,
Local<DamageSpread>,
Res<Metrics>,
),
) {
#[derive(LogMessage)]
pub struct MissTarget {
#[log(player)]
pub actor: Entity,
#[log(player)]
pub recipient: Entity,
#[log(weapon)]
pub weapon: Entity,
pub rounds: Option<u16>,
}
#[derive(LogMessage)]
struct HitTarget {
#[log(player)]
actor: Entity,
#[log(player)]
recipient: Entity,
#[log(weapon)]
weapon: Entity,
#[log(display)]
part: BodyPart,
part_mult: f32,
rounds: Option<u16>,
dmg: u32,
health_before: u16,
health_after: u16,
dmg_intrinsic: f32,
dmg_spread: f32,
armour_mitigation: f32,
def_mitigation: f32,
weapon_dmg: f32,
bonus_dmg: f32,
hit_chance: f32,
crit_rate: u16,
}
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.get_single()
else {
return;
};
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, edu, attacker) =
player_q.single();
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut();
if let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1;
if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>();
}
}
let spd_dex_ratio = (player_spd.value / target_dex.value).clamp(1.0 / 64.0, 64.0);
let base_hit_chance = if spd_dex_ratio < 1.0 {
0.5 * (8.0 * spd_dex_ratio.sqrt() - 1.0) / 7.0
} else {
1.0 - 0.5 * (8.0 / spd_dex_ratio.sqrt() - 1.0) / 7.0
};
let mut acc_eff = acc + acc_bonus;
let mut ammo = ammo_q
.get_mut(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);
(ammo, clips, rof_eff)
});
enum MultiAttack {
Blindfire,
Rage(u16),
Fury { first_hit: bool },
DoubleTap { first_shot: bool },
}
let mut multi_attack_proc = None;
let mut defeated = false;
let crit = player_crit + crit;
let def_str_ratio = (target_def.value / player_str.value).clamp(1.0 / 32.0, 14.0);
let def_mitigation = if def_str_ratio < 1.0 {
0.5 * def_str_ratio.log(32.0) + 0.5
} else {
0.5 * def_str_ratio.log(14.0) + 0.5
};
let dmg_intrinsic = 7.0 * (player_str.value / 10.0).log10().powi(2)
+ 27.0 * (player_str.value / 10.0).log10()
+ 30.0;
loop {
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| {
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0);
metrics.increment_counter(player, "rounds_fired", rounds.into());
metrics.increment_counter(weapon, "rounds_fired", rounds.into());
ammo.0 -= rounds;
if ammo.0 == 0 {
if clips.value == 0 {
commands.entity(weapon).remove::<Usable>();
} else {
commands.entity(weapon).insert(NeedsReload);
}
}
rounds
});
let hit_chance = if base_hit_chance < 0.5 {
base_hit_chance + acc_eff.value * base_hit_chance
} else {
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
};
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
logger.log(|| MissTarget {
weapon,
actor: player,
recipient: target,
rounds,
});
metrics.increment_counter(player, "miss", 1);
metrics.increment_counter(weapon, "miss", 1);
if multi_attack_proc.is_none() {
return;
};
} else {
let body_part = if !non_targeted {
rng.sample(crit)
} else {
BodyPart::Stomach
};
let mult = match body_part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
metrics.increment_counter(player, "crit", 1);
metrics.increment_counter(weapon, "crit", 1);
1.0
}
BodyPart::LeftHand
| BodyPart::RightHand
| BodyPart::LeftFoot
| BodyPart::RightFoot => 0.2,
BodyPart::LeftArm | BodyPart::RightArm | BodyPart::LeftLeg | BodyPart::RightLeg => {
1.0 / 3.5
}
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
};
metrics.increment_counter(player, "hit", 1);
metrics.increment_counter(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);
// NOTE: The beta distribution is defined on [0,1], so we rescale here
let dmg_spread = rng.sample(dmg_spread.0) / 10.0 + 1.0;
let mut dmg_bonus = dmg_bonus + p_dmg_bonus;
if edu.bio2380 && body_part == BodyPart::Throat {
dmg_bonus.value += 0.10;
}
// TODO: special ammo
let dmg = dmg_intrinsic
* w_dmg.0
* dmg_bonus.value
* (1.0 - armour_mitigation)
* (1.0 - def_mitigation)
* mult
* dmg_spread;
let dmg = dmg.round() as u32;
metrics.record_histogram(player, "dmg", dmg);
metrics.record_histogram(weapon, "dmg", dmg);
if dmg > 0 {
for effect in damage_proc_q.iter_many(children.get()) {
match effect {
DamageProcEffect::MultiTurn { value, bonus }
if multi_attack_proc.is_none() =>
{
if rng.gen_bool(*value as f64) {
match bonus {
MultiTurnBonus::Blindfire => {
multi_attack_proc = Some(MultiAttack::Blindfire)
}
MultiTurnBonus::Fury => {
multi_attack_proc =
Some(MultiAttack::Fury { first_hit: true })
}
MultiTurnBonus::Rage => {
multi_attack_proc =
Some(MultiAttack::Rage(rng.gen_range(2..=8)))
}
MultiTurnBonus::DoubleTap => {
multi_attack_proc =
Some(MultiAttack::DoubleTap { first_shot: true })
}
};
}
}
_ => (),
}
}
}
let health_before = health.value;
health.value = health.value.saturating_sub(dmg as u16);
logger.log(|| HitTarget {
actor: player,
recipient: target,
weapon,
part: body_part,
part_mult: mult,
rounds,
dmg,
health_before,
health_after: health.value,
dmg_spread,
dmg_intrinsic,
armour_mitigation,
def_mitigation,
weapon_dmg: w_dmg.0,
bonus_dmg: dmg_bonus.value,
hit_chance,
crit_rate: crit.value,
});
if health.value == 0 && !defeated {
defeated = true;
commands.entity(target).insert(Defeated);
logger.log(|| FightEnd {
actor: player,
recipient: target,
fight_end_type: if attacker {
FightEndType::Victory
} else {
FightEndType::Loss
},
});
metrics.increment_counter(player, "victory", 1);
}
}
match multi_attack_proc {
Some(MultiAttack::Blindfire) => {
if !ammo.as_ref().map(|(a, _, _)| a.0 != 0).unwrap_or_default() {
break;
}
acc_eff.value -= 5.0 / 50.0;
}
Some(MultiAttack::Fury { first_hit: true }) => {
multi_attack_proc = Some(MultiAttack::Fury { first_hit: false })
}
Some(MultiAttack::Rage(turns @ 1..)) => {
multi_attack_proc = Some(MultiAttack::Rage(turns - 1))
}
Some(MultiAttack::DoubleTap { first_shot: true }) => {
multi_attack_proc = Some(MultiAttack::DoubleTap { first_shot: false })
}
_ => break,
}
}
}
pub fn check_stalemate(
current_q: Query<(Entity, &CombatTurns, Option<&Attacker>), (With<Current>, With<Player>)>,
target_q: Query<Entity, With<CurrentTarget>>,
other_attackers_q: Query<(), (With<Attacker>, Without<Current>)>,
mut state: ResMut<FightStatus>,
mut commands: Commands,
mut logger: Logger,
metrics: Res<Metrics>,
) {
let (current, current_turns, attacker) = current_q.single();
if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker.is_some() {
commands.entity(current).insert(Defeated);
let target = target_q.single();
logger.log(|| FightEnd {
actor: current,
recipient: target,
fight_end_type: FightEndType::Stalemate,
});
metrics.increment_counter(current, "stalemate", 1);
if other_attackers_q.is_empty() {
*state = FightStatus::Over
}
}
}
pub fn restore_initial_state(
mut state: ResMut<FightStatus>,
mut player_q: Query<(Entity, &mut CombatTurns, Has<Attacker>)>,
mut commands: Commands,
) {
*state = FightStatus::Ongoing;
for (player, mut turns, attacker) in player_q.iter_mut() {
turns.0 = 0;
commands.entity(player).remove::<Defeated>();
if attacker {
commands
.entity(player)
.remove::<CurrentTarget>()
.insert(Current);
} else {
commands
.entity(player)
.remove::<Current>()
.insert(CurrentTarget);
}
}
}
fn record_post_fight_stats(
player_q: Query<(Entity, &SimpleStatEffective<Health>)>,
metrics: Res<Metrics>,
) {
for (player, health) in player_q.iter() {
metrics.record_histogram(player, "rem_health", health.value as u32);
}
}
pub(crate) fn configure(stages: &mut Stages) {
stats::configure(stages);
status_effect::configure(stages);
stages.add_event::<ChooseWeapon>();
stages.add_event::<InitiateHit>();
stages.equip.add_systems(designate_first);
stages.pre_fight.add_systems(derive_max_health);
stages.pre_turn.add_systems(pick_action);
stages.turn.add_systems(use_damaging_weapon);
stages
.post_turn
.add_systems((check_term_condition, change_roles))
.add_systems(
check_stalemate
.after(check_term_condition)
.before(change_roles),
);
stages.post_fight.add_systems(record_post_fight_stats);
stages.restore.add_systems(restore_initial_state);
}