initial commit
This commit is contained in:
commit
86f9333aec
21 changed files with 6449 additions and 0 deletions
916
src/player/mod.rs
Normal file
916
src/player/mod.rs
Normal 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);
|
||||
}
|
||||
572
src/player/stats.rs
Normal file
572
src/player/stats.rs
Normal file
|
|
@ -0,0 +1,572 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::{hierarchy::Parent, player::BodyPart, Stages};
|
||||
|
||||
pub trait SimpleStatMarker: Send + Sync + 'static {
|
||||
type ValueType: Send + Sync + Copy + std::fmt::Display + 'static;
|
||||
type BonusType: Send + Sync + Copy + 'static;
|
||||
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SimpleStatBaseline<Stat: SimpleStatMarker> {
|
||||
pub value: Stat::ValueType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SimpleStatEffective<Stat: SimpleStatMarker> {
|
||||
pub value: Stat::ValueType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SimpleStatBonus<Stat: SimpleStatMarker> {
|
||||
pub label: &'static str,
|
||||
pub value: Stat::BonusType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> SimpleStatBonus<Stat> {
|
||||
pub fn new(label: &'static str, value: Stat::BonusType) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct SimpleStatSnapshot<Stat: SimpleStatMarker> {
|
||||
value: Stat::ValueType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct SimpleStatBundle<Stat: SimpleStatMarker> {
|
||||
baseline: SimpleStatBaseline<Stat>,
|
||||
effective: SimpleStatEffective<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> SimpleStatBundle<Stat> {
|
||||
pub fn new(value: Stat::ValueType) -> Self {
|
||||
Self {
|
||||
baseline: SimpleStatBaseline {
|
||||
value,
|
||||
marker: PhantomData,
|
||||
},
|
||||
effective: SimpleStatEffective {
|
||||
value,
|
||||
marker: PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> Clone for SimpleStatEffective<Stat> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> Copy for SimpleStatEffective<Stat> where Stat::ValueType: Copy {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CritRate;
|
||||
|
||||
impl SimpleStatMarker for CritRate {
|
||||
type ValueType = u16;
|
||||
type BonusType = u16;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
impl<Stat> std::ops::Add<&SimpleStatEffective<Stat>> for &SimpleStatEffective<Stat>
|
||||
where
|
||||
Stat: SimpleStatMarker,
|
||||
Stat::ValueType: std::ops::Add<Stat::ValueType, Output = Stat::ValueType>,
|
||||
{
|
||||
type Output = SimpleStatEffective<Stat>;
|
||||
|
||||
fn add(self, rhs: &SimpleStatEffective<Stat>) -> Self::Output {
|
||||
SimpleStatEffective {
|
||||
value: self.value + rhs.value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rand::distributions::Distribution<BodyPart> for SimpleStatEffective<CritRate> {
|
||||
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> BodyPart {
|
||||
if rng.gen_ratio((self.value) as u32, 200) {
|
||||
match rng.gen_range(1..=10) {
|
||||
1 => BodyPart::Heart,
|
||||
2 => BodyPart::Throat,
|
||||
_ => BodyPart::Heart,
|
||||
}
|
||||
} else {
|
||||
match rng.gen_range(1..=20) {
|
||||
1 => BodyPart::LeftHand,
|
||||
2 => BodyPart::RightHand,
|
||||
3 => BodyPart::LeftArm,
|
||||
4 => BodyPart::RightArm,
|
||||
5 => BodyPart::LeftFoot,
|
||||
6 => BodyPart::RightFoot,
|
||||
7 | 8 => BodyPart::RightLeg,
|
||||
9 | 10 => BodyPart::LeftLeg,
|
||||
11..=15 => BodyPart::Chest,
|
||||
16 => BodyPart::Groin,
|
||||
_ => BodyPart::Stomach,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AmmoControl;
|
||||
|
||||
impl SimpleStatMarker for AmmoControl {
|
||||
type ValueType = f32;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DamageBonus;
|
||||
|
||||
impl SimpleStatMarker for DamageBonus {
|
||||
type ValueType = f32;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WeaponAccuracy;
|
||||
|
||||
impl SimpleStatMarker for WeaponAccuracy {
|
||||
type ValueType = f32;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClipSize;
|
||||
|
||||
impl SimpleStatMarker for ClipSize {
|
||||
type ValueType = u16;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as f32) * bonus).round() as u16
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as f32) / bonus).round() as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Clips;
|
||||
|
||||
impl SimpleStatMarker for Clips {
|
||||
type ValueType = u16;
|
||||
type BonusType = i16;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as i16) + bonus) as u16
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as i16) - bonus) as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Health;
|
||||
|
||||
impl SimpleStatMarker for Health {
|
||||
type ValueType = u16;
|
||||
type BonusType = u16;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum StatType {
|
||||
Str,
|
||||
Def,
|
||||
Spd,
|
||||
Dex,
|
||||
}
|
||||
|
||||
pub trait StatMarker: Send + Sync + 'static {
|
||||
fn stat_type() -> StatType;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Strength;
|
||||
impl StatMarker for Strength {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Str
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Defence;
|
||||
impl StatMarker for Defence {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Def
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Speed;
|
||||
impl StatMarker for Speed {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Spd
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Dexterity;
|
||||
impl StatMarker for Dexterity {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Dex
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct BaselineStat<Stat: StatMarker> {
|
||||
pub value: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> Default for BaselineStat<Stat> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: 10.0,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct EffectiveStat<Stat: StatMarker> {
|
||||
pub value: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct AdditiveBonuses<Stat: StatMarker> {
|
||||
pub factor: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> Default for AdditiveBonuses<Stat> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MultiplicativeBonuses<Stat: StatMarker> {
|
||||
pub factor: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> Default for MultiplicativeBonuses<Stat> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle, Default)]
|
||||
pub struct StatBundle<Stat: StatMarker> {
|
||||
baseline: BaselineStat<Stat>,
|
||||
additive: AdditiveBonuses<Stat>,
|
||||
multiplicative: MultiplicativeBonuses<Stat>,
|
||||
effective: EffectiveStat<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct StatSnapshot<Stat: StatMarker> {
|
||||
additive_bonuses: f32,
|
||||
multiplicative_bonuses: f32,
|
||||
effective: f32,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> StatBundle<Stat> {
|
||||
pub fn new(baseline: f32) -> Self {
|
||||
Self {
|
||||
baseline: BaselineStat {
|
||||
value: baseline,
|
||||
marker: PhantomData,
|
||||
},
|
||||
effective: EffectiveStat {
|
||||
value: baseline,
|
||||
marker: PhantomData,
|
||||
},
|
||||
additive: AdditiveBonuses {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
},
|
||||
multiplicative: MultiplicativeBonuses {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct AdditiveBonus<Stat: StatMarker> {
|
||||
pub label: &'static str,
|
||||
pub value: f32,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> AdditiveBonus<Stat> {
|
||||
pub fn new(label: &'static str, value: f32) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MultiplicativeBonus<Stat: StatMarker> {
|
||||
pub label: &'static str,
|
||||
pub value: f32,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> MultiplicativeBonus<Stat> {
|
||||
pub fn new(label: &'static str, value: f32) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn add_additive_bonus<Stat: StatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(&AdditiveBonus<Stat>, &Parent)>,
|
||||
mut stat_q: Query<(
|
||||
&BaselineStat<Stat>,
|
||||
&mut AdditiveBonuses<Stat>,
|
||||
&MultiplicativeBonuses<Stat>,
|
||||
&mut EffectiveStat<Stat>,
|
||||
)>,
|
||||
) {
|
||||
for (bonus, player) in effect_q.iter_many(entities) {
|
||||
let (baseline, mut add, mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||
add.factor += bonus.value;
|
||||
eff.value = baseline.value * add.factor * mult.factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn revert_additive_bonus<Stat: StatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(&AdditiveBonus<Stat>, &Parent)>,
|
||||
mut stat_q: Query<(
|
||||
&BaselineStat<Stat>,
|
||||
&mut AdditiveBonuses<Stat>,
|
||||
&MultiplicativeBonuses<Stat>,
|
||||
&mut EffectiveStat<Stat>,
|
||||
)>,
|
||||
) {
|
||||
for (bonus, player) in effect_q.iter_many(entities) {
|
||||
let (baseline, mut add, mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||
add.factor -= bonus.value;
|
||||
eff.value = baseline.value * add.factor * mult.factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn add_multiplicative_bonus<Stat: StatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(&MultiplicativeBonus<Stat>, &Parent)>,
|
||||
mut stat_q: Query<(
|
||||
&BaselineStat<Stat>,
|
||||
&AdditiveBonuses<Stat>,
|
||||
&mut MultiplicativeBonuses<Stat>,
|
||||
&mut EffectiveStat<Stat>,
|
||||
)>,
|
||||
) {
|
||||
for (bonus, player) in effect_q.iter_many(entities) {
|
||||
let (baseline, add, mut mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||
mult.factor *= bonus.value;
|
||||
eff.value = baseline.value * add.factor * mult.factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn revert_multiplicative_bonus<Stat: StatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(&MultiplicativeBonus<Stat>, &Parent)>,
|
||||
mut stat_q: Query<(
|
||||
&BaselineStat<Stat>,
|
||||
&AdditiveBonuses<Stat>,
|
||||
&mut MultiplicativeBonuses<Stat>,
|
||||
&mut EffectiveStat<Stat>,
|
||||
)>,
|
||||
) {
|
||||
for (bonus, player) in effect_q.iter_many(entities) {
|
||||
let (baseline, add, mut mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||
mult.factor /= bonus.value;
|
||||
eff.value = baseline.value * add.factor * mult.factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn create_stat_snapshots<Stat: StatMarker>(
|
||||
stat_q: Query<(
|
||||
Entity,
|
||||
&AdditiveBonuses<Stat>,
|
||||
&MultiplicativeBonuses<Stat>,
|
||||
&EffectiveStat<Stat>,
|
||||
)>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (stat, add, mult, eff) in stat_q.iter() {
|
||||
commands.entity(stat).insert(StatSnapshot::<Stat> {
|
||||
additive_bonuses: add.factor,
|
||||
multiplicative_bonuses: mult.factor,
|
||||
effective: eff.value,
|
||||
marker: PhantomData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_stats<Stat: StatMarker>(
|
||||
mut stat_q: Query<(
|
||||
&StatSnapshot<Stat>,
|
||||
&mut AdditiveBonuses<Stat>,
|
||||
&mut MultiplicativeBonuses<Stat>,
|
||||
&mut EffectiveStat<Stat>,
|
||||
)>,
|
||||
) {
|
||||
for (snapshot, mut add, mut mult, mut eff) in stat_q.iter_mut() {
|
||||
add.factor = snapshot.additive_bonuses;
|
||||
mult.factor = snapshot.multiplicative_bonuses;
|
||||
eff.value = snapshot.effective;
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_simple_stat_bonus<Stat: SimpleStatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(&SimpleStatBonus<Stat>, &Parent)>,
|
||||
mut stat_q: Query<&mut SimpleStatEffective<Stat>>,
|
||||
) {
|
||||
for (bonus, target) in effect_q.iter_many(entities) {
|
||||
let mut effective = stat_q.get_mut(target.get()).unwrap();
|
||||
effective.value = Stat::apply_bonus(effective.value, bonus.value);
|
||||
}
|
||||
}
|
||||
|
||||
fn revert_simple_stat_bonus<Stat: SimpleStatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(&SimpleStatBonus<Stat>, &Parent)>,
|
||||
mut stat_q: Query<&mut SimpleStatEffective<Stat>>,
|
||||
) {
|
||||
for (bonus, target) in effect_q.iter_many(entities) {
|
||||
let mut effective = stat_q.get_mut(target.get()).unwrap();
|
||||
effective.value = Stat::revert_bonus(effective.value, bonus.value);
|
||||
}
|
||||
}
|
||||
|
||||
fn create_simple_stat_snapshots<Stat: SimpleStatMarker>(
|
||||
stat_q: Query<(Entity, &SimpleStatEffective<Stat>)>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (stat, eff) in stat_q.iter() {
|
||||
commands.entity(stat).insert(SimpleStatSnapshot::<Stat> {
|
||||
value: eff.value,
|
||||
marker: PhantomData,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn restore_simple_stats<Stat: SimpleStatMarker>(
|
||||
mut stat_q: Query<(&mut SimpleStatEffective<Stat>, &SimpleStatSnapshot<Stat>)>,
|
||||
) {
|
||||
for (mut eff, snapshot) in stat_q.iter_mut() {
|
||||
eff.value = snapshot.value;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn configure(stages: &mut Stages) {
|
||||
fn register_stat_effects<Stat: StatMarker>(stages: &mut Stages) {
|
||||
stages
|
||||
.register_effect::<AdditiveBonus<Stat>>()
|
||||
.apply(add_additive_bonus::<Stat>)
|
||||
.teardown(revert_additive_bonus::<Stat>)
|
||||
.build();
|
||||
|
||||
stages
|
||||
.register_effect::<MultiplicativeBonus<Stat>>()
|
||||
.apply(add_multiplicative_bonus::<Stat>)
|
||||
.teardown(revert_multiplicative_bonus::<Stat>)
|
||||
.build();
|
||||
|
||||
stages.snapshot.add_systems(create_stat_snapshots::<Stat>);
|
||||
stages.restore.add_systems(restore_stats::<Stat>);
|
||||
}
|
||||
|
||||
fn register_simple_stat_effects<Stat: SimpleStatMarker>(stages: &mut Stages) {
|
||||
stages
|
||||
.register_effect::<SimpleStatBonus<Stat>>()
|
||||
.apply(apply_simple_stat_bonus::<Stat>)
|
||||
.teardown(revert_simple_stat_bonus::<Stat>)
|
||||
.build();
|
||||
|
||||
stages
|
||||
.snapshot
|
||||
.add_systems(create_simple_stat_snapshots::<Stat>);
|
||||
stages.restore.add_systems(restore_simple_stats::<Stat>);
|
||||
}
|
||||
|
||||
register_stat_effects::<Strength>(stages);
|
||||
register_stat_effects::<Defence>(stages);
|
||||
register_stat_effects::<Speed>(stages);
|
||||
register_stat_effects::<Dexterity>(stages);
|
||||
|
||||
register_simple_stat_effects::<CritRate>(stages);
|
||||
register_simple_stat_effects::<AmmoControl>(stages);
|
||||
register_simple_stat_effects::<DamageBonus>(stages);
|
||||
register_simple_stat_effects::<WeaponAccuracy>(stages);
|
||||
register_simple_stat_effects::<ClipSize>(stages);
|
||||
register_simple_stat_effects::<Health>(stages);
|
||||
}
|
||||
583
src/player/status_effect.rs
Normal file
583
src/player/status_effect.rs
Normal file
|
|
@ -0,0 +1,583 @@
|
|||
use std::{collections::VecDeque, marker::PhantomData};
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use macros::LogMessage;
|
||||
use rand::Rng as _;
|
||||
|
||||
use crate::{
|
||||
effect::{Effects, TimeLimitedEffect},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
log::Logger,
|
||||
weapon::temp::AssociatedWeapon,
|
||||
Rng, Stages,
|
||||
};
|
||||
|
||||
use super::stats::{
|
||||
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct StatusEffectStack<T> {
|
||||
pub effects: VecDeque<Entity>,
|
||||
pub bonus: Entity,
|
||||
pub marker: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct TempDebuffEffect<T>(std::marker::PhantomData<T>);
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct TempDebuffImmunity<T>(std::marker::PhantomData<T>);
|
||||
|
||||
pub trait DebuffingTempMarker: Send + Sync + 'static {
|
||||
type Stat: StatMarker;
|
||||
|
||||
fn factor() -> f32;
|
||||
fn duration() -> std::ops::Range<f32>;
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct TearGas;
|
||||
|
||||
impl DebuffingTempMarker for TearGas {
|
||||
type Stat = Dexterity;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 3.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
120.0..180.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct SmokeGrenade;
|
||||
|
||||
impl DebuffingTempMarker for SmokeGrenade {
|
||||
type Stat = Speed;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 3.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
120.0..180.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct PepperSpray;
|
||||
|
||||
impl DebuffingTempMarker for PepperSpray {
|
||||
type Stat = Dexterity;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 5.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
15.0..20.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct ConcussionGrenade;
|
||||
|
||||
impl DebuffingTempMarker for ConcussionGrenade {
|
||||
type Stat = Dexterity;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 5.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
15.0..20.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct FlashGrenade;
|
||||
|
||||
impl DebuffingTempMarker for FlashGrenade {
|
||||
type Stat = Speed;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 5.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
15.0..20.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct Sand;
|
||||
|
||||
impl DebuffingTempMarker for Sand {
|
||||
type Stat = Speed;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 5.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
15.0..20.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct LinkedComponents<const N: usize>([Entity; N]);
|
||||
|
||||
trait Stats<const N: usize> {
|
||||
fn spawn_additive_effects(
|
||||
effects: &mut Effects,
|
||||
target: Entity,
|
||||
value: f32,
|
||||
label: &'static str,
|
||||
) -> [Entity; N];
|
||||
}
|
||||
|
||||
impl<T> Stats<1> for T
|
||||
where
|
||||
T: StatMarker,
|
||||
{
|
||||
fn spawn_additive_effects(
|
||||
effects: &mut Effects,
|
||||
target: Entity,
|
||||
value: f32,
|
||||
label: &'static str,
|
||||
) -> [Entity; 1] {
|
||||
<(T,) as Stats<1>>::spawn_additive_effects(effects, target, value, label)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_n_stats {
|
||||
($n:expr, $($t:tt),+) => {
|
||||
impl<$($t,)+> Stats<$n> for ($($t,)+) where $($t: StatMarker,)+ {
|
||||
fn spawn_additive_effects(
|
||||
effects: &mut Effects,
|
||||
target: Entity,
|
||||
value: f32,
|
||||
label: &'static str,
|
||||
) -> [Entity; $n] {
|
||||
[$(effects.spawn(AdditiveBonus::<$t>::new(label, value), target),)+]
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_n_stats!(1, A);
|
||||
impl_n_stats!(2, A, B);
|
||||
impl_n_stats!(3, A, B, C);
|
||||
impl_n_stats!(4, A, B, C, D);
|
||||
|
||||
trait AdditiveStatusEffectMarker<const N: usize>: Send + Sync + 'static {
|
||||
type AffectedStats: Stats<N>;
|
||||
fn max_stack() -> usize;
|
||||
fn factor() -> f32;
|
||||
fn duration() -> f32;
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct AdditiveStatusEffect<const N: usize, M>
|
||||
where
|
||||
M: AdditiveStatusEffectMarker<N>,
|
||||
{
|
||||
marker: PhantomData<M>,
|
||||
extra_effectiveness: f32,
|
||||
extra_duration: f32,
|
||||
}
|
||||
|
||||
impl<const N: usize, M: AdditiveStatusEffectMarker<N>> Default for AdditiveStatusEffect<N, M> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
marker: PhantomData,
|
||||
extra_effectiveness: 0.0,
|
||||
extra_duration: 0.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize, M: AdditiveStatusEffectMarker<N>> AdditiveStatusEffect<N, M> {
|
||||
pub fn new(extra_effectiveness: f32, extra_duration: f32) -> Self {
|
||||
Self {
|
||||
marker: PhantomData,
|
||||
extra_effectiveness,
|
||||
extra_duration,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Withered;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Withered {
|
||||
type AffectedStats = Strength;
|
||||
fn max_stack() -> usize {
|
||||
3
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
-0.25
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Weakened;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Weakened {
|
||||
type AffectedStats = Defence;
|
||||
fn max_stack() -> usize {
|
||||
3
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
-0.25
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Slow;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Slow {
|
||||
type AffectedStats = Speed;
|
||||
fn max_stack() -> usize {
|
||||
3
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
-0.25
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Crippled;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Crippled {
|
||||
type AffectedStats = Dexterity;
|
||||
fn max_stack() -> usize {
|
||||
3
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
-0.25
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Motivate;
|
||||
|
||||
impl AdditiveStatusEffectMarker<4> for Motivate {
|
||||
type AffectedStats = (Strength, Defence, Speed, Dexterity);
|
||||
fn max_stack() -> usize {
|
||||
5
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
0.1
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Strengthened;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Strengthened {
|
||||
type AffectedStats = Strength;
|
||||
fn max_stack() -> usize {
|
||||
1
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
5.0
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
120.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Hardened;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Hardened {
|
||||
type AffectedStats = Defence;
|
||||
fn max_stack() -> usize {
|
||||
1
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
3.0
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
120.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Hastened;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Hastened {
|
||||
type AffectedStats = Speed;
|
||||
fn max_stack() -> usize {
|
||||
1
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
5.0
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
120.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Sharpened;
|
||||
|
||||
impl AdditiveStatusEffectMarker<1> for Sharpened {
|
||||
type AffectedStats = Dexterity;
|
||||
fn max_stack() -> usize {
|
||||
1
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
5.0
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
120.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Demoralise;
|
||||
|
||||
impl AdditiveStatusEffectMarker<4> for Demoralise {
|
||||
type AffectedStats = (Strength, Defence, Speed, Dexterity);
|
||||
fn max_stack() -> usize {
|
||||
5
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
-0.1
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Frozen;
|
||||
|
||||
impl AdditiveStatusEffectMarker<2> for Frozen {
|
||||
type AffectedStats = (Speed, Dexterity);
|
||||
fn max_stack() -> usize {
|
||||
1
|
||||
}
|
||||
fn factor() -> f32 {
|
||||
-0.5
|
||||
}
|
||||
fn duration() -> f32 {
|
||||
300.0
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(Entity, &Parent, &AdditiveStatusEffect<N, M>)>,
|
||||
mut parent_q: Query<Option<&mut StatusEffectStack<M>>>,
|
||||
mut commands: Commands,
|
||||
mut effects: Effects,
|
||||
) {
|
||||
for (entity, player, effect) in effect_q.iter_many(entities) {
|
||||
let stack = parent_q.get_mut(player.get()).unwrap();
|
||||
|
||||
let new_effects = <M::AffectedStats as Stats<N>>::spawn_additive_effects(
|
||||
&mut effects,
|
||||
player.get(),
|
||||
M::factor() * (1.0 + effect.extra_effectiveness),
|
||||
std::any::type_name::<M>(),
|
||||
);
|
||||
|
||||
commands.entity(entity).insert((
|
||||
LinkedComponents(new_effects),
|
||||
TimeLimitedEffect(M::duration() * (1.0 + effect.extra_duration)),
|
||||
));
|
||||
|
||||
if let Some(mut stack) = stack {
|
||||
stack.effects.push_back(entity);
|
||||
if stack.effects.len() > M::max_stack() {
|
||||
let first = stack.effects.pop_front().unwrap();
|
||||
effects.remove(first);
|
||||
}
|
||||
} else {
|
||||
commands.spawn(StatusEffectStack::<M> {
|
||||
effects: VecDeque::from([entity]),
|
||||
bonus: entity,
|
||||
marker: PhantomData,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
effect_q: Query<(Entity, &Parent)>,
|
||||
mut parent_q: Query<Option<&mut StatusEffectStack<M>>>,
|
||||
linked_q: Query<&LinkedComponents<N>>,
|
||||
mut effects: Effects,
|
||||
) {
|
||||
for (effect, player) in effect_q.iter_many(entities) {
|
||||
if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap() {
|
||||
if stack.effects.front() == Some(&effect) {
|
||||
stack.effects.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
let linked = linked_q.get(effect).unwrap();
|
||||
|
||||
for linked_effect in linked.0 {
|
||||
effects.remove(linked_effect);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_temp_debuff_effect<Temp: DebuffingTempMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
mut rng: ResMut<Rng>,
|
||||
temp_q: Query<(Entity, &Parent, &AssociatedWeapon)>,
|
||||
weapon_q: Query<&Parent>,
|
||||
mut parent_q: Query<(
|
||||
Option<&mut StatusEffectStack<Temp>>,
|
||||
Has<TempDebuffImmunity<Temp>>,
|
||||
)>,
|
||||
(mut commands, mut effects): (Commands, Effects),
|
||||
mut logger: Logger,
|
||||
) {
|
||||
#[derive(LogMessage)]
|
||||
pub struct UsedDebuffTemp {
|
||||
#[log(player)]
|
||||
pub actor: Entity,
|
||||
#[log(player)]
|
||||
pub recipient: Entity,
|
||||
#[log(weapon)]
|
||||
pub weapon: Entity,
|
||||
pub immune: bool,
|
||||
}
|
||||
|
||||
for (effect, player, weapon) in temp_q.iter_many(entities) {
|
||||
let (stack, immunity) = parent_q.get_mut(player.get()).unwrap();
|
||||
let user = weapon_q.get(weapon.0).unwrap();
|
||||
if immunity {
|
||||
commands.entity(effect).despawn();
|
||||
commands.entity(player.get()).remove_child(effect);
|
||||
logger.log(|| UsedDebuffTemp {
|
||||
actor: user.get(),
|
||||
recipient: player.get(),
|
||||
weapon: weapon.0,
|
||||
immune: true,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
let duration = rng.gen_range(Temp::duration());
|
||||
commands.entity(effect).insert(TimeLimitedEffect(duration));
|
||||
|
||||
let stack_size = stack.as_ref().map_or(0, |s| s.effects.len()) as i32;
|
||||
|
||||
let effective_factor = (0..=stack_size).fold(1.0, |acc, i| {
|
||||
acc * (1.0 - (1.0 - Temp::factor()) * 2.0f32.powi(-i))
|
||||
});
|
||||
|
||||
let bonus = effects.spawn(
|
||||
MultiplicativeBonus::<Temp::Stat>::new(std::any::type_name::<Temp>(), effective_factor),
|
||||
player.get(),
|
||||
);
|
||||
|
||||
if let Some(mut stack) = stack {
|
||||
effects.remove(stack.bonus);
|
||||
stack.bonus = bonus;
|
||||
stack.effects.push_back(effect);
|
||||
} else {
|
||||
commands
|
||||
.entity(player.get())
|
||||
.insert(StatusEffectStack::<Temp> {
|
||||
effects: VecDeque::from([effect]),
|
||||
bonus,
|
||||
marker: PhantomData,
|
||||
});
|
||||
}
|
||||
|
||||
logger.log(|| UsedDebuffTemp {
|
||||
actor: user.get(),
|
||||
recipient: player.get(),
|
||||
weapon: weapon.0,
|
||||
immune: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_temp_debuff_effect<Temp: DebuffingTempMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
temp_q: Query<&Parent>,
|
||||
mut parent_q: Query<(&mut StatusEffectStack<Temp>, Has<TempDebuffImmunity<Temp>>)>,
|
||||
mut commands: Commands,
|
||||
_logger: Logger,
|
||||
mut effects: Effects,
|
||||
) {
|
||||
#[derive(LogMessage)]
|
||||
struct RemovedDebuffTemp {
|
||||
#[log(player)]
|
||||
recipient: Entity,
|
||||
factor: f32,
|
||||
factor_remaining: f32,
|
||||
}
|
||||
|
||||
for player in temp_q.iter_many(entities) {
|
||||
let (mut stack, immunity) = parent_q.get_mut(player.get()).unwrap();
|
||||
if immunity {
|
||||
continue;
|
||||
}
|
||||
|
||||
effects.remove(stack.bonus);
|
||||
|
||||
let stack_size = (stack.effects.len() - 1) as i32;
|
||||
|
||||
if stack_size > 0 {
|
||||
let effective_factor = (0..=stack_size).fold(1.0, |acc, i| {
|
||||
acc * (1.0 - (1.0 - Temp::factor()) * 2.0f32.powi(-i))
|
||||
});
|
||||
|
||||
stack.bonus = effects.spawn(
|
||||
MultiplicativeBonus::<Temp::Stat>::new(
|
||||
std::any::type_name::<Temp>(),
|
||||
effective_factor,
|
||||
),
|
||||
player.get(),
|
||||
);
|
||||
|
||||
stack.effects.pop_front();
|
||||
} else {
|
||||
commands
|
||||
.entity(player.get())
|
||||
.remove::<StatusEffectStack<Temp>>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn register_debuff_temp<Temp: DebuffingTempMarker>(stages: &mut Stages) {
|
||||
stages
|
||||
.register_effect::<TempDebuffEffect<Temp>>()
|
||||
.apply(apply_temp_debuff_effect::<Temp>)
|
||||
.teardown(remove_temp_debuff_effect::<Temp>)
|
||||
.build();
|
||||
}
|
||||
|
||||
fn register_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(stages: &mut Stages) {
|
||||
stages
|
||||
.register_effect::<AdditiveStatusEffect<N, M>>()
|
||||
.apply(apply_additive_status_effect::<N, M>)
|
||||
.teardown(remove_additive_status_effect::<N, M>)
|
||||
.build();
|
||||
}
|
||||
|
||||
pub(crate) fn configure(stages: &mut Stages) {
|
||||
register_debuff_temp::<TearGas>(stages);
|
||||
register_debuff_temp::<SmokeGrenade>(stages);
|
||||
register_debuff_temp::<PepperSpray>(stages);
|
||||
register_debuff_temp::<ConcussionGrenade>(stages);
|
||||
register_debuff_temp::<FlashGrenade>(stages);
|
||||
register_debuff_temp::<Sand>(stages);
|
||||
|
||||
register_status_effect::<1, Withered>(stages);
|
||||
register_status_effect::<1, Weakened>(stages);
|
||||
register_status_effect::<1, Slow>(stages);
|
||||
register_status_effect::<1, Crippled>(stages);
|
||||
register_status_effect::<1, Strengthened>(stages);
|
||||
register_status_effect::<1, Hardened>(stages);
|
||||
register_status_effect::<1, Hastened>(stages);
|
||||
register_status_effect::<1, Sharpened>(stages);
|
||||
register_status_effect::<4, Motivate>(stages);
|
||||
register_status_effect::<4, Demoralise>(stages);
|
||||
register_status_effect::<2, Frozen>(stages);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue