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,23 +1,32 @@
[workspace] [workspace]
resolver = "2" resolver = "2"
members = [".", "macros"] members = [".", "macros", "models"]
[package] [package]
name = "proxisim" name = "proxisim"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2024"
[features] [features]
default = ["json"] default = ["json"]
debug = ["bevy_ecs/bevy_debug_stepping"]
json = ["dep:serde", "dep:serde_json"] json = ["dep:serde", "dep:serde_json"]
[dependencies] [dependencies]
bevy_ecs = "0.12.1" bevy_ecs = { version = "0.17.2", features = [] }
rand = { version = "0.8.5", default-features = false, features = ["std", "alloc", "small_rng"] } rand = { version = "0.9.2", default-features = false, features = [
rand_distr = "0.4.3" "std",
strum = { version = "0.25.0", features = ["derive"] } "alloc",
serde = { version = "1", features = [ "derive" ], optional = true } "small_rng",
"os_rng",
] }
rand_distr = "0.5.1"
strum = { version = "0.27.2", features = ["derive"] }
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
[dependencies.macros] [dependencies.macros]
path = "macros" path = "macros"
[dependencies.proxisim_models]
path = "models"

15
models/Cargo.toml Normal file
View file

@ -0,0 +1,15 @@
[package]
name = "proxisim_models"
version = "0.1.0"
edition = "2024"
[features]
json = ["dep:serde_json", "dep:serde"]
default = ["json"]
[dependencies]
serde = { version = "1", features = ["derive"], optional = true }
serde_json = { version = "1", optional = true }
bevy_ecs = "0.17.2"
rand = { version = "0.9.2", default-features = false }
strum = { version = "0.27.2", features = ["derive"] }

200
models/src/bundle/armour.rs Normal file
View file

@ -0,0 +1,200 @@
use bevy_ecs::{component::Component, entity::Entity};
use rand::distr::Distribution;
use strum::Display;
use crate::{bundle::player::BodyPart, dto::armour::CoverageDto};
#[derive(Clone, Copy, Debug, Display, Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum Immunity {
Radiation,
NerveGas,
TearGas,
PepperSpray,
FlashGrenade,
ConcussionGrenade,
Insanity,
}
#[derive(Component, Default)]
pub struct Immunities(pub Vec<Immunity>);
#[derive(Component, Default)]
pub struct Armour;
#[derive(Component, Default)]
pub struct ArmourCoverage(pub ArmourVec<f32>);
impl From<CoverageDto> for ArmourCoverage {
fn from(value: CoverageDto) -> Self {
let mut result = Self::default();
result.0[ArmourBodyPartSlot::Heart] = value.heart;
result.0[ArmourBodyPartSlot::Stomach] = value.stomach;
result.0[ArmourBodyPartSlot::Chest] = value.chest;
result.0[ArmourBodyPartSlot::Arms] = value.arm;
result.0[ArmourBodyPartSlot::Groin] = value.groin;
result.0[ArmourBodyPartSlot::Legs] = value.leg;
result.0[ArmourBodyPartSlot::Throat] = value.throat;
result.0[ArmourBodyPartSlot::Hands] = value.hand;
result.0[ArmourBodyPartSlot::Feet] = value.foot;
result.0[ArmourBodyPartSlot::Head] = value.head;
result
}
}
#[derive(Component, Default)]
pub struct ArmourValue(pub f32);
enum ArmourIterState {
Head,
Body,
Legs,
Feet,
Hands,
}
#[derive(Debug, Clone, Component)]
pub struct PlayerArmour {
pub head: Option<Entity>,
pub torso: Option<Entity>,
pub legs: Option<Entity>,
pub feet: Option<Entity>,
pub hands: Option<Entity>,
}
#[derive(Debug)]
pub struct BodyPartCoverage {
pub armour: Entity,
pub coverage: f32,
pub armour_value: f32,
}
#[derive(Component, Default, Debug)]
pub struct ArmourBodyPart {
pub armour_pieces: Vec<BodyPartCoverage>,
}
#[derive(Component, Debug)]
pub struct ArmourBodyParts(pub ArmourVec<Entity>);
pub struct ArmourIter<'a> {
state: Option<ArmourIterState>,
equipped_armour: &'a PlayerArmour,
}
impl<'a> Iterator for ArmourIter<'a> {
type Item = Entity;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (next, piece) = match self.state {
None => (ArmourIterState::Head, self.equipped_armour.head),
Some(ArmourIterState::Head) => (ArmourIterState::Body, self.equipped_armour.torso),
Some(ArmourIterState::Body) => (ArmourIterState::Legs, self.equipped_armour.legs),
Some(ArmourIterState::Legs) => (ArmourIterState::Feet, self.equipped_armour.feet),
Some(ArmourIterState::Feet) => (ArmourIterState::Hands, self.equipped_armour.hands),
Some(ArmourIterState::Hands) => return None,
};
self.state = Some(next);
if piece.is_some() {
return piece;
}
}
}
}
impl<'a> IntoIterator for &'a PlayerArmour {
type Item = Entity;
type IntoIter = ArmourIter<'a>;
fn into_iter(self) -> Self::IntoIter {
ArmourIter {
state: None,
equipped_armour: self,
}
}
}
#[repr(usize)]
#[derive(Clone, Copy, strum::EnumIter)]
pub enum ArmourBodyPartSlot {
Arms,
Stomach,
Heart,
Chest,
Throat,
Hands,
Groin,
Legs,
Head,
Feet,
}
impl From<BodyPart> for ArmourBodyPartSlot {
fn from(value: BodyPart) -> Self {
match value {
BodyPart::LeftArm | BodyPart::RightArm => Self::Arms,
BodyPart::Stomach => Self::Stomach,
BodyPart::Heart => Self::Heart,
BodyPart::Chest => Self::Chest,
BodyPart::Throat => Self::Throat,
BodyPart::LeftHand | BodyPart::RightHand => Self::Hands,
BodyPart::Groin => Self::Groin,
BodyPart::LeftLeg | BodyPart::RightLeg => Self::Legs,
BodyPart::Head => Self::Head,
BodyPart::LeftFoot | BodyPart::RightFoot => Self::Feet,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ArmourVec<T>(pub [T; 10]);
impl<T> std::ops::Index<ArmourBodyPartSlot> for ArmourVec<T> {
type Output = T;
fn index(&self, index: ArmourBodyPartSlot) -> &Self::Output {
&self.0[index as usize]
}
}
impl<T> std::ops::IndexMut<ArmourBodyPartSlot> for ArmourVec<T> {
fn index_mut(&mut self, index: ArmourBodyPartSlot) -> &mut Self::Output {
&mut self.0[index as usize]
}
}
impl<T> IntoIterator for ArmourVec<T> {
type Item = T;
type IntoIter = std::array::IntoIter<T, 10>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> Distribution<Option<&'a BodyPartCoverage>> for &'a ArmourBodyPart {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a BodyPartCoverage> {
let mut current = None;
for piece in &self.armour_pieces {
// NOTE: This is not strictly speaking correct, but the edge cases where this applies
// should be very rare, and it should be a decent enough heuristic to barely make a
// difference from the actual pixel comparisons that torn seems to be using for this.
if current
.map(|c: &BodyPartCoverage| c.armour_value)
.unwrap_or_default()
< piece.armour_value
&& rng.random_bool(piece.coverage as f64)
{
current = Some(piece);
}
}
current
}
}

199
models/src/bundle/bonus.rs Normal file
View file

@ -0,0 +1,199 @@
use bevy_ecs::{bundle::Bundle, component::Component};
use strum::Display;
use crate::bundle::player::BodyPart;
#[derive(Component, Debug, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponBonusType {
// Weapon passives
Berserk,
Conserve,
Expose,
Grace,
Powerful,
Specialist,
// Turn triggered passives
Empower,
Quicken,
// First turn effects
Assassinate,
// Additive status effects triggered by damaging hits
Cripple,
Demoralise,
Freeze,
Motivate,
Slow,
Toxin,
Weaken,
Wither,
// DOT status effects
Bleed,
Burning,
Lacerate,
Poison,
SevereBurning,
// Other status effects
Eviscerate,
Paralyse,
Schock,
Stun,
// Multi attack bonuses
Blindfire,
Fury,
DoubleTap,
Rage,
// Body part multipliers
Achilles,
Crusher,
Cupid,
Deadeye,
Roshambo,
Throttle,
// Attack nullification types
Homerun,
Parry,
}
#[derive(Component)]
pub struct BonusValue(pub f32);
#[derive(Bundle)]
pub struct WeaponBonusBundle {
pub bonus: WeaponBonusType,
pub value: BonusValue,
}
impl WeaponBonusBundle {
pub fn new(bonus: WeaponBonusType, value: f32) -> Self {
Self {
bonus,
value: BonusValue(value),
}
}
}
#[derive(Clone, Copy)]
pub enum TurnTriggeredBonus {
Empower,
Quicken,
}
#[derive(Clone, Copy)]
pub enum FirstTurnBonus {
Assassinate,
}
#[derive(Clone, Copy)]
pub enum MultiTurnBonus {
Blindfire,
Fury,
DoubleTap,
Rage,
}
impl MultiTurnBonus {
#[inline(always)]
pub fn counter_label(self) -> &'static str {
match self {
Self::Blindfire => "proc_blindfire",
Self::Fury => "proc_fury",
Self::DoubleTap => "proc_double_tap",
Self::Rage => "proc_rage",
}
}
}
#[derive(Clone, Copy)]
pub enum OpponentStatusEffect {
Cripple,
// TODO: implement for group fights
Demoralise,
Freeze,
Slow,
Toxin,
Weaken,
Wither,
}
#[derive(Clone, Copy)]
pub enum SelfStatusEffect {
Motivate,
}
#[derive(Clone, Copy)]
pub enum BonusPartDamageBonus {
Achilles,
Crusher,
Cupid,
Deadeye,
Roshambo,
Throttle,
}
impl BonusPartDamageBonus {
pub fn dmg_bonus(self, part: BodyPart, value: f32) -> Option<f32> {
match self {
Self::Achilles => match part {
BodyPart::LeftFoot | BodyPart::RightFoot => Some(value),
_ => None,
},
Self::Crusher => {
if part == BodyPart::Head {
Some(value)
} else {
None
}
}
Self::Cupid => {
if part == BodyPart::Heart {
Some(value)
} else {
None
}
}
Self::Deadeye => match part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => Some(value),
_ => None,
},
Self::Roshambo => {
if part == BodyPart::Groin {
Some(value)
} else {
None
}
}
Self::Throttle => {
if part == BodyPart::Throat {
Some(value)
} else {
None
}
}
}
}
}
#[derive(Clone, Copy, Debug, Display)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum ArmourBonusType {
Impregnable,
Impenetrable,
Insurmountable,
Invulnerable,
Imperviable,
Immmutable,
Irrepressible,
Kinetokinesis,
Impassable,
}

14
models/src/bundle/mod.rs Normal file
View file

@ -0,0 +1,14 @@
use bevy_ecs::component::Component;
pub mod armour;
pub mod bonus;
pub mod passive;
pub mod player;
pub mod stat;
pub mod weapon;
#[derive(Component, Debug, Default)]
pub struct Name(pub String);
#[derive(Component, Debug, Default, Clone, Copy)]
pub struct Id(pub usize);

View file

@ -0,0 +1,207 @@
use bevy_ecs::{bundle::Bundle, component::Component};
use crate::bundle::player::BodyPart;
#[derive(Component, Default)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Merits {
pub life: u16,
pub crits: u16,
pub brawn: u16,
pub protection: u16,
pub sharpness: u16,
pub evasion: u16,
pub heavy_artillery_mastery: u16,
pub machine_gun_mastery: u16,
pub rifle_mastery: u16,
pub smg_mastery: u16,
pub shotgun_mastery: u16,
pub pistol_mastery: u16,
pub club_mastery: u16,
pub piercing_mastery: u16,
pub slashing_mastery: u16,
pub mechanical_mastery: u16,
pub temporary_mastery: u16,
}
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Education {
/// Gain a 1% damage bonus to all weapons
pub bio2350: bool,
/// Gain a 10% damage increase when hitting an opponent's throat
pub bio2380: bool,
/// Gain a 3% chance increase of achieving a critical hit
pub bio2410: bool,
/// Gain a 1% passive bonus to speed
pub cbt2790: bool,
/// Gain a +1.00 accuracy increase with Machine Guns
pub cbt2820: bool,
/// Gain a +1.00 accuracy increase with Submachine guns
pub cbt2830: bool,
/// Gain a +1.00 accuracy increase with Pistols
pub cbt2840: bool,
/// Gain a +1.00 accuracy increase with Rifles
pub cbt2850: bool,
/// Gain a +1.00 accuracy increase with Heavy Artillery
pub cbt2860: bool,
/// Gain a +1.00 accuracy increase with Shotguns
pub cbt2125: bool,
/// Gain a +1.00 accuracy increase with Temporary weapons
pub gen2116: bool,
/// Gain a 5% damage increase with Temporary weapons
pub gen2119: bool,
/// Gain a 1% passive bonus to dexterity
pub haf2104: bool,
/// Gain a 1% passive bonus to speed
pub haf2105: bool,
/// Gain a 1% passive bonus to strength
pub haf2106: bool,
/// Gain a 2% passive bonus to strength
pub haf2107: bool,
/// Gain a 1% passive bonus to dexterity
pub haf2108: bool,
/// Gain a 3% passive bonus to speed
pub haf2109: bool,
/// Gain a 10% damage increase with Japanese blade weapons
pub his2160: bool,
/// Gain a 2% bonus to all melee damage
pub his2170: bool,
/// Gain a 1% passive bonus to speed
pub mth2240: bool,
/// Gain a 1% passive bonus to speed
pub mth2250: bool,
/// Gain a 1% passive bonus to defense
pub mth2260: bool,
/// Gain a 2% passive bonus to defense
pub mth2320: bool,
/// Gain a 5% bonus to ammo conservation
pub mth2310: bool,
/// Gain a 20% bonus to ammo conservation
pub mth3330: bool,
/// Gain a 1% passive bonus to dexterity
pub psy2640: bool,
/// Gain a 2% passive bonus to dexterity
pub psy2650: bool,
/// Gain a 4% passive bonus to dexterity
pub psy2660: bool,
/// Gain an 8% passive bonus to dexterity
pub psy2670: bool,
/// Gain a 1% passive bonus to defense
pub def2710: bool,
/// Gain a 2% passive bonus to defense
pub def2730: bool,
/// Gain a 3% passive bonus to defense
pub def2740: bool,
/// Gain a 2% passive bonus to speed
pub def2750: bool,
/// Gain a 3% passive bonus to speed
pub def2760: bool,
/// Gain a 100% increase in damage dealt when using fists alone
pub def3770: bool,
/// Gain a 10% increase in steroid effectiveness
// NOTE: this effect is additive with the strip club perks
pub spt2480: bool,
/// Gain a 2% passive bonus to speed and strength
pub spt2490: bool,
/// Gain a 2% passive bonus to defense and dexterity
pub spt2500: bool,
}
impl Default for Education {
fn default() -> Self {
Self {
bio2350: true,
bio2380: true,
bio2410: true,
cbt2790: true,
cbt2820: true,
cbt2830: true,
cbt2840: true,
cbt2850: true,
cbt2860: true,
cbt2125: true,
gen2116: true,
gen2119: true,
haf2104: true,
haf2105: true,
haf2106: true,
haf2107: true,
haf2108: true,
haf2109: true,
his2160: true,
his2170: true,
mth2240: true,
mth2250: true,
mth2260: true,
mth2320: true,
mth2310: true,
mth3330: true,
psy2640: true,
psy2650: true,
psy2660: true,
psy2670: true,
def2710: true,
def2730: true,
def2740: true,
def2750: true,
def2760: true,
def3770: true,
spt2480: true,
spt2490: true,
spt2500: true,
}
}
}
#[derive(Component, Default)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct FactionUpgrades {
pub str: u16,
pub spd: u16,
pub def: u16,
pub dex: u16,
pub life: u16,
pub acc: u16,
pub dmg: u16,
pub side_effects: u16,
}
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub enum DrugCooldown {
Xanax,
Vicodin,
}
#[derive(Clone, Copy)]
pub enum EducationPartDamageBonus {
Bio2380,
}
impl EducationPartDamageBonus {
pub fn dmg_bonus(self, part: BodyPart) -> Option<f32> {
match part {
BodyPart::Throat => Some(0.10),
_ => None,
}
}
}
#[derive(Bundle, Default)]
pub(crate) struct PassiveBundle {
pub merits: Merits,
pub education: Education,
pub faction: FactionUpgrades,
}

184
models/src/bundle/player.rs Normal file
View file

@ -0,0 +1,184 @@
use bevy_ecs::{bundle::Bundle, component::Component, entity::Entity, message::Message};
use strum::Display;
use crate::bundle::{
Name,
bonus::BonusPartDamageBonus,
passive::EducationPartDamageBonus,
stat::{CritRate, DamageBonus, SimpleStatBundle, WeaponAccuracy},
weapon::WeaponSlot,
};
#[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, Debug)]
pub struct Weapons {
pub primary: Option<Entity>,
pub secondary: Option<Entity>,
pub melee: Option<Entity>,
pub temporary: Option<Entity>,
pub fists: Entity,
pub kick: Entity,
}
#[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(Message)]
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,
},
}
#[derive(Message)]
pub enum HealthRestore {
Cauterise,
Serotonin { extra_effectiveness: f32 },
Bloodlust { dmg: u32, value: f32 },
}
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 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, level: u16, strategy: PlayerStrategy) -> Self {
Self {
name: Name(name.to_string()),
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(),
}
}
}

408
models/src/bundle/stat.rs Normal file
View file

@ -0,0 +1,408 @@
use std::marker::PhantomData;
use bevy_ecs::{bundle::Bundle, component::Component};
use crate::bundle::player::BodyPart;
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)]
pub struct SimpleStatSnapshot<Stat: SimpleStatMarker> {
pub value: Stat::ValueType,
pub marker: PhantomData<Stat>,
}
#[derive(Bundle)]
pub struct SimpleStatBundle<Stat: SimpleStatMarker> {
pub baseline: SimpleStatBaseline<Stat>,
pub 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 rand::distr::Distribution<BodyPart> for SimpleStatEffective<CritRate> {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> BodyPart {
if rng.random_ratio((self.value) as u32, 200) {
match rng.random_range(1..=10) {
1 => BodyPart::Heart,
2 => BodyPart::Throat,
_ => BodyPart::Heart,
}
} else {
match rng.random_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,
}
}
}
}
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,
}
}
}
#[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> {
pub baseline: BaselineStat<Stat>,
pub additive: AdditiveBonuses<Stat>,
pub multiplicative: MultiplicativeBonuses<Stat>,
pub effective: EffectiveStat<Stat>,
}
#[derive(Component)]
pub struct StatSnapshot<Stat: StatMarker> {
pub additive_bonuses: f32,
pub multiplicative_bonuses: f32,
pub effective: f32,
pub 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,
}
}
}

230
models/src/bundle/weapon.rs Normal file
View file

@ -0,0 +1,230 @@
use bevy_ecs::{bundle::Bundle, component::Component};
use strum::Display;
use crate::{
bundle::{
Name,
stat::{
AmmoControl, ClipSize, Clips, CritRate, DamageBonus, SimpleStatBundle, WeaponAccuracy,
},
},
dto::weapon::{WeaponAmmo, WeaponDto},
};
#[derive(Component)]
pub struct Usable;
#[derive(Component)]
pub struct Weapon;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponSlot {
Primary,
Secondary,
Melee,
Temporary,
Fists,
Kick,
}
#[derive(Component, Debug, Clone, Copy)]
pub enum WeaponVerb {
Hit,
Kicked,
Fired,
Threw,
Exploded,
}
#[derive(Component, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponCategory {
HeavyArtillery,
MachineGun,
Rifle,
Smg,
Shotgun,
Pistol,
Clubbing,
Piercing,
Slashing,
Mechanical,
Temporary,
HandToHand,
}
#[derive(Component, Debug)]
pub struct DamageStat(pub f32);
#[derive(Component)]
pub struct Japanese;
#[derive(Component)]
pub struct Ammo(pub u16);
#[derive(Component)]
pub struct RateOfFire(pub [u16; 2]);
#[derive(Component)]
pub struct NeedsReload;
#[derive(Component, Default)]
pub struct NonTargeted;
#[derive(Component, Default)]
pub struct Temporary;
#[derive(Component)]
pub struct Uses(pub u16);
impl Default for Uses {
fn default() -> Self {
Self(1)
}
}
#[derive(Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponMod {
ReflexSight,
HolographicSight,
AcogSight,
ThermalSight,
Laser1mw,
Laser5mw,
Laser30mw,
Laser100mw,
SmallSuppressor,
StandardSuppressor,
LargeSuppressor,
ExtendedMags,
HighCapacityMags,
ExtraClip,
ExtraClip2,
AdjustableTrigger,
HairTrigger,
Bipod,
Tripod,
CustomGrip,
SkeetChoke,
ImprovedChoke,
FullChoke,
RecoilPad,
StandardBrake,
HeavyDutyBrake,
TacticalBrake,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
#[derive(Clone, Copy)]
pub enum TurnTriggeredMod {
Bipod,
Tripod,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
#[derive(Component, Debug, Clone, Copy, Display)]
pub enum DebuffingTemp {
TearGas,
SmokeGrenade,
PepperSpray,
ConcussionGrenade,
FlashGrenade,
}
#[derive(Component, Debug, Clone, Copy, Display)]
pub enum BuffingTemp {
Serotonin,
Tyrosine,
Melatonin,
Epinephrine,
}
#[derive(Component)]
pub struct EquippedMods(pub Vec<WeaponMod>);
#[derive(Component)]
pub struct Experience(pub f32);
#[derive(Bundle)]
pub struct WeaponBundle {
pub usable: Usable,
pub weapon: Weapon,
pub name: Name,
pub verb: WeaponVerb,
pub slot: WeaponSlot,
}
#[derive(Bundle)]
pub struct DamagingWeaponBundle {
pub crit_rate: SimpleStatBundle<CritRate>,
pub dmg: DamageStat,
pub acc: SimpleStatBundle<WeaponAccuracy>,
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
pub equipped_mods: EquippedMods,
pub experience: Experience,
pub category: WeaponCategory,
}
#[derive(Bundle)]
pub struct AmmoWeaponBundle {
pub ammo: Ammo,
pub clips: SimpleStatBundle<Clips>,
pub clip_size: SimpleStatBundle<ClipSize>,
pub rate_of_fire: RateOfFire,
pub ammo_control: SimpleStatBundle<AmmoControl>,
}
impl WeaponBundle {
pub fn new(name: String, verb: WeaponVerb, slot: WeaponSlot) -> Self {
Self {
usable: Usable,
weapon: Weapon,
name: Name(name),
verb,
slot,
}
}
pub fn fists() -> Self {
Self::new("Fists".to_owned(), WeaponVerb::Hit, WeaponSlot::Fists)
}
pub fn kick() -> Self {
Self::new("Kick".to_owned(), WeaponVerb::Kicked, WeaponSlot::Kick)
}
}
impl AmmoWeaponBundle {
pub fn new(weapon_ammo: &WeaponAmmo) -> Self {
Self {
ammo: Ammo(weapon_ammo.clip_size),
clips: SimpleStatBundle::new(2),
clip_size: SimpleStatBundle::new(weapon_ammo.clip_size),
rate_of_fire: RateOfFire(weapon_ammo.rate_of_fire),
ammo_control: SimpleStatBundle::new(0.0),
}
}
}
impl DamagingWeaponBundle {
pub fn new(dto: &WeaponDto) -> Self {
Self {
crit_rate: SimpleStatBundle::new(0),
dmg: DamageStat(dto.dmg.unwrap_or(dto.base_dmg) / 10.0),
acc: SimpleStatBundle::new((dto.acc.unwrap_or(dto.base_acc) - 50.0) / 50.0),
dmg_bonus: SimpleStatBundle::new(0.0),
equipped_mods: EquippedMods(dto.mods.iter().filter_map(|m| *m).collect()),
experience: Experience(dto.experience.unwrap_or_default()),
category: dto.cat,
}
}
}

2122
models/src/dto/armour.rs Normal file

File diff suppressed because it is too large Load diff

32
models/src/dto/metrics.rs Normal file
View file

@ -0,0 +1,32 @@
#[cfg_attr(feature = "json", derive(serde::Serialize))]
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
#[derive(Debug, Clone)]
pub enum EntityInfo {
Player {
name: String,
id: usize,
is_attacker: bool,
},
Weapon {
name: String,
owner: usize,
id: usize,
},
Global,
}
#[cfg_attr(feature = "json", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Counter {
pub value: u64,
pub entity: EntityInfo,
pub label: &'static str,
}
#[cfg_attr(feature = "json", derive(serde::Serialize))]
#[derive(Debug)]
pub struct Histogram {
pub values: Vec<u32>,
pub entity: EntityInfo,
pub label: &'static str,
}

14
models/src/dto/mod.rs Normal file
View file

@ -0,0 +1,14 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use crate::bundle::Id;
pub mod armour;
pub mod metrics;
pub mod player;
pub mod weapon;
static ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
fn draw_id() -> Id {
Id(ID_COUNTER.fetch_add(1, Ordering::Relaxed))
}

107
models/src/dto/player.rs Normal file
View file

@ -0,0 +1,107 @@
use bevy_ecs::prelude::*;
use crate::{
bundle::{
armour::PlayerArmour,
passive::{DrugCooldown, Education, FactionUpgrades, Merits, PassiveBundle},
player::{PlayerBundle, PlayerStrategy, Weapons},
stat::{Defence, Dexterity, Speed, StatBundle, Strength},
},
dto::{armour::ArmourDto, draw_id, weapon::WeaponDto},
};
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct EquippedWeapons {
pub primary: Option<WeaponDto>,
pub secondary: Option<WeaponDto>,
pub melee: Option<WeaponDto>,
pub temporary: Option<WeaponDto>,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[derive(Default)]
pub struct EquippedArmour {
pub helmet: Option<ArmourDto>,
pub body: Option<ArmourDto>,
pub pants: Option<ArmourDto>,
pub gloves: Option<ArmourDto>,
pub boots: Option<ArmourDto>,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Stats {
pub str: f32,
pub def: f32,
pub spd: f32,
pub dex: f32,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct PlayerDto {
pub name: String,
pub level: u16,
pub stats: Stats,
pub merits: Option<Merits>,
pub education: Option<Education>,
pub weapons: EquippedWeapons,
pub armour: EquippedArmour,
pub faction: Option<FactionUpgrades>,
pub drug: Option<DrugCooldown>,
pub strategy: PlayerStrategy,
}
impl PlayerDto {
pub fn spawn(self, world: &mut World) -> EntityWorldMut<'_> {
let primary = self.weapons.primary.map(|p| p.spawn(world));
let secondary = self.weapons.secondary.map(|s| s.spawn(world));
let melee = self.weapons.melee.map(|m| m.spawn(world));
let temporary = self.weapons.temporary.map(|m| m.spawn(world));
let fists = WeaponDto::FISTS.spawn(world);
let kick = WeaponDto::KITCHEN_KNIFE.spawn(world);
let head = self.armour.helmet.map(|a| a.spawn(world));
let torso = self.armour.body.map(|a| a.spawn(world));
let legs = self.armour.pants.map(|a| a.spawn(world));
let hands = self.armour.gloves.map(|a| a.spawn(world));
let feet = self.armour.boots.map(|a| a.spawn(world));
let mut commands = world.spawn(draw_id());
commands.insert((
PlayerBundle::new(self.name, self.level, self.strategy),
StatBundle::<Strength>::new(self.stats.str),
StatBundle::<Defence>::new(self.stats.def),
StatBundle::<Speed>::new(self.stats.spd),
StatBundle::<Dexterity>::new(self.stats.dex),
));
let education = self.education.unwrap_or_default();
let merits = self.merits.unwrap_or_default();
let faction = self.faction.unwrap_or_default();
commands.insert(PassiveBundle {
education,
merits,
faction,
});
commands.insert(Weapons {
primary,
secondary,
melee,
temporary,
kick,
fists,
});
commands.insert(PlayerArmour {
torso,
head,
legs,
feet,
hands,
});
commands
}
}

5609
models/src/dto/weapon.rs Normal file

File diff suppressed because it is too large Load diff

178
models/src/hierarchy.rs Normal file
View file

@ -0,0 +1,178 @@
use bevy_ecs::{
prelude::*,
system::{Command, EntityCommands},
};
#[derive(Component, Debug, Clone, Copy)]
pub struct Parent(Entity);
impl Parent {
pub fn get(&self) -> Entity {
self.0
}
}
#[derive(Component, Debug, Clone, Default)]
pub struct Children(Vec<Entity>);
impl Children {
pub fn get(&self) -> &[Entity] {
&self.0
}
}
struct AddChild {
parent: Entity,
child: Entity,
}
impl Command for AddChild {
fn apply(self, world: &mut World) {
let mut parent = world.entity_mut(self.parent);
if let Some(mut children) = parent.get_mut::<Children>() {
children.0.push(self.child);
} else {
parent.insert(Children(vec![self.child]));
}
}
}
struct AddChildren {
parent: Entity,
children: Vec<Entity>,
}
impl Command for AddChildren {
fn apply(mut self, world: &mut World) {
let mut parent = world.entity_mut(self.parent);
if let Some(mut children) = parent.get_mut::<Children>() {
children.0.append(&mut self.children);
} else {
parent.insert(Children(self.children));
}
}
}
struct RemoveChild {
parent: Entity,
child: Entity,
}
impl Command for RemoveChild {
fn apply(self, world: &mut World) {
let mut parent = world.entity_mut(self.parent);
let mut children = parent
.get_mut::<Children>()
.expect("Parent component has no children");
children.0.retain(|child| *child != self.child);
}
}
pub trait HierarchyBuilder {
fn add_child(&mut self, child: Entity) -> &mut Self;
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self;
fn remove_child(&mut self, child: Entity) -> &mut Self;
fn set_parent(&mut self, parent: Entity) -> &mut Self;
}
impl<'a> HierarchyBuilder for EntityCommands<'a> {
fn add_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id();
self.commands().queue(AddChild { parent, child });
self.commands().entity(child).insert(Parent(parent));
self
}
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self {
let children = children.as_ref();
let parent = self.id();
self.commands().queue(AddChildren {
parent,
children: children.to_owned(),
});
self.commands().insert_batch(
children
.iter()
.map(|e| (*e, Parent(parent)))
.collect::<Vec<_>>(),
);
self
}
fn remove_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id();
self.commands().queue(RemoveChild { parent, child });
self.commands().entity(child).remove::<Parent>();
self
}
fn set_parent(&mut self, parent: Entity) -> &mut Self {
let child = self.id();
self.commands().queue(AddChild { parent, child });
self.commands().entity(child).insert(Parent(parent));
self
}
}
impl<'w> HierarchyBuilder for EntityWorldMut<'w> {
fn add_child(&mut self, child: Entity) -> &mut Self {
let parent_id = self.id();
unsafe {
self.world_mut()
.entity_mut(child)
.insert(Parent(parent_id))
.update_location();
}
if let Some(mut children) = self.get_mut::<Children>() {
children.0.push(child);
self
} else {
self.insert(Children(vec![child]))
}
}
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self {
let parent_id = self.id();
unsafe {
for child in children.as_ref() {
self.world_mut()
.entity_mut(*child)
.insert(Parent(parent_id))
.update_location();
}
}
if let Some(mut old_children) = self.get_mut::<Children>() {
old_children.0.append(&mut children.as_ref().to_owned());
self
} else {
self.insert(Children(children.as_ref().to_owned()))
}
}
fn remove_child(&mut self, child: Entity) -> &mut Self {
unsafe {
self.world_mut()
.entity_mut(child)
.remove::<Parent>()
.update_location();
}
if let Some(mut children) = self.get_mut::<Children>() {
children.0.retain(|c| *c != child);
}
self
}
fn set_parent(&mut self, parent: Entity) -> &mut Self {
let child_id = self.id();
unsafe {
self.world_mut()
.entity_mut(parent)
.add_child(child_id)
.update_location()
}
self
}
}

3
models/src/lib.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod bundle;
pub mod dto;
// pub mod hierarchy;

View file

@ -1,227 +1,22 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use rand::distributions::Distribution; use proxisim_models::{
bundle::armour::{
ArmourBodyPart, ArmourBodyPartSlot, ArmourBodyParts, ArmourCoverage, ArmourValue,
ArmourVec, BodyPartCoverage, Immunities, Immunity, PlayerArmour,
},
hierarchy::HierarchyBuilder,
};
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::{ use crate::{
hierarchy::HierarchyBuilder, player::status_effect::{
player::{ ConcussionGrenade, FlashGrenade, PepperSpray, TearGas, TempDebuffImmunity,
status_effect::{
ConcussionGrenade, FlashGrenade, PepperSpray, Sand, TearGas, TempDebuffImmunity,
},
BodyPart,
}, },
Name, Stages, Stages,
}; };
#[derive(Component, Default)]
pub struct Armour;
#[derive(Debug)]
pub struct BodyPartCoverage {
pub armour: Entity,
pub coverage: f32,
pub armour_value: f32,
}
#[derive(Component, Default)]
pub struct ArmourCoverage(ArmourVec<f32>);
#[derive(Component, Default)]
pub struct ArmourValue(pub f32);
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum Set {
Riot,
Assault,
Dune,
Delta,
Vanguard,
Sentinel,
Marauder,
Eod,
}
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum Immunity {
Radiation,
TearGas,
PepperSpray,
NerveGas,
Sand,
ConcussionGrenades,
FlashGrenades,
}
#[derive(Component)]
pub struct Immunities(pub Vec<Immunity>);
#[derive(Bundle, Default)]
pub struct ArmourBundle {
pub name: Name,
pub armour: Armour,
pub coverage: ArmourCoverage,
pub armour_value: ArmourValue,
}
impl ArmourBundle {
pub fn new(name: String, coverage: [f32; 10], armour_value: f32) -> Self {
Self {
name: Name(name),
armour: Armour,
coverage: ArmourCoverage(ArmourVec(coverage)),
armour_value: ArmourValue(armour_value / 100.0),
}
}
}
#[derive(Component, Default, Debug)]
pub struct ArmourBodyPart {
pub armour_pieces: Vec<BodyPartCoverage>,
}
#[derive(Component, Default)]
pub struct EquippedArmour {
pub head: Option<Entity>,
pub body: Option<Entity>,
pub legs: Option<Entity>,
pub feet: Option<Entity>,
pub hands: Option<Entity>,
}
#[derive(Component, Debug)]
pub struct ArmourBodyParts(pub ArmourVec<Entity>);
enum ArmourIterState {
Head,
Body,
Legs,
Feet,
Hands,
}
pub struct ArmourIter<'a> {
state: ArmourIterState,
equipped_armour: &'a EquippedArmour,
}
impl<'a> Iterator for ArmourIter<'a> {
type Item = Entity;
fn next(&mut self) -> Option<Self::Item> {
loop {
let (next, piece) = match self.state {
ArmourIterState::Head => (ArmourIterState::Body, self.equipped_armour.head),
ArmourIterState::Body => (ArmourIterState::Legs, self.equipped_armour.body),
ArmourIterState::Legs => (ArmourIterState::Feet, self.equipped_armour.legs),
ArmourIterState::Feet => (ArmourIterState::Hands, self.equipped_armour.feet),
ArmourIterState::Hands => return self.equipped_armour.hands,
};
self.state = next;
if piece.is_some() {
return piece;
}
}
}
}
impl<'a> IntoIterator for &'a EquippedArmour {
type Item = Entity;
type IntoIter = ArmourIter<'a>;
fn into_iter(self) -> Self::IntoIter {
ArmourIter {
state: ArmourIterState::Head,
equipped_armour: self,
}
}
}
#[repr(usize)]
#[derive(Clone, Copy, strum::EnumIter)]
pub enum ArmourBodyPartSlot {
Arms,
Stomach,
Heart,
Chest,
Throat,
Hands,
Groin,
Legs,
Head,
Feet,
}
impl From<BodyPart> for ArmourBodyPartSlot {
fn from(value: BodyPart) -> Self {
match value {
BodyPart::LeftArm | BodyPart::RightArm => Self::Arms,
BodyPart::Stomach => Self::Stomach,
BodyPart::Heart => Self::Heart,
BodyPart::Chest => Self::Chest,
BodyPart::Throat => Self::Throat,
BodyPart::LeftHand | BodyPart::RightHand => Self::Hands,
BodyPart::Groin => Self::Groin,
BodyPart::LeftLeg | BodyPart::RightLeg => Self::Legs,
BodyPart::Head => Self::Head,
BodyPart::LeftFoot | BodyPart::RightFoot => Self::Feet,
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct ArmourVec<T>([T; 10]);
impl<T> std::ops::Index<ArmourBodyPartSlot> for ArmourVec<T> {
type Output = T;
fn index(&self, index: ArmourBodyPartSlot) -> &Self::Output {
&self.0[index as usize]
}
}
impl<T> std::ops::IndexMut<ArmourBodyPartSlot> for ArmourVec<T> {
fn index_mut(&mut self, index: ArmourBodyPartSlot) -> &mut Self::Output {
&mut self.0[index as usize]
}
}
impl<T> IntoIterator for ArmourVec<T> {
type Item = T;
type IntoIter = std::array::IntoIter<T, 10>;
fn into_iter(self) -> Self::IntoIter {
self.0.into_iter()
}
}
impl<'a> Distribution<Option<&'a BodyPartCoverage>> for &'a ArmourBodyPart {
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> Option<&'a BodyPartCoverage> {
let mut current = None;
for piece in &self.armour_pieces {
// NOTE: This is not strictly speaking correct, but the edge cases where this applies
// should be very rare, and it should be a decent enough heuristic to barely make a
// difference from the actual pixel comparisons that torn seems to be using for this.
if current
.map(|c: &BodyPartCoverage| c.armour_value)
.unwrap_or_default()
< piece.armour_value
&& rng.gen_bool(piece.coverage as f64)
{
current = Some(piece);
}
}
current
}
}
fn generate_body_parts( fn generate_body_parts(
equip_q: Query<(Entity, &EquippedArmour)>, equip_q: Query<(Entity, &PlayerArmour)>,
armour_q: Query<(Entity, &ArmourCoverage, &ArmourValue, Option<&Immunities>)>, armour_q: Query<(Entity, &ArmourCoverage, &ArmourValue, Option<&Immunities>)>,
mut commands: Commands, mut commands: Commands,
) { ) {
@ -246,15 +41,15 @@ fn generate_body_parts(
Immunity::PepperSpray => { Immunity::PepperSpray => {
player.insert(TempDebuffImmunity::<PepperSpray>::default()); player.insert(TempDebuffImmunity::<PepperSpray>::default());
} }
Immunity::FlashGrenades => { Immunity::FlashGrenade => {
player.insert(TempDebuffImmunity::<FlashGrenade>::default()); player.insert(TempDebuffImmunity::<FlashGrenade>::default());
} }
Immunity::Sand => { Immunity::ConcussionGrenade => {
player.insert(TempDebuffImmunity::<Sand>::default());
}
Immunity::ConcussionGrenades => {
player.insert(TempDebuffImmunity::<ConcussionGrenade>::default()); player.insert(TempDebuffImmunity::<ConcussionGrenade>::default());
} }
Immunity::Insanity => {
// TODO: Insanity
}
} }
} }
} }
@ -264,7 +59,7 @@ fn generate_body_parts(
parts[slot].armour_pieces.push(BodyPartCoverage { parts[slot].armour_pieces.push(BodyPartCoverage {
armour, armour,
coverage: coverage.0[slot] / 100.0, coverage: coverage.0[slot] / 100.0,
armour_value: armour_value.0, armour_value: armour_value.0 / 100.0,
}); });
} }
} }
@ -274,7 +69,7 @@ fn generate_body_parts(
commands commands
.entity(player) .entity(player)
.add_children(parts) .add_children(&parts)
.insert(ArmourBodyParts(ArmourVec(parts))); .insert(ArmourBodyParts(ArmourVec(parts)));
} }
} }

177
src/attacker.json Normal file
View file

@ -0,0 +1,177 @@
{
"level": 100,
"name": "Pyrit",
"stats": {
"str": 1500203325,
"spd": 1547656240,
"def": 1597091496,
"dex": 2618494699
},
"strategy": {
"type": "in_order",
"order": [
"temporary",
"primary"
],
"reload": true
},
"education": {
"bio2350": true,
"bio2380": true,
"bio2410": true,
"cbt2790": false,
"cbt2820": true,
"cbt2830": true,
"cbt2840": true,
"cbt2850": true,
"cbt2860": true,
"cbt2125": true,
"gen2116": true,
"gen2119": true,
"haf2104": false,
"haf2105": false,
"haf2106": false,
"haf2107": false,
"haf2108": false,
"haf2109": false,
"his2160": true,
"his2170": true,
"mth2240": false,
"mth2250": false,
"mth2260": false,
"mth2320": false,
"mth2310": true,
"mth3330": true,
"psy2640": false,
"psy2650": false,
"psy2660": false,
"psy2670": false,
"def2710": false,
"def2730": false,
"def2740": false,
"def2750": false,
"def2760": false,
"def3770": true,
"spt2480": true,
"spt2490": false,
"spt2500": false
},
"merits": {
"life": 10,
"crits": 10,
"brawn": 0,
"protection": 0,
"sharpness": 0,
"evasion": 0,
"heavy_artillery_mastery": 0,
"machine_gun_mastery": 0,
"rifle_mastery": 5,
"smg_mastery": 10,
"shotgun_mastery": 0,
"pistol_mastery": 0,
"club_mastery": 10,
"piercing_mastery": 0,
"slashing_mastery": 0,
"mechanical_mastery": 0,
"temporary_mastery": 0
},
"faction": {
"str": 0,
"def": 0,
"spd": 0,
"dex": 0,
"life": 0,
"dmg": 0,
"acc": 0,
"side_effects": 10
},
"weapons": {
"primary": {
"id": 488,
"name": "MP 40",
"kind": "primary",
"cat": "smg",
"base_dmg": 37,
"base_acc": 41,
"dmg": 5,
"acc": 41,
"ammo": {
"clip_size": 32,
"rate_of_fire": [
3,
5
]
},
"mods": [
"high_capacity_mags",
null
],
"bonuses": [
{
"bonus": "specialist",
"value": 10
},
null
],
"compatible_mods": [
"reflex_sight",
"holographic_sight",
"acog_sight",
"thermal_sight",
"laser1mw",
"laser5mw",
"laser30mw",
"laser100mw",
"small_suppressor",
"standard_suppressor",
"large_suppressor",
"extended_mags",
"high_capacity_mags",
"extra_clip",
"extra_clip2",
"adjustable_trigger",
"hair_trigger",
"custom_grip",
"standard_brake",
"heavy_duty_brake",
"tactical_brake",
"small_light",
"precision_light",
"tactical_illuminator"
],
"experience": 0,
"japanese": false
},
"secondary": null,
"melee": null,
"temporary": {
"id": 464,
"name": "Melatonin",
"kind": "temporary",
"cat": "temporary",
"base_dmg": 0,
"base_acc": 0,
"dmg": 0,
"acc": 0,
"ammo": null,
"mods": [
null,
null
],
"bonuses": [
null,
null
],
"compatible_mods": [],
"experience": 0,
"japanese": false
}
},
"armour": {
"helmet": null,
"body": null,
"pants": null,
"gloves": null,
"boots": null
}
}

97
src/defender.json Normal file
View file

@ -0,0 +1,97 @@
{
"level": 100,
"name": "Evil Pyrit",
"stats": {
"str": 1415286156,
"spd": 1344108854,
"def": 1425974550,
"dex": 2182078916
},
"strategy": {
"type": "in_order",
"order": [],
"reload": false
},
"education": {
"bio2350": true,
"bio2380": true,
"bio2410": true,
"cbt2790": true,
"cbt2820": true,
"cbt2830": true,
"cbt2840": true,
"cbt2850": true,
"cbt2860": true,
"cbt2125": true,
"gen2116": true,
"gen2119": true,
"haf2104": true,
"haf2105": true,
"haf2106": true,
"haf2107": true,
"haf2108": true,
"haf2109": true,
"his2160": true,
"his2170": true,
"mth2240": true,
"mth2250": true,
"mth2260": true,
"mth2320": true,
"mth2310": true,
"mth3330": true,
"psy2640": true,
"psy2650": true,
"psy2660": true,
"psy2670": true,
"def2710": true,
"def2730": true,
"def2740": true,
"def2750": true,
"def2760": true,
"def3770": true,
"spt2480": true,
"spt2490": true,
"spt2500": true
},
"merits": {
"life": 10,
"crits": 10,
"brawn": 10,
"protection": 10,
"sharpness": 10,
"evasion": 10,
"heavy_artillery_mastery": 0,
"machine_gun_mastery": 0,
"rifle_mastery": 5,
"smg_mastery": 10,
"shotgun_mastery": 0,
"pistol_mastery": 0,
"club_mastery": 0,
"piercing_mastery": 0,
"slashing_mastery": 0,
"mechanical_mastery": 0,
"temporary_mastery": 0
},
"faction": {
"str": 0,
"def": 0,
"spd": 0,
"dex": 0,
"life": 0,
"dmg": 0,
"acc": 0,
"side_effects": 10
},
"weapons": {
"primary": null,
"secondary": null,
"melee": null
},
"armour": {
"helmet": null,
"body": null,
"pants": null,
"gloves": null,
"boots": null
}
}

View file

@ -1,6 +1,6 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use crate::entity_registry::EntityInfo; pub use crate::entity_registry::EntityInfo;
pub use crate::passives::{DrugCooldown, Education, FactionUpgrades, Merits}; pub use crate::passives::{DrugCooldown, Education, FactionUpgrades, Merits};
pub use crate::player::PlayerStrategy; pub use crate::player::PlayerStrategy;
use crate::weapon::Japanese; use crate::weapon::Japanese;

View file

@ -1,21 +1,21 @@
use std::{any::TypeId, collections::HashMap, sync::Mutex}; use std::{any::TypeId, collections::HashMap, sync::Mutex};
use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_ecs::{prelude::*, system::SystemParam};
use proxisim_models::{
use crate::{ bundle::player::{Current, Defender, Player},
hierarchy::{HierarchyBuilder, Parent}, hierarchy::{HierarchyBuilder, Parent},
player::{Current, Defender, Player},
Stages,
}; };
use crate::Stages;
const SECONDS_PER_ACTION: f32 = 1.1; const SECONDS_PER_ACTION: f32 = 1.1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component)]
pub struct EffectId(usize); pub struct EffectId(usize);
pub struct EffectInfo { pub struct EffectInfo {
pub apply: Box<dyn System<In = Vec<Entity>, Out = ()>>, pub apply: Box<dyn System<In = In<Vec<Entity>>, Out = ()>>,
pub teardown: Box<dyn System<In = Vec<Entity>, Out = ()>>, pub teardown: Box<dyn System<In = In<Vec<Entity>>, Out = ()>>,
} }
// TODO: remove need for unsafe code by splitting the system info from this struct // TODO: remove need for unsafe code by splitting the system info from this struct
@ -114,8 +114,8 @@ impl<'w, 's> Effects<'w, 's> {
pub struct EffectBuilder<'w> { pub struct EffectBuilder<'w> {
world: &'w mut World, world: &'w mut World,
apply: Option<Box<dyn System<In = Vec<Entity>, Out = ()>>>, apply: Option<Box<dyn System<In = In<Vec<Entity>>, Out = ()>>>,
teardown: Option<Box<dyn System<In = Vec<Entity>, Out = ()>>>, teardown: Option<Box<dyn System<In = In<Vec<Entity>>, Out = ()>>>,
id: EffectId, id: EffectId,
} }
@ -123,7 +123,7 @@ impl<'r> EffectBuilder<'r> {
#[must_use] #[must_use]
pub fn apply<A, S>(&mut self, system: S) -> &mut Self pub fn apply<A, S>(&mut self, system: S) -> &mut Self
where where
S: IntoSystem<Vec<Entity>, (), A> + 'static, S: IntoSystem<In<Vec<Entity>>, (), A> + 'static,
{ {
let mut system = IntoSystem::into_system(system); let mut system = IntoSystem::into_system(system);
system.initialize(self.world); system.initialize(self.world);
@ -134,7 +134,7 @@ impl<'r> EffectBuilder<'r> {
#[must_use] #[must_use]
pub fn teardown<T, S>(&mut self, system: S) -> &mut Self pub fn teardown<T, S>(&mut self, system: S) -> &mut Self
where where
S: IntoSystem<Vec<Entity>, (), T> + 'static, S: IntoSystem<In<Vec<Entity>>, (), T> + 'static,
{ {
let mut system = IntoSystem::into_system(system); let mut system = IntoSystem::into_system(system);
system.initialize(self.world); system.initialize(self.world);
@ -174,7 +174,7 @@ pub(crate) fn run_effects(world: &mut World) {
let unsafe_world = world.as_unsafe_world_cell(); let unsafe_world = world.as_unsafe_world_cell();
let mut registry = unsafe_world.get_resource_mut::<EffectRegistry>().unwrap(); let mut registry = unsafe_world.get_resource_mut::<EffectRegistry>().unwrap();
let info = registry.system_info.get_mut(&next_effect).unwrap(); let info = registry.system_info.get_mut(&next_effect).unwrap();
info.apply.run(entities, unsafe_world.world_mut()); info.apply.run(entities, unsafe_world.world_mut()).unwrap();
info.apply.apply_deferred(unsafe_world.world_mut()); info.apply.apply_deferred(unsafe_world.world_mut());
}; };
} }
@ -196,7 +196,8 @@ pub(crate) fn run_effects(world: &mut World) {
let mut registry = unsafe_world.get_resource_mut::<EffectRegistry>().unwrap(); let mut registry = unsafe_world.get_resource_mut::<EffectRegistry>().unwrap();
let info = registry.system_info.get_mut(&next_effect).unwrap(); let info = registry.system_info.get_mut(&next_effect).unwrap();
info.teardown info.teardown
.run(entities.clone(), unsafe_world.world_mut()); .run(entities.clone(), unsafe_world.world_mut())
.unwrap();
info.teardown.apply_deferred(unsafe_world.world_mut()); info.teardown.apply_deferred(unsafe_world.world_mut());
}; };
@ -209,7 +210,7 @@ pub(crate) fn run_effects(world: &mut World) {
} }
#[must_use] #[must_use]
pub(crate) fn register_effect<Effect: 'static>(stages: &mut Stages) -> EffectBuilder { pub(crate) fn register_effect<Effect: 'static>(stages: &mut Stages) -> EffectBuilder<'_> {
let mut registry = stages.world.resource_mut::<EffectRegistry>(); let mut registry = stages.world.resource_mut::<EffectRegistry>();
let id = EffectId(registry.id_counter); let id = EffectId(registry.id_counter);
registry.id_counter += 1; registry.id_counter += 1;
@ -245,7 +246,7 @@ fn advance_clock(
mut clock: ResMut<Clock>, mut clock: ResMut<Clock>,
current_q: Query<Has<Defender>, (With<Current>, With<Player>)>, current_q: Query<Has<Defender>, (With<Current>, With<Player>)>,
) { ) {
let is_defender = current_q.single(); let is_defender = current_q.single().unwrap();
if !is_defender { if !is_defender {
clock.0 += SECONDS_PER_ACTION; clock.0 += SECONDS_PER_ACTION;
} }

View file

@ -1,29 +1,17 @@
use std::collections::HashMap; use std::collections::HashMap;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::{
use crate::{ bundle::{
player::{Attacker, Player},
weapon::Weapon,
Id, Name,
},
dto::metrics::EntityInfo,
hierarchy::Parent, hierarchy::Parent,
player::{Attacker, Player},
weapon::Weapon,
Id, Name, Stages,
}; };
#[cfg_attr(feature = "json", derive(serde::Serialize))] use crate::Stages;
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
#[derive(Debug, Clone)]
pub enum EntityInfo {
Player {
name: String,
id: usize,
is_attacker: bool,
},
Weapon {
name: String,
owner: usize,
id: usize,
},
}
#[derive(Resource, Default)] #[derive(Resource, Default)]
pub struct EntityRegistry(pub HashMap<Entity, EntityInfo>); pub struct EntityRegistry(pub HashMap<Entity, EntityInfo>);

View file

@ -78,10 +78,10 @@ pub trait HierarchyBuilder {
fn set_parent(&mut self, parent: Entity) -> &mut Self; fn set_parent(&mut self, parent: Entity) -> &mut Self;
} }
impl<'w, 's, 'a> HierarchyBuilder for EntityCommands<'w, 's, 'a> { impl<'a> HierarchyBuilder for EntityCommands<'a> {
fn add_child(&mut self, child: Entity) -> &mut Self { fn add_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id(); let parent = self.id();
self.commands().add(AddChild { parent, child }); self.commands().queue(AddChild { parent, child });
self.commands().entity(child).insert(Parent(parent)); self.commands().entity(child).insert(Parent(parent));
self self
} }
@ -89,11 +89,11 @@ impl<'w, 's, 'a> HierarchyBuilder for EntityCommands<'w, 's, 'a> {
fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self { fn add_children(&mut self, children: impl AsRef<[Entity]>) -> &mut Self {
let children = children.as_ref(); let children = children.as_ref();
let parent = self.id(); let parent = self.id();
self.commands().add(AddChildren { self.commands().queue(AddChildren {
parent, parent,
children: children.to_owned(), children: children.to_owned(),
}); });
self.commands().insert_or_spawn_batch( self.commands().insert_batch(
children children
.iter() .iter()
.map(|e| (*e, Parent(parent))) .map(|e| (*e, Parent(parent)))
@ -104,14 +104,14 @@ impl<'w, 's, 'a> HierarchyBuilder for EntityCommands<'w, 's, 'a> {
fn remove_child(&mut self, child: Entity) -> &mut Self { fn remove_child(&mut self, child: Entity) -> &mut Self {
let parent = self.id(); let parent = self.id();
self.commands().add(RemoveChild { parent, child }); self.commands().queue(RemoveChild { parent, child });
self.commands().entity(child).remove::<Parent>(); self.commands().entity(child).remove::<Parent>();
self self
} }
fn set_parent(&mut self, parent: Entity) -> &mut Self { fn set_parent(&mut self, parent: Entity) -> &mut Self {
let child = self.id(); let child = self.id();
self.commands().add(AddChild { parent, child }); self.commands().queue(AddChild { parent, child });
self.commands().entity(child).insert(Parent(parent)); self.commands().entity(child).insert(Parent(parent));
self self
} }

View file

@ -1,32 +1,29 @@
#![warn(clippy::perf, clippy::style, clippy::all)] #![warn(clippy::perf, clippy::style, clippy::all)]
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
use bevy_ecs::{prelude::*, schedule::ScheduleLabel}; use bevy_ecs::{message::MessageRegistry, prelude::*, schedule::ScheduleLabel};
use effect::{register_effect, EffectBuilder}; use effect::{register_effect, EffectBuilder};
use metrics::Metrics; use metrics::Metrics;
use proxisim_models::{
bundle::player::{Attacker, Current, Defender},
dto::{
metrics::{Counter, Histogram},
player::PlayerDto,
},
};
use rand::SeedableRng; use rand::SeedableRng;
use crate::{ use crate::log::{Log, Logging};
log::{Log, Logging},
player::{Attacker, Current, Defender},
};
mod armour; mod armour;
pub mod dto;
mod effect; mod effect;
mod entity_registry; mod entity_registry;
mod hierarchy; // mod hierarchy;
pub mod log; pub mod log;
mod metrics; mod metrics;
mod passives; mod passives;
mod player; mod player;
mod weapon; mod weapon;
#[derive(Component, Debug, Default)]
struct Name(String);
#[derive(Component, Debug, Default)]
struct Id(usize);
// TODO: This is a bottleneck, so probably better to change this to a `Local` or use `thread_rng` // TODO: This is a bottleneck, so probably better to change this to a `Local` or use `thread_rng`
// instead. Then again, the whole simulator isn't very parallelisable, so it may be a moot point // instead. Then again, the whole simulator isn't very parallelisable, so it may be a moot point
#[derive(Resource)] #[derive(Resource)]
@ -79,19 +76,20 @@ struct Stages {
impl Stages { impl Stages {
fn add_event<T>(&mut self) -> &mut Self fn add_event<T>(&mut self) -> &mut Self
where where
T: Event, T: Message,
{ {
if !self.world.contains_resource::<Events<T>>() { if !self.world.contains_resource::<Messages<T>>() {
self.world.init_resource::<Events<T>>(); /* self.world.init_resource::<Messages<T>>();
self.pre_turn.add_systems( self.pre_turn.add_systems(
bevy_ecs::event::event_update_system::<T> bevy_ecs::event::event_update_system::<T>
.run_if(bevy_ecs::event::event_update_condition::<T>), .run_if(bevy_ecs::event::event_update_condition::<T>),
); ); */
MessageRegistry::register_message::<T>(&mut self.world);
} }
self self
} }
fn register_effect<Effect: 'static>(&mut self) -> EffectBuilder { fn register_effect<Effect: 'static>(&mut self) -> EffectBuilder<'_> {
register_effect::<Effect>(self) register_effect::<Effect>(self)
} }
} }
@ -99,7 +97,7 @@ impl Stages {
pub struct Simulation(Stages); pub struct Simulation(Stages);
impl Simulation { impl Simulation {
pub fn new(attacker: dto::Player, defender: dto::Player) -> Self { pub fn new(attacker: PlayerDto, defender: PlayerDto) -> Self {
let world = World::new(); let world = World::new();
let mut stages = Stages { let mut stages = Stages {
equip: Schedule::new(Stage::Equip), equip: Schedule::new(Stage::Equip),
@ -125,7 +123,7 @@ impl Simulation {
stages.world.insert_resource(FightStatus::Ongoing); stages.world.insert_resource(FightStatus::Ongoing);
stages stages
.world .world
.insert_resource(Rng(rand::rngs::SmallRng::from_entropy())); .insert_resource(Rng(rand::rngs::SmallRng::from_os_rng()));
stages.world.insert_resource(Logging(true)); stages.world.insert_resource(Logging(true));
@ -157,17 +155,17 @@ impl Simulation {
metrics.active = recording; metrics.active = recording;
} }
pub fn consume_metrics(&mut self) -> (Vec<dto::Counter>, Vec<dto::Histogram>) { pub fn consume_metrics(&mut self) -> (Vec<Counter>, Vec<Histogram>) {
let entities = self.0.world.entities().len(); let entities = self.0.world.entities().len();
let components = self.0.world.components().len(); let components = self.0.world.components().len();
self.0 self.0
.world .world
.resource::<metrics::Metrics>() .resource::<metrics::Metrics>()
.increment_counter(Entity::from_raw(0), "entities", entities.into()); .increment_counter(None, "entities", entities.into());
self.0 self.0
.world .world
.resource::<metrics::Metrics>() .resource::<metrics::Metrics>()
.increment_counter(Entity::from_raw(0), "components", components as u64); .increment_counter(None, "components", components as u64);
metrics::consume_metrics(&self.0.world) metrics::consume_metrics(&self.0.world)
} }
@ -333,110 +331,12 @@ impl Simulation {
mod tests { mod tests {
use super::*; use super::*;
fn attacker() -> dto::Player { fn attacker() -> PlayerDto {
use dto::*; serde_json::from_str(include_str!("./attacker.json")).unwrap()
Player {
name: "Test".to_owned(),
id: 0,
level: 10,
stats: Stats {
str: 10_000.0,
def: 10.0,
spd: 10.0,
dex: 10.0,
},
merits: Default::default(),
education: Default::default(),
faction: Default::default(),
drug: None,
strategy: PlayerStrategy::AlwaysFists,
weapons: Weapons {
primary: Some(Weapon {
name: "Test".to_owned(),
cat: WeaponCategory::Rifle,
dmg: 50.0,
acc: 50.0,
ammo: WeaponAmmo {
clips: 3,
clip_size: 25,
rate_of_fire: [3, 5],
},
mods: vec![WeaponMod::HairTrigger],
bonuses: vec![WeaponBonusInfo {
bonus: WeaponBonus::Expose,
value: 9.0,
}],
experience: 100.0,
}),
..Default::default()
},
armour: ArmourPieces {
helmet: Some(Armour {
name: "Test".to_owned(),
armour_value: 50.0,
coverage: [
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
],
immunities: Vec::default(),
set: None,
}),
..Default::default()
},
}
} }
fn defender() -> dto::Player { fn defender() -> PlayerDto {
use dto::*; serde_json::from_str(include_str!("./defender.json")).unwrap()
Player {
name: "Test".to_owned(),
id: 1,
level: 10,
stats: Stats {
str: 10_000.0,
def: 10.0,
spd: 10.0,
dex: 10.0,
},
merits: Default::default(),
education: Default::default(),
faction: Default::default(),
drug: None,
strategy: PlayerStrategy::AlwaysFists,
weapons: Weapons {
primary: Some(Weapon {
name: "Test".to_owned(),
cat: WeaponCategory::Rifle,
dmg: 50.0,
acc: 50.0,
ammo: WeaponAmmo {
clips: 3,
clip_size: 25,
rate_of_fire: [3, 5],
},
mods: Vec::default(),
bonuses: vec![WeaponBonusInfo {
bonus: WeaponBonus::Powerful,
value: 35.0,
}],
experience: 100.0,
}),
..Default::default()
},
armour: ArmourPieces {
helmet: Some(Armour {
name: "Test".to_owned(),
armour_value: 50.0,
coverage: [
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
],
immunities: Vec::default(),
set: None,
}),
..Default::default()
},
}
} }
#[test] #[test]
@ -450,6 +350,5 @@ mod tests {
sim.set_metrics(true); sim.set_metrics(true);
sim.run_once(); sim.run_once();
sim.consume_metrics(); sim.consume_metrics();
panic!();
} }
} }

View file

@ -1,23 +1,24 @@
use std::sync::Mutex; use std::sync::Mutex;
use bevy_ecs::{prelude::*, system::SystemParam}; use bevy_ecs::{prelude::*, query::QuerySingleError, system::SystemParam};
use proxisim_models::{
use crate::{ bundle::{
entity_registry::EntityRegistry, stat::{
hierarchy::Children, AdditiveBonus, BaselineStat, ClipSize, Clips, CritRate, DamageBonus, Defence,
player::stats::{ Dexterity, EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus,
AdditiveBonus, BaselineStat, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy,
MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective, },
SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy, weapon::WeaponVerb,
}, },
weapon::WeaponVerb, hierarchy::Children,
Stages,
}; };
use crate::{Stages, entity_registry::EntityRegistry};
#[derive(Resource)] #[derive(Resource)]
pub struct Logging(pub bool); pub struct Logging(pub bool);
#[derive(Event)] #[derive(Message)]
struct LogEvent(Mutex<Option<DynamicLogMessage>>); struct LogEvent(Mutex<Option<DynamicLogMessage>>);
impl From<DynamicLogMessage> for LogEvent { impl From<DynamicLogMessage> for LogEvent {
@ -38,66 +39,71 @@ pub struct WeaponInfo {
} }
#[derive(Clone)] #[derive(Clone)]
pub enum LogValue<'a> { pub enum LogValue {
Float(f32), Float(f32),
Unsigned(u32), Unsigned(u32),
Signed(i32),
Bool(bool), Bool(bool),
String(String), String(String),
Entity(Entity), Entity(Entity),
OptionNone, OptionNone,
Display(&'a (dyn std::fmt::Display + Send + Sync)),
Debug(&'a (dyn std::fmt::Debug + Send + Sync)),
Player(Entity), Player(Entity),
Weapon(Entity), Weapon(Entity),
Array(Vec<LogValue<'a>>), Array(Vec<LogValue>),
Map(Vec<(&'static str, LogValue<'a>)>), Map(Vec<(&'static str, LogValue)>),
} }
impl<'a> From<String> for LogValue<'a> { impl From<String> for LogValue {
fn from(value: String) -> Self { fn from(value: String) -> Self {
Self::String(value) Self::String(value)
} }
} }
impl<'a> From<&'a str> for LogValue<'static> { impl<'a> From<&'a str> for LogValue {
fn from(value: &'a str) -> Self { fn from(value: &'a str) -> Self {
Self::String(value.to_owned()) Self::String(value.to_owned())
} }
} }
impl<'a> From<f32> for LogValue<'a> { impl From<f32> for LogValue {
fn from(value: f32) -> Self { fn from(value: f32) -> Self {
Self::Float(value) Self::Float(value)
} }
} }
impl<'a> From<u32> for LogValue<'a> { impl From<u32> for LogValue {
fn from(value: u32) -> Self { fn from(value: u32) -> Self {
Self::Unsigned(value) Self::Unsigned(value)
} }
} }
impl<'a> From<u16> for LogValue<'a> { impl From<u16> for LogValue {
fn from(value: u16) -> Self { fn from(value: u16) -> Self {
Self::Unsigned(value.into()) Self::Unsigned(value.into())
} }
} }
impl From<bool> for LogValue<'static> { impl From<i16> for LogValue {
fn from(value: i16) -> Self {
Self::Signed(value.into())
}
}
impl From<bool> for LogValue {
fn from(value: bool) -> Self { fn from(value: bool) -> Self {
Self::Bool(value) Self::Bool(value)
} }
} }
impl From<Entity> for LogValue<'static> { impl From<Entity> for LogValue {
fn from(value: Entity) -> Self { fn from(value: Entity) -> Self {
Self::Entity(value) Self::Entity(value)
} }
} }
impl<'a, T> From<Option<T>> for LogValue<'a> impl<T> From<Option<T>> for LogValue
where where
T: Into<LogValue<'a>>, T: Into<LogValue>,
{ {
fn from(value: Option<T>) -> Self { fn from(value: Option<T>) -> Self {
match value { match value {
@ -107,26 +113,38 @@ where
} }
} }
impl<'a, V> From<Vec<V>> for LogValue<'a> impl<V> From<Vec<V>> for LogValue
where where
V: Into<LogValue<'a>>, V: Into<LogValue>,
{ {
fn from(value: Vec<V>) -> Self { fn from(value: Vec<V>) -> Self {
LogValue::Array(value.into_iter().map(Into::into).collect()) LogValue::Array(value.into_iter().map(Into::into).collect())
} }
} }
impl<'a, V> From<Vec<(&'static str, V)>> for LogValue<'a> impl<V> From<Vec<(&'static str, V)>> for LogValue
where where
V: Into<LogValue<'a>>, V: Into<LogValue>,
{ {
fn from(value: Vec<(&'static str, V)>) -> Self { fn from(value: Vec<(&'static str, V)>) -> Self {
LogValue::Map(value.into_iter().map(|(k, v)| (k, v.into())).collect()) LogValue::Map(value.into_iter().map(|(k, v)| (k, v.into())).collect())
} }
} }
impl<V> From<Result<V, QuerySingleError>> for LogValue
where
V: Into<LogValue>,
{
fn from(value: Result<V, QuerySingleError>) -> Self {
match value {
Ok(value) => value.into(),
Err(_) => LogValue::String("No found".to_owned()),
}
}
}
#[cfg(feature = "json")] #[cfg(feature = "json")]
impl<'a> LogValue<'a> { impl LogValue {
fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value { fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value {
match self { match self {
LogValue::OptionNone => serde_json::Value::Null, LogValue::OptionNone => serde_json::Value::Null,
@ -136,8 +154,7 @@ impl<'a> LogValue<'a> {
LogValue::String(val) => serde_json::Value::String(val.clone()), LogValue::String(val) => serde_json::Value::String(val.clone()),
LogValue::Bool(val) => serde_json::Value::Bool(*val), LogValue::Bool(val) => serde_json::Value::Bool(*val),
LogValue::Unsigned(val) => serde_json::Value::Number(serde_json::Number::from(*val)), LogValue::Unsigned(val) => serde_json::Value::Number(serde_json::Number::from(*val)),
LogValue::Debug(boxed) => serde_json::Value::String(format!("{boxed:?}")), LogValue::Signed(val) => serde_json::Value::Number(serde_json::Number::from(*val)),
LogValue::Display(boxed) => serde_json::Value::String(format!("{boxed}")),
LogValue::Player(id) => { LogValue::Player(id) => {
serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap() serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap()
} }
@ -162,7 +179,7 @@ impl<'a> LogValue<'a> {
trait LogMessage: Send + Sync + 'static { trait LogMessage: Send + Sync + 'static {
fn tag(&self) -> &'static str; fn tag(&self) -> &'static str;
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)>; fn entries(&self) -> Vec<(&'static str, LogValue)>;
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
@ -212,11 +229,10 @@ impl std::fmt::Display for Log {
LogValue::Float(val) => write!(f, "{val}")?, LogValue::Float(val) => write!(f, "{val}")?,
LogValue::Bool(val) => write!(f, "{val}")?, LogValue::Bool(val) => write!(f, "{val}")?,
LogValue::Unsigned(val) => write!(f, "{val}")?, LogValue::Unsigned(val) => write!(f, "{val}")?,
LogValue::Signed(val) => write!(f, "{val}")?,
LogValue::OptionNone => write!(f, "None")?, LogValue::OptionNone => write!(f, "None")?,
LogValue::Display(val) => write!(f, "\"{val}\"")?,
LogValue::Debug(val) => write!(f, "\"{val:?}\"")?,
LogValue::Player(id) | LogValue::Weapon(id) | LogValue::Entity(id) => { LogValue::Player(id) | LogValue::Weapon(id) | LogValue::Entity(id) => {
write!(f, "{:?}", id)? write!(f, "{id}")?
} }
LogValue::Array(_) | LogValue::Map(_) => (), LogValue::Array(_) | LogValue::Map(_) => (),
}; };
@ -421,7 +437,7 @@ impl std::fmt::Display for Log {
#[derive(SystemParam)] #[derive(SystemParam)]
pub struct Logger<'w> { pub struct Logger<'w> {
event_writer: EventWriter<'w, LogEvent>, event_writer: MessageWriter<'w, LogEvent>,
logging: Res<'w, Logging>, logging: Res<'w, Logging>,
} }
@ -431,7 +447,7 @@ impl<'w> Logger<'w> {
B: FnOnce() -> DynamicLogMessage, B: FnOnce() -> DynamicLogMessage,
{ {
if self.logging.0 { if self.logging.0 {
self.event_writer.send(body().into()); self.event_writer.write(body().into());
} }
} }
} }
@ -440,7 +456,7 @@ fn logging_enabled(logging: Res<Logging>) -> bool {
logging.0 logging.0
} }
fn append_log_messages(mut events: EventReader<LogEvent>, mut log: ResMut<Log>) { fn append_log_messages(mut events: MessageReader<LogEvent>, mut log: ResMut<Log>) {
for event in events.read() { for event in events.read() {
if let Some(entry) = event.0.lock().unwrap().take() { if let Some(entry) = event.0.lock().unwrap().take() {
log.entries.push(entry); log.entries.push(entry);
@ -450,7 +466,7 @@ fn append_log_messages(mut events: EventReader<LogEvent>, mut log: ResMut<Log>)
pub struct DynamicLogMessage { pub struct DynamicLogMessage {
pub label: &'static str, pub label: &'static str,
pub entries: Vec<(&'static str, LogValue<'static>)>, pub entries: Vec<(&'static str, LogValue)>,
} }
impl LogMessage for DynamicLogMessage { impl LogMessage for DynamicLogMessage {
@ -458,7 +474,7 @@ impl LogMessage for DynamicLogMessage {
self.label self.label
} }
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)> { fn entries(&self) -> Vec<(&'static str, LogValue)> {
self.entries.clone() self.entries.clone()
} }
} }
@ -549,8 +565,8 @@ fn log_simple_stat_changes<Stat: SimpleStatMarker>(
bonus_q: Query<&SimpleStatBonus<Stat>>, bonus_q: Query<&SimpleStatBonus<Stat>>,
mut logger: Logger, mut logger: Logger,
) where ) where
Stat::ValueType: Into<LogValue<'static>>, Stat::ValueType: Into<LogValue>,
Stat::BonusType: Into<LogValue<'static>>, Stat::BonusType: Into<LogValue>,
{ {
for (target, baseline, effective, children) in stat_q.iter() { for (target, baseline, effective, children) in stat_q.iter() {
let bonuses: Vec<_> = bonus_q let bonuses: Vec<_> = bonus_q
@ -588,6 +604,8 @@ pub(crate) fn configure(stages: &mut Stages) {
log_simple_stat_changes::<CritRate>, log_simple_stat_changes::<CritRate>,
log_simple_stat_changes::<WeaponAccuracy>, log_simple_stat_changes::<WeaponAccuracy>,
log_simple_stat_changes::<DamageBonus>, log_simple_stat_changes::<DamageBonus>,
log_simple_stat_changes::<Clips>,
log_simple_stat_changes::<ClipSize>,
) )
.run_if(logging_enabled), .run_if(logging_enabled),
); );

View file

@ -4,7 +4,7 @@ use std::{
sync::{atomic, Mutex, RwLock}, sync::{atomic, Mutex, RwLock},
}; };
use crate::{dto, entity_registry::EntityRegistry, Stages}; use crate::{entity_registry::EntityRegistry, Stages};
#[derive(Default)] #[derive(Default)]
pub struct Histogram<T> pub struct Histogram<T>
@ -38,7 +38,7 @@ impl Counter {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MetricKey { pub struct MetricKey {
entity: Entity, entity: Option<Entity>,
label: &'static str, label: &'static str,
} }
@ -50,7 +50,7 @@ pub struct Metrics {
} }
impl Metrics { impl Metrics {
pub fn record_histogram(&self, entity: Entity, label: &'static str, value: u32) { pub fn record_histogram(&self, entity: Option<Entity>, label: &'static str, value: u32) {
if self.active { if self.active {
let key = MetricKey { entity, label }; let key = MetricKey { entity, label };
let r_hist = self.histograms.read().unwrap(); let r_hist = self.histograms.read().unwrap();
@ -70,7 +70,7 @@ impl Metrics {
} }
} }
pub fn increment_counter(&self, entity: Entity, label: &'static str, value: u64) { pub fn increment_counter(&self, entity: Option<Entity>, label: &'static str, value: u64) {
if self.active { if self.active {
let key = MetricKey { entity, label }; let key = MetricKey { entity, label };
let r_counters = self.counters.read().unwrap(); let r_counters = self.counters.read().unwrap();
@ -91,7 +91,12 @@ impl Metrics {
} }
} }
pub(crate) fn consume_metrics(world: &World) -> (Vec<dto::Counter>, Vec<dto::Histogram>) { pub(crate) fn consume_metrics(
world: &World,
) -> (
Vec<proxisim_models::dto::metrics::Counter>,
Vec<proxisim_models::dto::metrics::Histogram>,
) {
let metrics = world.resource::<Metrics>(); let metrics = world.resource::<Metrics>();
let entities = world.resource::<EntityRegistry>(); let entities = world.resource::<EntityRegistry>();
@ -100,8 +105,12 @@ pub(crate) fn consume_metrics(world: &World) -> (Vec<dto::Counter>, Vec<dto::His
.try_write() .try_write()
.unwrap() .unwrap()
.drain() .drain()
.map(|(key, value)| dto::Counter { .map(|(key, value)| proxisim_models::dto::metrics::Counter {
entity: entities.0.get(&key.entity).unwrap().clone(), entity: key
.entity
.as_ref()
.map(|e| entities.0.get(e).unwrap().clone())
.unwrap_or(proxisim_models::dto::metrics::EntityInfo::Global),
value: value.inner.load(atomic::Ordering::Relaxed), value: value.inner.load(atomic::Ordering::Relaxed),
label: key.label, label: key.label,
}) })
@ -112,8 +121,12 @@ pub(crate) fn consume_metrics(world: &World) -> (Vec<dto::Counter>, Vec<dto::His
.try_write() .try_write()
.unwrap() .unwrap()
.drain() .drain()
.map(|(key, value)| dto::Histogram { .map(|(key, value)| proxisim_models::dto::metrics::Histogram {
entity: entities.0.get(&key.entity).unwrap().clone(), entity: key
.entity
.as_ref()
.map(|e| entities.0.get(e).unwrap().clone())
.unwrap_or(proxisim_models::dto::metrics::EntityInfo::Global),
values: value.inner.into_inner().unwrap(), values: value.inner.into_inner().unwrap(),
label: key.label, label: key.label,
}) })

View file

@ -1,217 +1,10 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{
use crate::{ passive::{DrugCooldown, Education, FactionUpgrades, Merits},
effect::Effects, stat::{AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength},
player::{
stats::{AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength},
BodyPart,
},
Stages,
}; };
#[derive(Component, Default)] use crate::{effect::Effects, Stages};
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Merits {
pub life: u16,
pub crits: u16,
pub brawn: u16,
pub protection: u16,
pub sharpness: u16,
pub evasion: u16,
pub heavy_artillery_mastery: u16,
pub machine_gun_mastery: u16,
pub rifle_mastery: u16,
pub smg_mastery: u16,
pub shotgun_mastery: u16,
pub pistol_mastery: u16,
pub club_mastery: u16,
pub piercing_mastery: u16,
pub slashing_mastery: u16,
pub mechanical_mastery: u16,
pub temporary_mastery: u16,
}
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct Education {
/// Gain a 1% damage bonus to all weapons
pub bio2350: bool,
/// Gain a 10% damage increase when hitting an opponent's throat
pub bio2380: bool,
/// Gain a 3% chance increase of achieving a critical hit
pub bio2410: bool,
/// Gain a 1% passive bonus to speed
pub cbt2790: bool,
/// Gain a +1.00 accuracy increase with Machine Guns
pub cbt2820: bool,
/// Gain a +1.00 accuracy increase with Submachine guns
pub cbt2830: bool,
/// Gain a +1.00 accuracy increase with Pistols
pub cbt2840: bool,
/// Gain a +1.00 accuracy increase with Rifles
pub cbt2850: bool,
/// Gain a +1.00 accuracy increase with Heavy Artillery
pub cbt2860: bool,
/// Gain a +1.00 accuracy increase with Shotguns
pub cbt2125: bool,
/// Gain a +1.00 accuracy increase with Temporary weapons
pub gen2116: bool,
/// Gain a 5% damage increase with Temporary weapons
pub gen2119: bool,
/// Gain a 1% passive bonus to dexterity
pub haf2104: bool,
/// Gain a 1% passive bonus to speed
pub haf2105: bool,
/// Gain a 1% passive bonus to strength
pub haf2106: bool,
/// Gain a 2% passive bonus to strength
pub haf2107: bool,
/// Gain a 1% passive bonus to dexterity
pub haf2108: bool,
/// Gain a 3% passive bonus to speed
pub haf2109: bool,
/// Gain a 10% damage increase with Japanese blade weapons
pub his2160: bool,
/// Gain a 2% bonus to all melee damage
pub his2170: bool,
/// Gain a 1% passive bonus to speed
pub mth2240: bool,
/// Gain a 1% passive bonus to speed
pub mth2250: bool,
/// Gain a 1% passive bonus to defense
pub mth2260: bool,
/// Gain a 2% passive bonus to defense
pub mth2320: bool,
/// Gain a 5% bonus to ammo conservation
pub mth2310: bool,
/// Gain a 20% bonus to ammo conservation
pub mth3330: bool,
/// Gain a 1% passive bonus to dexterity
pub psy2640: bool,
/// Gain a 2% passive bonus to dexterity
pub psy2650: bool,
/// Gain a 4% passive bonus to dexterity
pub psy2660: bool,
/// Gain an 8% passive bonus to dexterity
pub psy2670: bool,
/// Gain a 1% passive bonus to defense
pub def2710: bool,
/// Gain a 2% passive bonus to defense
pub def2730: bool,
/// Gain a 3% passive bonus to defense
pub def2740: bool,
/// Gain a 2% passive bonus to speed
pub def2750: bool,
/// Gain a 3% passive bonus to speed
pub def2760: bool,
/// Gain a 100% increase in damage dealt when using fists alone
pub def3770: bool,
/// Gain a 10% increase in steroid effectiveness
// NOTE: this effect is additive with the strip club perks
pub spt2480: bool,
/// Gain a 2% passive bonus to speed and strength
pub spt2490: bool,
/// Gain a 2% passive bonus to defense and dexterity
pub spt2500: bool,
}
impl Default for Education {
fn default() -> Self {
Self {
bio2350: true,
bio2380: true,
bio2410: true,
cbt2790: true,
cbt2820: true,
cbt2830: true,
cbt2840: true,
cbt2850: true,
cbt2860: true,
cbt2125: true,
gen2116: true,
gen2119: true,
haf2104: true,
haf2105: true,
haf2106: true,
haf2107: true,
haf2108: true,
haf2109: true,
his2160: true,
his2170: true,
mth2240: true,
mth2250: true,
mth2260: true,
mth2320: true,
mth2310: true,
mth3330: true,
psy2640: true,
psy2650: true,
psy2660: true,
psy2670: true,
def2710: true,
def2730: true,
def2740: true,
def2750: true,
def2760: true,
def3770: true,
spt2480: true,
spt2490: true,
spt2500: true,
}
}
}
#[derive(Component, Default)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub struct FactionUpgrades {
pub str: u16,
pub spd: u16,
pub def: u16,
pub dex: u16,
pub life: u16,
pub acc: u16,
pub dmg: u16,
pub side_effects: u16,
}
#[derive(Component)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub enum DrugCooldown {
Xanax,
Vicodin,
}
#[derive(Clone, Copy)]
pub enum EducationPartDamageBonus {
Bio2380,
}
impl EducationPartDamageBonus {
pub fn dmg_bonus(self, part: BodyPart) -> Option<f32> {
match part {
BodyPart::Throat => Some(0.10),
_ => None,
}
}
}
#[derive(Bundle, Default)]
pub(crate) struct PassiveBundle {
pub merits: Merits,
pub education: Education,
pub faction: FactionUpgrades,
}
fn spawn_permanent_effects( fn spawn_permanent_effects(
merit_q: Query<( merit_q: Query<(

View file

@ -1,224 +1,59 @@
use bevy_ecs::prelude::*; 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 rand::Rng as _;
use strum::Display;
use crate::{ use crate::{
armour, FightStatus, Rng, Stages,
effect::Effects, effect::Effects,
hierarchy::Children,
log, log,
log::Logger, log::Logger,
metrics::Metrics, metrics::Metrics,
passives::{EducationPartDamageBonus, FactionUpgrades, Merits}, weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus},
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,
}; };
pub mod stats; pub mod stats;
pub mod status_effect; pub mod status_effect;
#[derive(Component)] fn select_weapon(
pub struct Attacker; 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)] let (needs_reload, children) = usable_q.get(id).ok()?;
pub struct Defender;
#[derive(Component)] if !reload && needs_reload {
pub struct Defeated; None
} else {
#[derive(Component)] Some((id, children.cloned()))
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(),
}
} }
} }
@ -250,7 +85,7 @@ fn designate_first(
mut commands: Commands, mut commands: Commands,
) { ) {
let attacker = attacker_q.iter().next().unwrap(); 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(attacker).insert(Current);
commands.entity(defender).insert(CurrentTarget); commands.entity(defender).insert(CurrentTarget);
} }
@ -260,8 +95,8 @@ fn change_roles(
target_q: Query<Entity, With<CurrentTarget>>, target_q: Query<Entity, With<CurrentTarget>>,
mut commands: Commands, mut commands: Commands,
) { ) {
let current = current_q.single(); let current = current_q.single().unwrap();
let target = target_q.single(); let target = target_q.single().unwrap();
// TODO: Group fights // TODO: Group fights
commands commands
@ -291,36 +126,38 @@ pub fn pick_action(
(With<Current>, With<Player>), (With<Current>, With<Player>),
>, >,
target_q: Query<Entity, With<CurrentTarget>>, 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>, weapon_trigger_q: Query<&TurnTriggeredEffect>,
mut commands: Commands, mut commands: Commands,
mut effects: Effects, mut effects: Effects,
metrics: Res<Metrics>, 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 { let (weapon, children) = match strat {
PlayerStrategy::AlwaysFists => (weapons.fists.unwrap(), Default::default()), PlayerStrategy::AlwaysFists => (weapons.fists, None),
PlayerStrategy::AlwaysKicks => weapons PlayerStrategy::AlwaysKicks => select_weapon(weapons, WeaponSlot::Kick, true, &usable_q)
.select(WeaponSlot::Kick, true, &usable_q) .unwrap_or_else(|| (weapons.fists, Default::default())),
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())), PlayerStrategy::PrimaryMelee { reload } => {
PlayerStrategy::PrimaryMelee { reload } => weapons select_weapon(weapons, WeaponSlot::Primary, *reload, &usable_q)
.select(WeaponSlot::Primary, *reload, &usable_q) .or_else(|| select_weapon(weapons, WeaponSlot::Melee, true, &usable_q))
.or_else(|| weapons.select(WeaponSlot::Melee, true, &usable_q)) .unwrap_or_else(|| (weapons.fists, Default::default()))
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())), }
PlayerStrategy::InOrder { order, reload } => order PlayerStrategy::InOrder { order, reload } => order
.iter() .iter()
.find_map(|slot| weapons.select(*slot, *reload, &usable_q)) .find_map(|slot| select_weapon(weapons, *slot, *reload, &usable_q))
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())), .unwrap_or_else(|| (weapons.fists, Default::default())),
}; };
metrics.increment_counter(current, "turn", 1); metrics.increment_counter(Some(current), "turn", 1);
metrics.increment_counter(weapon, "turn", 1); metrics.increment_counter(Some(weapon), "turn", 1);
commands.entity(weapon).insert(Current); 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()) { if let Some(children) = children {
effect.trigger(&mut effects, current, target); for effect in weapon_trigger_q.iter_many(children.get()) {
effect.trigger(&mut effects, current, target);
}
} }
turns.0 += 1; turns.0 += 1;
@ -368,12 +205,12 @@ pub fn use_damaging_weapon(
Entity, Entity,
&EffectiveStat<Dexterity>, &EffectiveStat<Dexterity>,
&EffectiveStat<Defence>, &EffectiveStat<Defence>,
&armour::ArmourBodyParts, &ArmourBodyParts,
&mut SimpleStatEffective<Health>, &mut SimpleStatEffective<Health>,
), ),
With<CurrentTarget>, With<CurrentTarget>,
>, >,
armour_q: Query<&armour::ArmourBodyPart>, armour_q: Query<&ArmourBodyPart>,
(damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>),
(mut ammo_q, mut temp_q): ( (mut ammo_q, mut temp_q): (
Query<( Query<(
@ -392,13 +229,13 @@ pub fn use_damaging_weapon(
Effects, 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 { else {
return; return;
}; };
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) = let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) =
player_q.single(); player_q.single().unwrap();
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut(); 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) { if let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1; uses.0 -= 1;
@ -448,10 +285,10 @@ pub fn use_damaging_weapon(
+ 30.0; + 30.0;
loop { loop {
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| { let rounds = ammo.as_mut().map(|(ammo, clips, rof)| {
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0); let rounds = (rng.random_range(rof.clone()).round() as u16).clamp(1, ammo.0);
metrics.increment_counter(player, "rounds_fired", rounds.into()); metrics.increment_counter(Some(player), "rounds_fired", rounds.into());
metrics.increment_counter(weapon, "rounds_fired", rounds.into()); metrics.increment_counter(Some(weapon), "rounds_fired", rounds.into());
ammo.0 -= rounds; ammo.0 -= rounds;
if ammo.0 == 0 { if ammo.0 == 0 {
if clips.value == 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) 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", { log!(logger, "miss_target", {
weapon: weapon, weapon: weapon,
actor: player, actor: player,
@ -477,8 +314,8 @@ pub fn use_damaging_weapon(
rounds: rounds, rounds: rounds,
hit_chance: hit_chance, hit_chance: hit_chance,
}); });
metrics.increment_counter(player, "miss", 1); metrics.increment_counter(Some(player), "miss", 1);
metrics.increment_counter(weapon, "miss", 1); metrics.increment_counter(Some(weapon), "miss", 1);
if multi_attack_proc.is_none() { if multi_attack_proc.is_none() {
return; return;
@ -492,8 +329,8 @@ pub fn use_damaging_weapon(
let mult = match body_part { let mult = match body_part {
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => { BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
metrics.increment_counter(player, "crit", 1); metrics.increment_counter(Some(player), "crit", 1);
metrics.increment_counter(weapon, "crit", 1); metrics.increment_counter(Some(weapon), "crit", 1);
1.0 1.0
} }
BodyPart::LeftHand BodyPart::LeftHand
@ -506,8 +343,8 @@ pub fn use_damaging_weapon(
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75, BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
}; };
metrics.increment_counter(player, "hit", 1); metrics.increment_counter(Some(player), "hit", 1);
metrics.increment_counter(weapon, "hit", 1); metrics.increment_counter(Some(weapon), "hit", 1);
let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap(); let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap();
let piece = rng.sample(armour_parts); let piece = rng.sample(armour_parts);
@ -530,15 +367,15 @@ pub fn use_damaging_weapon(
let dmg = dmg_intrinsic let dmg = dmg_intrinsic
* w_dmg.0 * w_dmg.0
* dmg_bonus.value * (1.0 + dmg_bonus.value)
* (1.0 - armour_mitigation) * (1.0 - armour_mitigation)
* (1.0 - def_mitigation) * (1.0 - def_mitigation)
* mult * mult
* dmg_spread; * dmg_spread;
let dmg = dmg.round() as u32; let dmg = dmg.round() as u32;
metrics.record_histogram(player, "dmg", dmg); metrics.record_histogram(Some(player), "dmg", dmg);
metrics.record_histogram(weapon, "dmg", dmg); metrics.record_histogram(Some(weapon), "dmg", dmg);
if dmg > 0 { if dmg > 0 {
for effect in damage_proc_q.iter_many(children.get()) { for effect in damage_proc_q.iter_many(children.get()) {
@ -548,7 +385,7 @@ pub fn use_damaging_weapon(
continue; continue;
} }
let chance = (value / 100.0) as f64; 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 { match bonus {
MultiTurnBonus::Blindfire => { MultiTurnBonus::Blindfire => {
multi_attack_proc = Some(MultiAttack::Blindfire) multi_attack_proc = Some(MultiAttack::Blindfire)
@ -559,26 +396,26 @@ pub fn use_damaging_weapon(
} }
MultiTurnBonus::Rage => { MultiTurnBonus::Rage => {
multi_attack_proc = multi_attack_proc =
Some(MultiAttack::Rage(rng.gen_range(2..=8))) Some(MultiAttack::Rage(rng.random_range(2..=8)))
} }
MultiTurnBonus::DoubleTap => { MultiTurnBonus::DoubleTap => {
multi_attack_proc = multi_attack_proc =
Some(MultiAttack::DoubleTap { first_shot: true }) Some(MultiAttack::DoubleTap { first_shot: true })
} }
}; };
metrics.increment_counter(player, bonus.counter_label(), 1); metrics.increment_counter(Some(player), bonus.counter_label(), 1);
metrics.increment_counter(weapon, bonus.counter_label(), 1); metrics.increment_counter(Some(weapon), bonus.counter_label(), 1);
} }
} }
DamageProcEffect::SelfEffect { value, bonus } => { DamageProcEffect::SelfEffect { value, bonus } => {
let chance = (value / 100.0) as f64; 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); bonus.spawn(player, &mut effects);
} }
} }
DamageProcEffect::OpponentEffect { value, bonus } => { DamageProcEffect::OpponentEffect { value, bonus } => {
let chance = (value / 100.0) as f64; 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); bonus.spawn(target, &mut effects, &mut rng.0);
} }
} }
@ -592,19 +429,20 @@ pub fn use_damaging_weapon(
log!(logger, "hit_target", { log!(logger, "hit_target", {
actor: player, actor: player,
acc: acc_eff.value,
recipient: target, recipient: target,
weapon: weapon, weapon,
part: %body_part, part: %body_part,
part_mult: mult, part_mult: mult,
dmg: dmg, rounds,
rounds: rounds,
health_before: health_before, health_before: health_before,
health_after: health.value, health_after: health.value,
dmg_spread: dmg_spread, dmg,
dmg_intrinsic: dmg_intrinsic, dmg_spread,
armour_mitigation: armour_mitigation, dmg_intrinsic,
def_mitigation: def_mitigation, dmg_weapon: w_dmg.0,
weapon_dmg: w_dmg.0, armour_mitigation,
def_mitigation,
bonus_dmg: dmg_bonus.value, bonus_dmg: dmg_bonus.value,
hit_chance: hit_chance, hit_chance: hit_chance,
crit_rate: crit.value, crit_rate: crit.value,
@ -623,17 +461,23 @@ pub fn use_damaging_weapon(
FightEndType::Loss 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 { match multi_attack_proc {
Some(MultiAttack::Blindfire) => { 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; break;
} }
acc_eff.value -= 5.0 / 50.0;
} }
Some(MultiAttack::Fury { first_hit: true }) => { Some(MultiAttack::Fury { first_hit: true }) => {
multi_attack_proc = Some(MultiAttack::Fury { first_hit: false }) multi_attack_proc = Some(MultiAttack::Fury { first_hit: false })
@ -658,7 +502,7 @@ pub fn check_stalemate(
mut logger: Logger, mut logger: Logger,
metrics: Res<Metrics>, 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 { if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker {
commands.entity(current).insert(Defeated); commands.entity(current).insert(Defeated);
let target = target_q.single(); let target = target_q.single();
@ -668,7 +512,7 @@ pub fn check_stalemate(
recipient: target, recipient: target,
fight_end_type: %FightEndType::Stalemate, fight_end_type: %FightEndType::Stalemate,
}); });
metrics.increment_counter(current, "stalemate", 1); metrics.increment_counter(Some(current), "stalemate", 1);
if other_attackers_q.is_empty() { if other_attackers_q.is_empty() {
*state = FightStatus::Over *state = FightStatus::Over
@ -705,7 +549,7 @@ fn record_post_fight_stats(
metrics: Res<Metrics>, metrics: Res<Metrics>,
) { ) {
for (player, health) in player_q.iter() { 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 std::marker::PhantomData;
use bevy_ecs::prelude::*; 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}; use crate::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,
}
}
}
fn add_additive_bonus<Stat: StatMarker>( fn add_additive_bonus<Stat: StatMarker>(
In(entities): In<Vec<Entity>>, In(entities): In<Vec<Entity>>,

View file

@ -1,19 +1,20 @@
use std::{collections::VecDeque, marker::PhantomData}; use std::{collections::VecDeque, marker::PhantomData};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::{
bundle::stat::{
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
},
hierarchy::Parent,
};
use rand::Rng as _; use rand::Rng as _;
use crate::{ use crate::{
Rng, Stages,
effect::{Effects, TimeLimitedEffect}, effect::{Effects, TimeLimitedEffect},
hierarchy::{HierarchyBuilder, Parent},
log, log,
log::Logger, log::Logger,
weapon::temp::AssociatedWeapon, weapon::temp::AssociatedWeapon,
Rng, Stages,
};
use super::stats::{
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
}; };
#[derive(Component)] #[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)] #[derive(Component)]
struct LinkedComponents<const N: usize>([Entity; N]); struct LinkedComponents<const N: usize>([Entity; N]);
@ -136,7 +124,7 @@ where
value: f32, value: f32,
label: &'static str, label: &'static str,
) -> [Entity; 1] { ) -> [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!(2, A, B);
impl_n_stats!(3, A, B, C); impl_n_stats!(3, A, B, C);
impl_n_stats!(4, A, B, C, D); 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, mut effects: Effects,
) { ) {
for (effect, player) in effect_q.iter_many(entities) { for (effect, player) in effect_q.iter_many(entities) {
if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap() { if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap()
if stack.effects.front() == Some(&effect) { && stack.effects.front() == Some(&effect)
stack.effects.pop_front(); {
} stack.effects.pop_front();
} }
let linked = linked_q.get(effect).unwrap(); let linked = linked_q.get(effect).unwrap();
@ -448,7 +436,7 @@ fn apply_temp_debuff_effect<Temp: DebuffingTempMarker>(
continue; continue;
} }
let duration = rng.gen_range(Temp::duration()); let duration = rng.random_range(Temp::duration());
commands.entity(effect).insert(TimeLimitedEffect(duration)); commands.entity(effect).insert(TimeLimitedEffect(duration));
let stack_size = stack.as_ref().map_or(0, |s| s.effects.len()) as i32; 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::<PepperSpray>(stages);
register_debuff_temp::<ConcussionGrenade>(stages); register_debuff_temp::<ConcussionGrenade>(stages);
register_debuff_temp::<FlashGrenade>(stages); register_debuff_temp::<FlashGrenade>(stages);
register_debuff_temp::<Sand>(stages);
register_status_effect::<1, Withered>(stages); register_status_effect::<1, Withered>(stages);
register_status_effect::<1, Weakened>(stages); register_status_effect::<1, Weakened>(stages);

View file

@ -1,102 +1,26 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::{
use crate::{ bundle::{
effect::{Effects, TurnLimitedEffect}, bonus::{BonusPartDamageBonus, BonusValue, WeaponBonusType},
hierarchy::{HierarchyBuilder, Parent}, player::PartDamageBonus,
player::{ stat::{
stats::{
AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus,
SimpleStatEffective, Speed, Strength, WeaponAccuracy, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
}, },
status_effect::{
AdditiveStatusEffect, Crippled, Demoralise, Frozen, Motivate, Slow, Weakened, Withered,
},
BodyPart, PartDamageBonus,
}, },
hierarchy::{HierarchyBuilder, Parent},
};
use crate::{
Stages, Stages,
effect::{Effects, TurnLimitedEffect},
player::status_effect::{
AdditiveStatusEffect, Crippled, Demoralise, Frozen, Motivate, Slow, Weakened, Withered,
},
}; };
use super::{DamageProcEffect, FirstTurnEffect, TurnTriggeredEffect}; use super::{DamageProcEffect, FirstTurnEffect, TurnTriggeredEffect};
#[derive(Component, Debug, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponBonus {
// Weapon passives
Berserk,
Conserve,
Expose,
Grace,
Powerful,
Specialist,
// Turn triggered passives
Empower,
Quicken,
// First turn effects
Assassinate,
// Additive status effects triggered by damaging hits
Cripple,
Demoralise,
Freeze,
Motivate,
Slow,
Toxin,
Weaken,
Wither,
// DOT status effects
Bleed,
Burning,
Lacerate,
Poison,
SevereBurning,
// Other status effects
Eviscerate,
Paralyse,
Schock,
Stun,
// Multi attack bonuses
Blindfire,
Fury,
DoubleTap,
Rage,
// Body part multipliers
Achilles,
Crusher,
Cupid,
Deadeye,
Roshambo,
Throttle,
// Attack nullification types
Homerun,
Parry,
}
#[derive(Component)]
pub struct BonusValue(f32);
#[derive(Bundle)]
pub struct WeaponBonusBundle {
pub bonus: WeaponBonus,
pub value: BonusValue,
}
impl WeaponBonusBundle {
pub fn new(bonus: WeaponBonus, value: f32) -> Self {
Self {
bonus,
value: BonusValue(value),
}
}
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum TurnTriggeredBonus { pub enum TurnTriggeredBonus {
Empower, Empower,
@ -188,7 +112,7 @@ impl OpponentStatusEffect {
Self::Slow => { Self::Slow => {
effects.spawn(AdditiveStatusEffect::<1, Slow>::default(), target); effects.spawn(AdditiveStatusEffect::<1, Slow>::default(), target);
} }
Self::Toxin => match rng.gen_range(0..4) { Self::Toxin => match rng.random_range(0..4) {
0 => OpponentStatusEffect::Cripple.spawn(target, effects, rng), 0 => OpponentStatusEffect::Cripple.spawn(target, effects, rng),
1 => OpponentStatusEffect::Slow.spawn(target, effects, rng), 1 => OpponentStatusEffect::Slow.spawn(target, effects, rng),
2 => OpponentStatusEffect::Weaken.spawn(target, effects, rng), 2 => OpponentStatusEffect::Weaken.spawn(target, effects, rng),
@ -219,7 +143,7 @@ impl SelfStatusEffect {
} }
} }
#[derive(Clone, Copy)] /* #[derive(Clone, Copy)]
pub enum BonusPartDamageBonus { pub enum BonusPartDamageBonus {
Achilles, Achilles,
Crusher, Crusher,
@ -270,21 +194,17 @@ impl BonusPartDamageBonus {
} }
} }
} }
} } */
pub(crate) fn prepare_bonuses( pub(crate) fn prepare_bonuses(
bonus_q: Query<( bonus_q: Query<(&Parent, &WeaponBonusType, &BonusValue)>,
&Parent, clips_q: Query<&SimpleStatEffective<Clips>>,
&WeaponBonus,
&BonusValue,
Option<&SimpleStatEffective<Clips>>,
)>,
mut effects: Effects, mut effects: Effects,
mut commands: Commands, mut commands: Commands,
) { ) {
for (weapon, bonus, value, clips) in bonus_q.iter() { for (weapon, bonus, value) in bonus_q.iter() {
match bonus { match bonus {
WeaponBonus::Berserk => { WeaponBonusType::Berserk => {
effects.spawn( effects.spawn(
SimpleStatBonus::<DamageBonus>::new("beserk", value.0 / 100.0), SimpleStatBonus::<DamageBonus>::new("beserk", value.0 / 100.0),
weapon.get(), weapon.get(),
@ -294,19 +214,19 @@ pub(crate) fn prepare_bonuses(
weapon.get(), weapon.get(),
); );
} }
WeaponBonus::Conserve => { WeaponBonusType::Conserve => {
effects.spawn( effects.spawn(
SimpleStatBonus::<AmmoControl>::new("conserve", value.0 / 100.0), SimpleStatBonus::<AmmoControl>::new("conserve", value.0 / 100.0),
weapon.get(), weapon.get(),
); );
} }
WeaponBonus::Expose => { WeaponBonusType::Expose => {
effects.spawn( effects.spawn(
SimpleStatBonus::<CritRate>::new("expose", (value.0 / 0.5) as u16), SimpleStatBonus::<CritRate>::new("expose", (value.0 / 0.5) as u16),
weapon.get(), weapon.get(),
); );
} }
WeaponBonus::Grace => { WeaponBonusType::Grace => {
effects.spawn( effects.spawn(
SimpleStatBonus::<DamageBonus>::new("grace", -value.0 / 2.0 / 100.0), SimpleStatBonus::<DamageBonus>::new("grace", -value.0 / 2.0 / 100.0),
weapon.get(), weapon.get(),
@ -316,27 +236,27 @@ pub(crate) fn prepare_bonuses(
weapon.get(), weapon.get(),
); );
} }
WeaponBonus::Powerful => { WeaponBonusType::Powerful => {
effects.spawn( effects.spawn(
SimpleStatBonus::<DamageBonus>::new("powerful", value.0 / 100.0), SimpleStatBonus::<DamageBonus>::new("powerful", value.0 / 100.0),
weapon.get(), weapon.get(),
); );
} }
WeaponBonus::Specialist => { WeaponBonusType::Specialist => {
effects.spawn( effects.spawn(
SimpleStatBonus::<DamageBonus>::new("specialist", value.0 / 100.0), SimpleStatBonus::<DamageBonus>::new("specialist", value.0 / 100.0),
weapon.get(), weapon.get(),
); );
effects.spawn(
SimpleStatBonus::<Clips>::new( if let Ok(clips) = clips_q.get(weapon.get()) {
"specialist", effects.spawn(
-clips.map(|c| c.value as i16).unwrap_or_default(), SimpleStatBonus::<Clips>::new("specialist", -(clips.value as i16)),
), weapon.get(),
weapon.get(), );
); }
} }
WeaponBonus::Empower => { WeaponBonusType::Empower => {
commands commands
.spawn(TurnTriggeredEffect::Bonus { .spawn(TurnTriggeredEffect::Bonus {
value: value.0, value: value.0,
@ -344,7 +264,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Quicken => { WeaponBonusType::Quicken => {
commands commands
.spawn(TurnTriggeredEffect::Bonus { .spawn(TurnTriggeredEffect::Bonus {
value: value.0, value: value.0,
@ -353,7 +273,7 @@ pub(crate) fn prepare_bonuses(
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Assassinate => { WeaponBonusType::Assassinate => {
commands commands
.spawn(FirstTurnEffect::Bonus { .spawn(FirstTurnEffect::Bonus {
value: value.0, value: value.0,
@ -362,7 +282,7 @@ pub(crate) fn prepare_bonuses(
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Blindfire => { WeaponBonusType::Blindfire => {
commands commands
.spawn(DamageProcEffect::MultiTurn { .spawn(DamageProcEffect::MultiTurn {
value: value.0, value: value.0,
@ -370,7 +290,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Fury => { WeaponBonusType::Fury => {
commands commands
.spawn(DamageProcEffect::MultiTurn { .spawn(DamageProcEffect::MultiTurn {
value: value.0, value: value.0,
@ -378,7 +298,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Rage => { WeaponBonusType::Rage => {
commands commands
.spawn(DamageProcEffect::MultiTurn { .spawn(DamageProcEffect::MultiTurn {
value: value.0, value: value.0,
@ -386,7 +306,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::DoubleTap => { WeaponBonusType::DoubleTap => {
commands commands
.spawn(DamageProcEffect::MultiTurn { .spawn(DamageProcEffect::MultiTurn {
value: value.0, value: value.0,
@ -395,7 +315,7 @@ pub(crate) fn prepare_bonuses(
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Achilles => { WeaponBonusType::Achilles => {
commands commands
.spawn(PartDamageBonus::WeaponBonus { .spawn(PartDamageBonus::WeaponBonus {
value: value.0 / 100.0, value: value.0 / 100.0,
@ -403,7 +323,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Cupid => { WeaponBonusType::Cupid => {
commands commands
.spawn(PartDamageBonus::WeaponBonus { .spawn(PartDamageBonus::WeaponBonus {
value: value.0 / 100.0, value: value.0 / 100.0,
@ -411,7 +331,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Crusher => { WeaponBonusType::Crusher => {
commands commands
.spawn(PartDamageBonus::WeaponBonus { .spawn(PartDamageBonus::WeaponBonus {
value: value.0 / 100.0, value: value.0 / 100.0,
@ -419,7 +339,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Deadeye => { WeaponBonusType::Deadeye => {
commands commands
.spawn(PartDamageBonus::WeaponBonus { .spawn(PartDamageBonus::WeaponBonus {
value: value.0 / 100.0, value: value.0 / 100.0,
@ -427,7 +347,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Throttle => { WeaponBonusType::Throttle => {
commands commands
.spawn(PartDamageBonus::WeaponBonus { .spawn(PartDamageBonus::WeaponBonus {
value: value.0 / 100.0, value: value.0 / 100.0,
@ -435,7 +355,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Roshambo => { WeaponBonusType::Roshambo => {
commands commands
.spawn(PartDamageBonus::WeaponBonus { .spawn(PartDamageBonus::WeaponBonus {
value: value.0 / 100.0, value: value.0 / 100.0,
@ -444,7 +364,7 @@ pub(crate) fn prepare_bonuses(
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Cripple => { WeaponBonusType::Cripple => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -452,7 +372,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Demoralise => { WeaponBonusType::Demoralise => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -460,7 +380,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Freeze => { WeaponBonusType::Freeze => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -468,7 +388,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Slow => { WeaponBonusType::Slow => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -476,7 +396,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Toxin => { WeaponBonusType::Toxin => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -484,7 +404,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Weaken => { WeaponBonusType::Weaken => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -492,7 +412,7 @@ pub(crate) fn prepare_bonuses(
}) })
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Wither => { WeaponBonusType::Wither => {
commands commands
.spawn(DamageProcEffect::OpponentEffect { .spawn(DamageProcEffect::OpponentEffect {
value: value.0, value: value.0,
@ -501,7 +421,7 @@ pub(crate) fn prepare_bonuses(
.set_parent(weapon.get()); .set_parent(weapon.get());
} }
WeaponBonus::Motivate => { WeaponBonusType::Motivate => {
commands commands
.spawn(DamageProcEffect::SelfEffect { .spawn(DamageProcEffect::SelfEffect {
value: value.0, value: value.0,

View file

@ -1,19 +1,25 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::{
bundle::{
passive::{Education, EducationPartDamageBonus, FactionUpgrades, Merits},
player::{Current, PartDamageBonus, Weapons},
stat::{
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
SimpleStatBonus, SimpleStatEffective, WeaponAccuracy,
},
weapon::{
Ammo, EquippedMods, Experience, Japanese, NeedsReload, Usable, Weapon, WeaponCategory,
WeaponMod, WeaponSlot,
},
},
hierarchy::{HierarchyBuilder, Parent},
};
use crate::{ use crate::{
effect::{Effects, TurnLimitedEffect}, effect::{Effects, TurnLimitedEffect},
hierarchy::{HierarchyBuilder, Parent},
log, log,
log::Logger, log::Logger,
passives::{Education, EducationPartDamageBonus, FactionUpgrades, Merits}, Stages,
player::{
stats::{
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, WeaponAccuracy,
},
Current, PartDamageBonus, Weapons,
},
Id, Name, Stages,
}; };
use self::bonus::{ use self::bonus::{
@ -23,102 +29,6 @@ use self::bonus::{
pub mod bonus; pub mod bonus;
pub mod temp; pub mod temp;
#[derive(Component)]
pub struct Usable;
#[derive(Component)]
pub struct Weapon;
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponSlot {
Primary,
Secondary,
Melee,
Temporary,
Fists,
Kick,
}
#[derive(Component, Debug, Clone, Copy)]
pub enum WeaponVerb {
Hit,
Kicked,
Fired,
Threw,
Exploded,
}
#[derive(Component, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponCategory {
HeavyArtillery,
MachineGun,
Rifle,
Smg,
Shotgun,
Pistol,
Clubbing,
Piercing,
Slashing,
Mechanical,
Temporary,
HandToHand,
}
#[derive(Component, Debug)]
pub struct DamageStat(pub f32);
#[derive(Component)]
pub struct Japanese;
#[derive(Component)]
pub struct Ammo(pub u16);
#[derive(Component)]
pub struct RateOfFire(pub [u16; 2]);
#[derive(Component)]
pub struct NeedsReload;
#[derive(Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum WeaponMod {
ReflexSight,
HolographicSight,
AcogSight,
ThermalSight,
Laser1mw,
Laser5mw,
Laser30mw,
Laser100mw,
SmallSuppressor,
StandardSuppressor,
LargeSuppressor,
ExtendedMags,
HighCapacityMags,
ExtraClip,
ExtraClip2,
AdjustableTrigger,
HairTrigger,
Bipod,
Tripod,
CustomGrip,
SkeetChoke,
ImprovedChoke,
FullChoke,
RecoilPad,
StandardBrake,
HeavyDutyBrake,
TacticalBrake,
SmallLight,
PrecisionLight,
TacticalIlluminator,
}
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum TurnTriggeredMod { pub enum TurnTriggeredMod {
Bipod, Bipod,
@ -247,109 +157,6 @@ pub enum DamageProcEffect {
}, },
} }
#[derive(Component)]
pub struct EquippedMods(pub Vec<WeaponMod>);
#[derive(Component)]
pub struct Experience(pub f32);
#[derive(Bundle)]
pub struct WeaponBundle {
pub usable: Usable,
pub weapon: Weapon,
pub name: Name,
pub id: Id,
pub verb: WeaponVerb,
pub slot: WeaponSlot,
}
#[derive(Bundle)]
pub struct DamagingWeaponBundle {
pub crit_rate: SimpleStatBundle<CritRate>,
pub dmg: DamageStat,
pub acc: SimpleStatBundle<WeaponAccuracy>,
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
pub equipped_mods: EquippedMods,
pub experience: Experience,
pub category: WeaponCategory,
}
#[derive(Bundle)]
pub struct AmmoWeaponBundle {
pub ammo: Ammo,
pub clips: SimpleStatBundle<Clips>,
pub clip_size: SimpleStatBundle<ClipSize>,
pub rate_of_fire: RateOfFire,
pub ammo_control: SimpleStatBundle<AmmoControl>,
}
impl WeaponBundle {
pub fn new(name: String, id: usize, verb: WeaponVerb, slot: WeaponSlot) -> Self {
Self {
usable: Usable,
weapon: Weapon,
name: Name(name),
id: Id(id),
verb,
slot,
}
}
pub fn fists(id: usize) -> Self {
Self::new("Fists".to_owned(), id, WeaponVerb::Hit, WeaponSlot::Fists)
}
pub fn kick(id: usize) -> Self {
Self::new("Kick".to_owned(), id, WeaponVerb::Kicked, WeaponSlot::Kick)
}
}
impl DamagingWeaponBundle {
pub fn new(
dmg: f32,
acc: f32,
mods: Vec<WeaponMod>,
exp: f32,
category: WeaponCategory,
) -> Self {
Self {
crit_rate: SimpleStatBundle::new(0),
dmg: DamageStat(dmg / 10.0),
acc: SimpleStatBundle::new((acc - 50.0) / 50.0),
dmg_bonus: SimpleStatBundle::new(1.0),
equipped_mods: EquippedMods(mods),
experience: Experience(exp),
category,
}
}
pub fn fists() -> Self {
// NOTE: The accuracy value is taken from the attack page. The damage value here differs
// from the one in Proxima's simulator, but in some quick tests 10.0 proofed to be a better fit
// This might have changed in the weapon damage update
Self::new(10.0, 50.0, Vec::default(), 0.0, WeaponCategory::HandToHand)
}
pub fn kick() -> Self {
// NOTE: The accuracy value is taken from the attack page. The damage value here differs
// from the one in Proxima's simulator, but in some quick tests 30.0 proofed to be a better fit
// This might have changed in the weapon damage update
Self::new(30.0, 40.0, Vec::default(), 0.0, WeaponCategory::HandToHand)
}
}
impl AmmoWeaponBundle {
pub fn new(clips: u16, clip_size: u16, rof: [u16; 2]) -> Self {
Self {
ammo: Ammo(clip_size),
clips: SimpleStatBundle::new(clips - 1),
clip_size: SimpleStatBundle::new(clip_size),
rate_of_fire: RateOfFire(rof),
ammo_control: SimpleStatBundle::new(0.0),
}
}
}
fn set_owner(weapons_q: Query<(Entity, &Weapons)>, mut commands: Commands) { fn set_owner(weapons_q: Query<(Entity, &Weapons)>, mut commands: Commands) {
for (player, weapons) in weapons_q.iter() { for (player, weapons) in weapons_q.iter() {
if let Some(primary) = weapons.primary { if let Some(primary) = weapons.primary {
@ -364,10 +171,8 @@ fn set_owner(weapons_q: Query<(Entity, &Weapons)>, mut commands: Commands) {
if let Some(temp) = weapons.temporary { if let Some(temp) = weapons.temporary {
commands.entity(temp).set_parent(player); commands.entity(temp).set_parent(player);
} }
commands.entity(weapons.fists.unwrap()).set_parent(player); commands.entity(weapons.fists).set_parent(player);
if let Some(kick) = weapons.kick { commands.entity(weapons.kick).set_parent(player);
commands.entity(kick).set_parent(player);
}
} }
} }

View file

@ -1,202 +1,33 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use strum::Display; use proxisim_models::bundle::{
player::{Current, CurrentTarget, Player},
use crate::{ weapon::{BuffingTemp, DebuffingTemp, Usable, Uses},
effect::Effects,
player::{
status_effect::{
ConcussionGrenade, FlashGrenade, PepperSpray, Sand, SmokeGrenade, TearGas,
TempDebuffEffect,
},
Current, CurrentTarget,
},
Stages,
}; };
use super::{DamagingWeaponBundle, Usable, WeaponBundle, WeaponCategory, WeaponSlot, WeaponVerb}; use crate::{
Stages,
#[derive(Component, Default)] effect::Effects,
pub struct NonTargeted; log,
log::Logger,
#[derive(Component, Default)] player::status_effect::{
pub struct Temporary; AdditiveStatusEffect, ConcussionGrenade, FlashGrenade, Hardened, Hastened, PepperSpray,
Sharpened, SmokeGrenade, Strengthened, TearGas, TempDebuffEffect,
#[derive(Component)] },
pub struct Uses(pub u16); };
impl Default for Uses {
fn default() -> Self {
Self(1)
}
}
#[derive(Component)] #[derive(Component)]
pub struct AssociatedWeapon(pub Entity); pub struct AssociatedWeapon(pub Entity);
#[derive(Component, Debug, Clone, Copy, Display)]
pub enum DebuffingTemp {
TearGas,
SmokeGrenade,
PepperSpray,
ConcussionGrenade,
FlashGrenade,
Sand,
}
#[derive(Bundle, Default)]
pub struct TemporaryBundle {
pub temporary: Temporary,
pub uses: Uses,
}
#[derive(Debug, Clone, Copy)]
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
pub enum Temp {
Heg,
NailBomb,
Grenade,
Fireworks,
ClaymoreMine,
TearGas,
SmokeGrenade,
PepperSpray,
ConcussionGrenade,
FlashGrenade,
Sand,
}
impl Temp {
pub fn spawn(self, world: &mut World, id: usize) -> EntityWorldMut<'_> {
match self {
Self::Heg => world.spawn((
WeaponBundle::new(
"HEG".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
DamagingWeaponBundle::new(90.00, 116.00, vec![], 0.0, WeaponCategory::Temporary),
TemporaryBundle::default(),
NonTargeted,
)),
Self::NailBomb => world.spawn((
WeaponBundle::new(
"Nail Bomb".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
DamagingWeaponBundle::new(99.00, 106.00, vec![], 0.0, WeaponCategory::Temporary),
TemporaryBundle::default(),
NonTargeted,
)),
Self::Grenade => world.spawn((
WeaponBundle::new(
"Grenade".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
DamagingWeaponBundle::new(86.00, 106.00, vec![], 0.0, WeaponCategory::Temporary),
TemporaryBundle::default(),
NonTargeted,
)),
Self::Fireworks => world.spawn((
WeaponBundle::new(
"Fireworks".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
DamagingWeaponBundle::new(45.00, 34.00, vec![], 0.0, WeaponCategory::Temporary),
TemporaryBundle::default(),
NonTargeted,
)),
Self::ClaymoreMine => world.spawn((
WeaponBundle::new(
"Claymore Mine".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
DamagingWeaponBundle::new(83.00, 27.00, vec![], 0.0, WeaponCategory::Temporary),
TemporaryBundle::default(),
NonTargeted,
)),
Self::TearGas => world.spawn((
WeaponBundle::new(
"Tear Gas".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
TemporaryBundle::default(),
DebuffingTemp::TearGas,
)),
Self::SmokeGrenade => world.spawn((
WeaponBundle::new(
"Smoke Grenade".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
TemporaryBundle::default(),
DebuffingTemp::SmokeGrenade,
)),
Self::PepperSpray => world.spawn((
WeaponBundle::new(
"Pepper Spray".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
TemporaryBundle::default(),
DebuffingTemp::PepperSpray,
)),
Self::ConcussionGrenade => world.spawn((
WeaponBundle::new(
"Concussion Grenade".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
TemporaryBundle::default(),
DebuffingTemp::ConcussionGrenade,
)),
Self::FlashGrenade => world.spawn((
WeaponBundle::new(
"Flash Grenade".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
TemporaryBundle::default(),
DebuffingTemp::FlashGrenade,
)),
Self::Sand => world.spawn((
WeaponBundle::new(
"Sand".to_owned(),
id,
WeaponVerb::Exploded,
WeaponSlot::Temporary,
),
TemporaryBundle::default(),
DebuffingTemp::Sand,
)),
}
}
}
fn use_debuffing_temp( fn use_debuffing_temp(
mut temp_q: Query<(Entity, &DebuffingTemp, &mut Uses), With<Current>>, mut temp_q: Query<(Entity, &DebuffingTemp, &mut Uses), With<Current>>,
target_q: Query<Entity, With<CurrentTarget>>, target_q: Query<Entity, With<CurrentTarget>>,
mut effects: Effects, mut effects: Effects,
mut commands: Commands, mut commands: Commands,
) { ) {
let Ok((weapon, temp, mut uses)) = temp_q.get_single_mut() else { let Ok((weapon, temp, mut uses)) = temp_q.single_mut() else {
return; return;
}; };
let target = target_q.single(); let target = target_q.single().unwrap();
match temp { match temp {
DebuffingTemp::TearGas => effects.spawn_and_insert( DebuffingTemp::TearGas => effects.spawn_and_insert(
@ -224,13 +55,54 @@ fn use_debuffing_temp(
target, target,
AssociatedWeapon(weapon), AssociatedWeapon(weapon),
), ),
DebuffingTemp::Sand => effects.spawn_and_insert( };
TempDebuffEffect::<Sand>::default(),
target, uses.0 -= 1;
if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>();
}
}
fn use_buffing_temp(
mut temp_q: Query<(Entity, &BuffingTemp, &mut Uses), With<Current>>,
current_q: Query<Entity, (With<Current>, With<Player>)>,
mut effects: Effects,
mut commands: Commands,
mut logger: Logger,
) {
let Ok((weapon, temp, mut uses)) = temp_q.single_mut() else {
return;
};
let current = current_q.single().unwrap();
match temp {
BuffingTemp::Serotonin => effects.spawn_and_insert(
AdditiveStatusEffect::<1, Hardened>::default(),
current,
AssociatedWeapon(weapon),
),
BuffingTemp::Tyrosine => effects.spawn_and_insert(
AdditiveStatusEffect::<1, Sharpened>::default(),
current,
AssociatedWeapon(weapon),
),
BuffingTemp::Melatonin => effects.spawn_and_insert(
AdditiveStatusEffect::<1, Hastened>::default(),
current,
AssociatedWeapon(weapon),
),
BuffingTemp::Epinephrine => effects.spawn_and_insert(
AdditiveStatusEffect::<1, Strengthened>::default(),
current,
AssociatedWeapon(weapon), AssociatedWeapon(weapon),
), ),
}; };
log!(logger, "used_buff_temp", {
actor: current,
weapon: weapon,
});
uses.0 -= 1; uses.0 -= 1;
if uses.0 == 0 { if uses.0 == 0 {
commands.entity(weapon).remove::<Usable>(); commands.entity(weapon).remove::<Usable>();
@ -245,5 +117,6 @@ fn restore_uses(mut uses_q: Query<&mut Uses>) {
pub(crate) fn configure(stages: &mut Stages) { pub(crate) fn configure(stages: &mut Stages) {
stages.turn.add_systems(use_debuffing_temp); stages.turn.add_systems(use_debuffing_temp);
stages.turn.add_systems(use_buffing_temp);
stages.restore.add_systems(restore_uses); stages.restore.add_systems(restore_uses);
} }