This commit is contained in:
TotallyNot 2025-11-03 16:36:45 +01:00
parent e7d6b74aab
commit 35413b563c
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
33 changed files with 10238 additions and 1891 deletions

View file

@ -1,224 +1,59 @@
use bevy_ecs::prelude::*;
use proxisim_models::{
bundle::{
armour::{ArmourBodyPart, ArmourBodyParts},
passive::{FactionUpgrades, Merits},
player::{
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated,
Defender, FightEndType, Level, MaxHealth, PartDamageBonus, Player, PlayerStrategy,
Weapons,
},
stat::{
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health,
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
},
weapon::{
Ammo, DamageStat, NeedsReload, NonTargeted, RateOfFire, Usable, Uses, Weapon,
WeaponSlot,
},
},
hierarchy::Children,
};
use rand::Rng as _;
use strum::Display;
use crate::{
armour,
FightStatus, Rng, Stages,
effect::Effects,
hierarchy::Children,
log,
log::Logger,
metrics::Metrics,
passives::{EducationPartDamageBonus, FactionUpgrades, Merits},
weapon::{
bonus::{BonusPartDamageBonus, 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,
weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus},
};
pub mod stats;
pub mod status_effect;
#[derive(Component)]
pub struct Attacker;
fn select_weapon(
weapons: &Weapons,
slot: WeaponSlot,
reload: bool,
usable_q: &Query<(Has<NeedsReload>, Option<&Children>), With<Usable>>,
) -> Option<(Entity, Option<Children>)> {
let id = match slot {
WeaponSlot::Primary => weapons.primary?,
WeaponSlot::Secondary => weapons.secondary?,
WeaponSlot::Melee => weapons.melee?,
WeaponSlot::Temporary => weapons.temporary?,
WeaponSlot::Fists => weapons.fists,
WeaponSlot::Kick => weapons.kick,
};
#[derive(Component)]
pub struct Defender;
let (needs_reload, children) = usable_q.get(id).ok()?;
#[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(Clone, Copy, PartialEq, Eq, Display)]
pub enum FightEndType {
Victory,
Stalemate,
Loss,
}
#[derive(Component)]
pub enum PartDamageBonus {
Education(EducationPartDamageBonus),
WeaponBonus {
value: f32,
bonus: BonusPartDamageBonus,
},
}
impl PartDamageBonus {
pub fn dmg_bonus(&self, part: BodyPart) -> Option<f32> {
match self {
Self::Education(edu) => edu.dmg_bonus(part),
Self::WeaponBonus { value, bonus } => bonus.dmg_bonus(part, *value),
}
}
}
#[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(),
}
if !reload && needs_reload {
None
} else {
Some((id, children.cloned()))
}
}
@ -250,7 +85,7 @@ fn designate_first(
mut commands: Commands,
) {
let attacker = attacker_q.iter().next().unwrap();
let defender = defender_q.single();
let defender = defender_q.single().unwrap();
commands.entity(attacker).insert(Current);
commands.entity(defender).insert(CurrentTarget);
}
@ -260,8 +95,8 @@ fn change_roles(
target_q: Query<Entity, With<CurrentTarget>>,
mut commands: Commands,
) {
let current = current_q.single();
let target = target_q.single();
let current = current_q.single().unwrap();
let target = target_q.single().unwrap();
// TODO: Group fights
commands
@ -291,36 +126,38 @@ pub fn pick_action(
(With<Current>, With<Player>),
>,
target_q: Query<Entity, With<CurrentTarget>>,
usable_q: Query<(Has<NeedsReload>, &Children), With<Usable>>,
usable_q: Query<(Has<NeedsReload>, Option<&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 (current, weapons, strat, mut turns) = p_query.single_mut().unwrap();
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::AlwaysFists => (weapons.fists, None),
PlayerStrategy::AlwaysKicks => select_weapon(weapons, WeaponSlot::Kick, true, &usable_q)
.unwrap_or_else(|| (weapons.fists, Default::default())),
PlayerStrategy::PrimaryMelee { reload } => {
select_weapon(weapons, WeaponSlot::Primary, *reload, &usable_q)
.or_else(|| select_weapon(weapons, WeaponSlot::Melee, true, &usable_q))
.unwrap_or_else(|| (weapons.fists, 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())),
.find_map(|slot| select_weapon(weapons, *slot, *reload, &usable_q))
.unwrap_or_else(|| (weapons.fists, Default::default())),
};
metrics.increment_counter(current, "turn", 1);
metrics.increment_counter(weapon, "turn", 1);
metrics.increment_counter(Some(current), "turn", 1);
metrics.increment_counter(Some(weapon), "turn", 1);
commands.entity(weapon).insert(Current);
let target = target_q.single();
let target = target_q.single().unwrap();
for effect in weapon_trigger_q.iter_many(children.get()) {
effect.trigger(&mut effects, current, target);
if let Some(children) = children {
for effect in weapon_trigger_q.iter_many(children.get()) {
effect.trigger(&mut effects, current, target);
}
}
turns.0 += 1;
@ -368,12 +205,12 @@ pub fn use_damaging_weapon(
Entity,
&EffectiveStat<Dexterity>,
&EffectiveStat<Defence>,
&armour::ArmourBodyParts,
&ArmourBodyParts,
&mut SimpleStatEffective<Health>,
),
With<CurrentTarget>,
>,
armour_q: Query<&armour::ArmourBodyPart>,
armour_q: Query<&ArmourBodyPart>,
(damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>),
(mut ammo_q, mut temp_q): (
Query<(
@ -392,13 +229,13 @@ pub fn use_damaging_weapon(
Effects,
),
) {
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.get_single()
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.single()
else {
return;
};
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) =
player_q.single();
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut();
player_q.single().unwrap();
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut().unwrap();
if let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1;
@ -448,10 +285,10 @@ pub fn use_damaging_weapon(
+ 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());
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());
ammo.0 -= rounds;
if ammo.0 == 0 {
if clips.value == 0 {
@ -469,7 +306,7 @@ pub fn use_damaging_weapon(
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
};
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
if hit_chance <= 1.0 && !rng.random_bool(hit_chance as f64) {
log!(logger, "miss_target", {
weapon: weapon,
actor: player,
@ -477,8 +314,8 @@ pub fn use_damaging_weapon(
rounds: rounds,
hit_chance: hit_chance,
});
metrics.increment_counter(player, "miss", 1);
metrics.increment_counter(weapon, "miss", 1);
metrics.increment_counter(Some(player), "miss", 1);
metrics.increment_counter(Some(weapon), "miss", 1);
if multi_attack_proc.is_none() {
return;
@ -492,8 +329,8 @@ pub fn use_damaging_weapon(
let mult = match body_part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
metrics.increment_counter(player, "crit", 1);
metrics.increment_counter(weapon, "crit", 1);
metrics.increment_counter(Some(player), "crit", 1);
metrics.increment_counter(Some(weapon), "crit", 1);
1.0
}
BodyPart::LeftHand
@ -506,8 +343,8 @@ pub fn use_damaging_weapon(
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
};
metrics.increment_counter(player, "hit", 1);
metrics.increment_counter(weapon, "hit", 1);
metrics.increment_counter(Some(player), "hit", 1);
metrics.increment_counter(Some(weapon), "hit", 1);
let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap();
let piece = rng.sample(armour_parts);
@ -530,15 +367,15 @@ pub fn use_damaging_weapon(
let dmg = dmg_intrinsic
* w_dmg.0
* dmg_bonus.value
* (1.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);
metrics.record_histogram(Some(player), "dmg", dmg);
metrics.record_histogram(Some(weapon), "dmg", dmg);
if dmg > 0 {
for effect in damage_proc_q.iter_many(children.get()) {
@ -548,7 +385,7 @@ pub fn use_damaging_weapon(
continue;
}
let chance = (value / 100.0) as f64;
if chance > 1.0 || rng.gen_bool(chance) {
if chance > 1.0 || rng.random_bool(chance) {
match bonus {
MultiTurnBonus::Blindfire => {
multi_attack_proc = Some(MultiAttack::Blindfire)
@ -559,26 +396,26 @@ pub fn use_damaging_weapon(
}
MultiTurnBonus::Rage => {
multi_attack_proc =
Some(MultiAttack::Rage(rng.gen_range(2..=8)))
Some(MultiAttack::Rage(rng.random_range(2..=8)))
}
MultiTurnBonus::DoubleTap => {
multi_attack_proc =
Some(MultiAttack::DoubleTap { first_shot: true })
}
};
metrics.increment_counter(player, bonus.counter_label(), 1);
metrics.increment_counter(weapon, bonus.counter_label(), 1);
metrics.increment_counter(Some(player), bonus.counter_label(), 1);
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.gen_bool(chance) {
if chance > 1.0 || rng.random_bool(chance) {
bonus.spawn(player, &mut effects);
}
}
DamageProcEffect::OpponentEffect { value, bonus } => {
let chance = (value / 100.0) as f64;
if chance > 1.0 || rng.gen_bool(chance) {
if chance > 1.0 || rng.random_bool(chance) {
bonus.spawn(target, &mut effects, &mut rng.0);
}
}
@ -592,19 +429,20 @@ pub fn use_damaging_weapon(
log!(logger, "hit_target", {
actor: player,
acc: acc_eff.value,
recipient: target,
weapon: weapon,
weapon,
part: %body_part,
part_mult: mult,
dmg: dmg,
rounds: rounds,
rounds,
health_before: health_before,
health_after: health.value,
dmg_spread: dmg_spread,
dmg_intrinsic: dmg_intrinsic,
armour_mitigation: armour_mitigation,
def_mitigation: def_mitigation,
weapon_dmg: w_dmg.0,
dmg,
dmg_spread,
dmg_intrinsic,
dmg_weapon: w_dmg.0,
armour_mitigation,
def_mitigation,
bonus_dmg: dmg_bonus.value,
hit_chance: hit_chance,
crit_rate: crit.value,
@ -623,17 +461,23 @@ pub fn use_damaging_weapon(
FightEndType::Loss
},
});
metrics.increment_counter(player, "victory", 1);
metrics.increment_counter(Some(player), "victory", 1);
}
}
// Technically only douple tap and blindfire have this condition, but we can run into with
// invalid bonus/weapon combinations without checking this for all bonuses
if ammo.as_ref().is_some_and(|(a, _, _)| a.0 == 0) {
break;
}
match multi_attack_proc {
Some(MultiAttack::Blindfire) => {
if !ammo.as_ref().map(|(a, _, _)| a.0 != 0).unwrap_or_default() {
acc_eff.value -= 5.0 / 50.0;
// Prevent infinite loop if used on a melee
if ammo.is_none() {
break;
}
acc_eff.value -= 5.0 / 50.0;
}
Some(MultiAttack::Fury { first_hit: true }) => {
multi_attack_proc = Some(MultiAttack::Fury { first_hit: false })
@ -658,7 +502,7 @@ pub fn check_stalemate(
mut logger: Logger,
metrics: Res<Metrics>,
) {
let (current, current_turns, attacker) = current_q.single();
let (current, current_turns, attacker) = current_q.single().unwrap();
if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker {
commands.entity(current).insert(Defeated);
let target = target_q.single();
@ -668,7 +512,7 @@ pub fn check_stalemate(
recipient: target,
fight_end_type: %FightEndType::Stalemate,
});
metrics.increment_counter(current, "stalemate", 1);
metrics.increment_counter(Some(current), "stalemate", 1);
if other_attackers_q.is_empty() {
*state = FightStatus::Over
@ -705,7 +549,7 @@ fn record_post_fight_stats(
metrics: Res<Metrics>,
) {
for (player, health) in player_q.iter() {
metrics.record_histogram(player, "rem_health", health.value as u32);
metrics.record_histogram(Some(player), "rem_health", health.value as u32);
}
}

View file

@ -1,411 +1,17 @@
use std::marker::PhantomData;
use bevy_ecs::prelude::*;
use proxisim_models::{
bundle::stat::{
AdditiveBonus, AdditiveBonuses, AmmoControl, BaselineStat, ClipSize, Clips, CritRate,
DamageBonus, Defence, Dexterity, EffectiveStat, Health, MultiplicativeBonus,
MultiplicativeBonuses, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker,
SimpleStatSnapshot, Speed, StatMarker, StatSnapshot, Strength, WeaponAccuracy,
},
hierarchy::Parent,
};
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;
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value
}
}
#[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
}
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value * 100.0
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value * 100.0
}
}
#[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
}
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value * 100.0
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value * 100.0
}
}
#[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
}
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
value * 50.0 + 50.0
}
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
value * 50.0
}
}
#[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,
}
}
}
use crate::Stages;
fn add_additive_bonus<Stat: StatMarker>(
In(entities): In<Vec<Entity>>,

View file

@ -1,19 +1,20 @@
use std::{collections::VecDeque, marker::PhantomData};
use bevy_ecs::prelude::*;
use proxisim_models::{
bundle::stat::{
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
},
hierarchy::Parent,
};
use rand::Rng as _;
use crate::{
Rng, Stages,
effect::{Effects, TimeLimitedEffect},
hierarchy::{HierarchyBuilder, Parent},
log,
log::Logger,
weapon::temp::AssociatedWeapon,
Rng, Stages,
};
use super::stats::{
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
};
#[derive(Component)]
@ -101,19 +102,6 @@ impl DebuffingTempMarker for FlashGrenade {
}
}
#[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]);
@ -136,7 +124,7 @@ where
value: f32,
label: &'static str,
) -> [Entity; 1] {
<(T,) as Stats<1>>::spawn_additive_effects(effects, target, value, label)
[effects.spawn(AdditiveBonus::<T>::new(label, value), target)]
}
}
@ -155,7 +143,7 @@ macro_rules! impl_n_stats {
};
}
impl_n_stats!(1, A);
// 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);
@ -407,10 +395,10 @@ fn remove_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<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();
}
if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap()
&& stack.effects.front() == Some(&effect)
{
stack.effects.pop_front();
}
let linked = linked_q.get(effect).unwrap();
@ -448,7 +436,7 @@ fn apply_temp_debuff_effect<Temp: DebuffingTempMarker>(
continue;
}
let duration = rng.gen_range(Temp::duration());
let duration = rng.random_range(Temp::duration());
commands.entity(effect).insert(TimeLimitedEffect(duration));
let stack_size = stack.as_ref().map_or(0, |s| s.effects.len()) as i32;
@ -547,7 +535,6 @@ pub(crate) fn configure(stages: &mut 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);