updates?
This commit is contained in:
parent
e7d6b74aab
commit
35413b563c
33 changed files with 10238 additions and 1891 deletions
23
Cargo.toml
23
Cargo.toml
|
|
@ -1,23 +1,32 @@
|
|||
[workspace]
|
||||
resolver = "2"
|
||||
members = [".", "macros"]
|
||||
members = [".", "macros", "models"]
|
||||
|
||||
[package]
|
||||
name = "proxisim"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
default = ["json"]
|
||||
debug = ["bevy_ecs/bevy_debug_stepping"]
|
||||
json = ["dep:serde", "dep:serde_json"]
|
||||
|
||||
[dependencies]
|
||||
bevy_ecs = "0.12.1"
|
||||
rand = { version = "0.8.5", default-features = false, features = ["std", "alloc", "small_rng"] }
|
||||
rand_distr = "0.4.3"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
serde = { version = "1", features = [ "derive" ], optional = true }
|
||||
bevy_ecs = { version = "0.17.2", features = [] }
|
||||
rand = { version = "0.9.2", default-features = false, features = [
|
||||
"std",
|
||||
"alloc",
|
||||
"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 }
|
||||
|
||||
[dependencies.macros]
|
||||
path = "macros"
|
||||
|
||||
[dependencies.proxisim_models]
|
||||
path = "models"
|
||||
|
|
|
|||
15
models/Cargo.toml
Normal file
15
models/Cargo.toml
Normal 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
200
models/src/bundle/armour.rs
Normal 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
199
models/src/bundle/bonus.rs
Normal 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
14
models/src/bundle/mod.rs
Normal 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);
|
||||
207
models/src/bundle/passive.rs
Normal file
207
models/src/bundle/passive.rs
Normal 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
184
models/src/bundle/player.rs
Normal 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
408
models/src/bundle/stat.rs
Normal 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
230
models/src/bundle/weapon.rs
Normal 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
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
32
models/src/dto/metrics.rs
Normal 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
14
models/src/dto/mod.rs
Normal 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
107
models/src/dto/player.rs
Normal 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
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
178
models/src/hierarchy.rs
Normal 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
3
models/src/lib.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod bundle;
|
||||
pub mod dto;
|
||||
// pub mod hierarchy;
|
||||
|
|
@ -1,227 +1,22 @@
|
|||
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 crate::{
|
||||
hierarchy::HierarchyBuilder,
|
||||
player::{
|
||||
status_effect::{
|
||||
ConcussionGrenade, FlashGrenade, PepperSpray, Sand, TearGas, TempDebuffImmunity,
|
||||
},
|
||||
BodyPart,
|
||||
player::status_effect::{
|
||||
ConcussionGrenade, FlashGrenade, PepperSpray, TearGas, TempDebuffImmunity,
|
||||
},
|
||||
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(
|
||||
equip_q: Query<(Entity, &EquippedArmour)>,
|
||||
equip_q: Query<(Entity, &PlayerArmour)>,
|
||||
armour_q: Query<(Entity, &ArmourCoverage, &ArmourValue, Option<&Immunities>)>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
|
|
@ -246,15 +41,15 @@ fn generate_body_parts(
|
|||
Immunity::PepperSpray => {
|
||||
player.insert(TempDebuffImmunity::<PepperSpray>::default());
|
||||
}
|
||||
Immunity::FlashGrenades => {
|
||||
Immunity::FlashGrenade => {
|
||||
player.insert(TempDebuffImmunity::<FlashGrenade>::default());
|
||||
}
|
||||
Immunity::Sand => {
|
||||
player.insert(TempDebuffImmunity::<Sand>::default());
|
||||
}
|
||||
Immunity::ConcussionGrenades => {
|
||||
Immunity::ConcussionGrenade => {
|
||||
player.insert(TempDebuffImmunity::<ConcussionGrenade>::default());
|
||||
}
|
||||
Immunity::Insanity => {
|
||||
// TODO: Insanity
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -264,7 +59,7 @@ fn generate_body_parts(
|
|||
parts[slot].armour_pieces.push(BodyPartCoverage {
|
||||
armour,
|
||||
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
|
||||
.entity(player)
|
||||
.add_children(parts)
|
||||
.add_children(&parts)
|
||||
.insert(ArmourBodyParts(ArmourVec(parts)));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
177
src/attacker.json
Normal file
177
src/attacker.json
Normal 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
97
src/defender.json
Normal 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
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
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::player::PlayerStrategy;
|
||||
use crate::weapon::Japanese;
|
||||
|
|
|
|||
|
|
@ -1,21 +1,21 @@
|
|||
use std::{any::TypeId, collections::HashMap, sync::Mutex};
|
||||
|
||||
use bevy_ecs::{prelude::*, system::SystemParam};
|
||||
|
||||
use crate::{
|
||||
use proxisim_models::{
|
||||
bundle::player::{Current, Defender, Player},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
player::{Current, Defender, Player},
|
||||
Stages,
|
||||
};
|
||||
|
||||
use crate::Stages;
|
||||
|
||||
const SECONDS_PER_ACTION: f32 = 1.1;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Component)]
|
||||
pub struct EffectId(usize);
|
||||
|
||||
pub struct EffectInfo {
|
||||
pub apply: Box<dyn System<In = Vec<Entity>, Out = ()>>,
|
||||
pub teardown: Box<dyn System<In = Vec<Entity>, Out = ()>>,
|
||||
pub apply: Box<dyn System<In = 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
|
||||
|
|
@ -114,8 +114,8 @@ impl<'w, 's> Effects<'w, 's> {
|
|||
|
||||
pub struct EffectBuilder<'w> {
|
||||
world: &'w mut World,
|
||||
apply: Option<Box<dyn System<In = Vec<Entity>, Out = ()>>>,
|
||||
teardown: Option<Box<dyn System<In = Vec<Entity>, Out = ()>>>,
|
||||
apply: Option<Box<dyn System<In = In<Vec<Entity>>, Out = ()>>>,
|
||||
teardown: Option<Box<dyn System<In = In<Vec<Entity>>, Out = ()>>>,
|
||||
id: EffectId,
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +123,7 @@ impl<'r> EffectBuilder<'r> {
|
|||
#[must_use]
|
||||
pub fn apply<A, S>(&mut self, system: S) -> &mut Self
|
||||
where
|
||||
S: IntoSystem<Vec<Entity>, (), A> + 'static,
|
||||
S: IntoSystem<In<Vec<Entity>>, (), A> + 'static,
|
||||
{
|
||||
let mut system = IntoSystem::into_system(system);
|
||||
system.initialize(self.world);
|
||||
|
|
@ -134,7 +134,7 @@ impl<'r> EffectBuilder<'r> {
|
|||
#[must_use]
|
||||
pub fn teardown<T, S>(&mut self, system: S) -> &mut Self
|
||||
where
|
||||
S: IntoSystem<Vec<Entity>, (), T> + 'static,
|
||||
S: IntoSystem<In<Vec<Entity>>, (), T> + 'static,
|
||||
{
|
||||
let mut system = IntoSystem::into_system(system);
|
||||
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 mut registry = unsafe_world.get_resource_mut::<EffectRegistry>().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());
|
||||
};
|
||||
}
|
||||
|
|
@ -196,7 +196,8 @@ pub(crate) fn run_effects(world: &mut World) {
|
|||
let mut registry = unsafe_world.get_resource_mut::<EffectRegistry>().unwrap();
|
||||
let info = registry.system_info.get_mut(&next_effect).unwrap();
|
||||
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());
|
||||
};
|
||||
|
||||
|
|
@ -209,7 +210,7 @@ pub(crate) fn run_effects(world: &mut World) {
|
|||
}
|
||||
|
||||
#[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 id = EffectId(registry.id_counter);
|
||||
registry.id_counter += 1;
|
||||
|
|
@ -245,7 +246,7 @@ fn advance_clock(
|
|||
mut clock: ResMut<Clock>,
|
||||
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 {
|
||||
clock.0 += SECONDS_PER_ACTION;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,17 @@
|
|||
use std::collections::HashMap;
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::{
|
||||
use proxisim_models::{
|
||||
bundle::{
|
||||
player::{Attacker, Player},
|
||||
weapon::Weapon,
|
||||
Id, Name,
|
||||
},
|
||||
dto::metrics::EntityInfo,
|
||||
hierarchy::Parent,
|
||||
player::{Attacker, Player},
|
||||
weapon::Weapon,
|
||||
Id, Name, Stages,
|
||||
};
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
use crate::Stages;
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct EntityRegistry(pub HashMap<Entity, EntityInfo>);
|
||||
|
|
|
|||
|
|
@ -78,10 +78,10 @@ pub trait HierarchyBuilder {
|
|||
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 {
|
||||
let parent = self.id();
|
||||
self.commands().add(AddChild { parent, child });
|
||||
self.commands().queue(AddChild { parent, child });
|
||||
self.commands().entity(child).insert(Parent(parent));
|
||||
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 {
|
||||
let children = children.as_ref();
|
||||
let parent = self.id();
|
||||
self.commands().add(AddChildren {
|
||||
self.commands().queue(AddChildren {
|
||||
parent,
|
||||
children: children.to_owned(),
|
||||
});
|
||||
self.commands().insert_or_spawn_batch(
|
||||
self.commands().insert_batch(
|
||||
children
|
||||
.iter()
|
||||
.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 {
|
||||
let parent = self.id();
|
||||
self.commands().add(RemoveChild { parent, child });
|
||||
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().add(AddChild { parent, child });
|
||||
self.commands().queue(AddChild { parent, child });
|
||||
self.commands().entity(child).insert(Parent(parent));
|
||||
self
|
||||
}
|
||||
|
|
|
|||
151
src/lib.rs
151
src/lib.rs
|
|
@ -1,32 +1,29 @@
|
|||
#![warn(clippy::perf, clippy::style, clippy::all)]
|
||||
#![allow(clippy::type_complexity)]
|
||||
use bevy_ecs::{prelude::*, schedule::ScheduleLabel};
|
||||
use bevy_ecs::{message::MessageRegistry, prelude::*, schedule::ScheduleLabel};
|
||||
use effect::{register_effect, EffectBuilder};
|
||||
use metrics::Metrics;
|
||||
use proxisim_models::{
|
||||
bundle::player::{Attacker, Current, Defender},
|
||||
dto::{
|
||||
metrics::{Counter, Histogram},
|
||||
player::PlayerDto,
|
||||
},
|
||||
};
|
||||
use rand::SeedableRng;
|
||||
|
||||
use crate::{
|
||||
log::{Log, Logging},
|
||||
player::{Attacker, Current, Defender},
|
||||
};
|
||||
use crate::log::{Log, Logging};
|
||||
|
||||
mod armour;
|
||||
pub mod dto;
|
||||
mod effect;
|
||||
mod entity_registry;
|
||||
mod hierarchy;
|
||||
// mod hierarchy;
|
||||
pub mod log;
|
||||
mod metrics;
|
||||
mod passives;
|
||||
mod player;
|
||||
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`
|
||||
// instead. Then again, the whole simulator isn't very parallelisable, so it may be a moot point
|
||||
#[derive(Resource)]
|
||||
|
|
@ -79,19 +76,20 @@ struct Stages {
|
|||
impl Stages {
|
||||
fn add_event<T>(&mut self) -> &mut Self
|
||||
where
|
||||
T: Event,
|
||||
T: Message,
|
||||
{
|
||||
if !self.world.contains_resource::<Events<T>>() {
|
||||
self.world.init_resource::<Events<T>>();
|
||||
if !self.world.contains_resource::<Messages<T>>() {
|
||||
/* self.world.init_resource::<Messages<T>>();
|
||||
self.pre_turn.add_systems(
|
||||
bevy_ecs::event::event_update_system::<T>
|
||||
.run_if(bevy_ecs::event::event_update_condition::<T>),
|
||||
);
|
||||
); */
|
||||
MessageRegistry::register_message::<T>(&mut self.world);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
fn register_effect<Effect: 'static>(&mut self) -> EffectBuilder {
|
||||
fn register_effect<Effect: 'static>(&mut self) -> EffectBuilder<'_> {
|
||||
register_effect::<Effect>(self)
|
||||
}
|
||||
}
|
||||
|
|
@ -99,7 +97,7 @@ impl Stages {
|
|||
pub struct Simulation(Stages);
|
||||
|
||||
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 mut stages = Stages {
|
||||
equip: Schedule::new(Stage::Equip),
|
||||
|
|
@ -125,7 +123,7 @@ impl Simulation {
|
|||
stages.world.insert_resource(FightStatus::Ongoing);
|
||||
stages
|
||||
.world
|
||||
.insert_resource(Rng(rand::rngs::SmallRng::from_entropy()));
|
||||
.insert_resource(Rng(rand::rngs::SmallRng::from_os_rng()));
|
||||
|
||||
stages.world.insert_resource(Logging(true));
|
||||
|
||||
|
|
@ -157,17 +155,17 @@ impl Simulation {
|
|||
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 components = self.0.world.components().len();
|
||||
self.0
|
||||
.world
|
||||
.resource::<metrics::Metrics>()
|
||||
.increment_counter(Entity::from_raw(0), "entities", entities.into());
|
||||
.increment_counter(None, "entities", entities.into());
|
||||
self.0
|
||||
.world
|
||||
.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)
|
||||
}
|
||||
|
||||
|
|
@ -333,110 +331,12 @@ impl Simulation {
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn attacker() -> dto::Player {
|
||||
use dto::*;
|
||||
|
||||
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 attacker() -> PlayerDto {
|
||||
serde_json::from_str(include_str!("./attacker.json")).unwrap()
|
||||
}
|
||||
|
||||
fn defender() -> dto::Player {
|
||||
use dto::*;
|
||||
|
||||
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()
|
||||
},
|
||||
}
|
||||
fn defender() -> PlayerDto {
|
||||
serde_json::from_str(include_str!("./defender.json")).unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -450,6 +350,5 @@ mod tests {
|
|||
sim.set_metrics(true);
|
||||
sim.run_once();
|
||||
sim.consume_metrics();
|
||||
panic!();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
106
src/log.rs
106
src/log.rs
|
|
@ -1,23 +1,24 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use bevy_ecs::{prelude::*, system::SystemParam};
|
||||
|
||||
use crate::{
|
||||
entity_registry::EntityRegistry,
|
||||
hierarchy::Children,
|
||||
player::stats::{
|
||||
AdditiveBonus, BaselineStat, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat,
|
||||
MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective,
|
||||
SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy,
|
||||
use bevy_ecs::{prelude::*, query::QuerySingleError, system::SystemParam};
|
||||
use proxisim_models::{
|
||||
bundle::{
|
||||
stat::{
|
||||
AdditiveBonus, BaselineStat, ClipSize, Clips, CritRate, DamageBonus, Defence,
|
||||
Dexterity, EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus,
|
||||
SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy,
|
||||
},
|
||||
weapon::WeaponVerb,
|
||||
},
|
||||
weapon::WeaponVerb,
|
||||
Stages,
|
||||
hierarchy::Children,
|
||||
};
|
||||
|
||||
use crate::{Stages, entity_registry::EntityRegistry};
|
||||
|
||||
#[derive(Resource)]
|
||||
pub struct Logging(pub bool);
|
||||
|
||||
#[derive(Event)]
|
||||
#[derive(Message)]
|
||||
struct LogEvent(Mutex<Option<DynamicLogMessage>>);
|
||||
|
||||
impl From<DynamicLogMessage> for LogEvent {
|
||||
|
|
@ -38,66 +39,71 @@ pub struct WeaponInfo {
|
|||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum LogValue<'a> {
|
||||
pub enum LogValue {
|
||||
Float(f32),
|
||||
Unsigned(u32),
|
||||
Signed(i32),
|
||||
Bool(bool),
|
||||
String(String),
|
||||
Entity(Entity),
|
||||
OptionNone,
|
||||
Display(&'a (dyn std::fmt::Display + Send + Sync)),
|
||||
Debug(&'a (dyn std::fmt::Debug + Send + Sync)),
|
||||
Player(Entity),
|
||||
Weapon(Entity),
|
||||
Array(Vec<LogValue<'a>>),
|
||||
Map(Vec<(&'static str, LogValue<'a>)>),
|
||||
Array(Vec<LogValue>),
|
||||
Map(Vec<(&'static str, LogValue)>),
|
||||
}
|
||||
|
||||
impl<'a> From<String> for LogValue<'a> {
|
||||
impl From<String> for LogValue {
|
||||
fn from(value: String) -> Self {
|
||||
Self::String(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for LogValue<'static> {
|
||||
impl<'a> From<&'a str> for LogValue {
|
||||
fn from(value: &'a str) -> Self {
|
||||
Self::String(value.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<f32> for LogValue<'a> {
|
||||
impl From<f32> for LogValue {
|
||||
fn from(value: f32) -> Self {
|
||||
Self::Float(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<u32> for LogValue<'a> {
|
||||
impl From<u32> for LogValue {
|
||||
fn from(value: u32) -> Self {
|
||||
Self::Unsigned(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<u16> for LogValue<'a> {
|
||||
impl From<u16> for LogValue {
|
||||
fn from(value: u16) -> Self {
|
||||
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 {
|
||||
Self::Bool(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Entity> for LogValue<'static> {
|
||||
impl From<Entity> for LogValue {
|
||||
fn from(value: Entity) -> Self {
|
||||
Self::Entity(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<Option<T>> for LogValue<'a>
|
||||
impl<T> From<Option<T>> for LogValue
|
||||
where
|
||||
T: Into<LogValue<'a>>,
|
||||
T: Into<LogValue>,
|
||||
{
|
||||
fn from(value: Option<T>) -> Self {
|
||||
match value {
|
||||
|
|
@ -107,26 +113,38 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<'a, V> From<Vec<V>> for LogValue<'a>
|
||||
impl<V> From<Vec<V>> for LogValue
|
||||
where
|
||||
V: Into<LogValue<'a>>,
|
||||
V: Into<LogValue>,
|
||||
{
|
||||
fn from(value: Vec<V>) -> Self {
|
||||
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
|
||||
V: Into<LogValue<'a>>,
|
||||
V: Into<LogValue>,
|
||||
{
|
||||
fn from(value: Vec<(&'static str, V)>) -> Self {
|
||||
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")]
|
||||
impl<'a> LogValue<'a> {
|
||||
impl LogValue {
|
||||
fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value {
|
||||
match self {
|
||||
LogValue::OptionNone => serde_json::Value::Null,
|
||||
|
|
@ -136,8 +154,7 @@ impl<'a> LogValue<'a> {
|
|||
LogValue::String(val) => serde_json::Value::String(val.clone()),
|
||||
LogValue::Bool(val) => serde_json::Value::Bool(*val),
|
||||
LogValue::Unsigned(val) => serde_json::Value::Number(serde_json::Number::from(*val)),
|
||||
LogValue::Debug(boxed) => serde_json::Value::String(format!("{boxed:?}")),
|
||||
LogValue::Display(boxed) => serde_json::Value::String(format!("{boxed}")),
|
||||
LogValue::Signed(val) => serde_json::Value::Number(serde_json::Number::from(*val)),
|
||||
LogValue::Player(id) => {
|
||||
serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap()
|
||||
}
|
||||
|
|
@ -162,7 +179,7 @@ impl<'a> LogValue<'a> {
|
|||
trait LogMessage: Send + Sync + 'static {
|
||||
fn tag(&self) -> &'static str;
|
||||
|
||||
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)>;
|
||||
fn entries(&self) -> Vec<(&'static str, LogValue)>;
|
||||
}
|
||||
|
||||
#[derive(Resource, Default)]
|
||||
|
|
@ -212,11 +229,10 @@ impl std::fmt::Display for Log {
|
|||
LogValue::Float(val) => write!(f, "{val}")?,
|
||||
LogValue::Bool(val) => write!(f, "{val}")?,
|
||||
LogValue::Unsigned(val) => write!(f, "{val}")?,
|
||||
LogValue::Signed(val) => write!(f, "{val}")?,
|
||||
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) => {
|
||||
write!(f, "{:?}", id)?
|
||||
write!(f, "{id}")?
|
||||
}
|
||||
LogValue::Array(_) | LogValue::Map(_) => (),
|
||||
};
|
||||
|
|
@ -421,7 +437,7 @@ impl std::fmt::Display for Log {
|
|||
|
||||
#[derive(SystemParam)]
|
||||
pub struct Logger<'w> {
|
||||
event_writer: EventWriter<'w, LogEvent>,
|
||||
event_writer: MessageWriter<'w, LogEvent>,
|
||||
logging: Res<'w, Logging>,
|
||||
}
|
||||
|
||||
|
|
@ -431,7 +447,7 @@ impl<'w> Logger<'w> {
|
|||
B: FnOnce() -> DynamicLogMessage,
|
||||
{
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
if let Some(entry) = event.0.lock().unwrap().take() {
|
||||
log.entries.push(entry);
|
||||
|
|
@ -450,7 +466,7 @@ fn append_log_messages(mut events: EventReader<LogEvent>, mut log: ResMut<Log>)
|
|||
|
||||
pub struct DynamicLogMessage {
|
||||
pub label: &'static str,
|
||||
pub entries: Vec<(&'static str, LogValue<'static>)>,
|
||||
pub entries: Vec<(&'static str, LogValue)>,
|
||||
}
|
||||
|
||||
impl LogMessage for DynamicLogMessage {
|
||||
|
|
@ -458,7 +474,7 @@ impl LogMessage for DynamicLogMessage {
|
|||
self.label
|
||||
}
|
||||
|
||||
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)> {
|
||||
fn entries(&self) -> Vec<(&'static str, LogValue)> {
|
||||
self.entries.clone()
|
||||
}
|
||||
}
|
||||
|
|
@ -549,8 +565,8 @@ fn log_simple_stat_changes<Stat: SimpleStatMarker>(
|
|||
bonus_q: Query<&SimpleStatBonus<Stat>>,
|
||||
mut logger: Logger,
|
||||
) where
|
||||
Stat::ValueType: Into<LogValue<'static>>,
|
||||
Stat::BonusType: Into<LogValue<'static>>,
|
||||
Stat::ValueType: Into<LogValue>,
|
||||
Stat::BonusType: Into<LogValue>,
|
||||
{
|
||||
for (target, baseline, effective, children) in stat_q.iter() {
|
||||
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::<WeaponAccuracy>,
|
||||
log_simple_stat_changes::<DamageBonus>,
|
||||
log_simple_stat_changes::<Clips>,
|
||||
log_simple_stat_changes::<ClipSize>,
|
||||
)
|
||||
.run_if(logging_enabled),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ use std::{
|
|||
sync::{atomic, Mutex, RwLock},
|
||||
};
|
||||
|
||||
use crate::{dto, entity_registry::EntityRegistry, Stages};
|
||||
use crate::{entity_registry::EntityRegistry, Stages};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Histogram<T>
|
||||
|
|
@ -38,7 +38,7 @@ impl Counter {
|
|||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct MetricKey {
|
||||
entity: Entity,
|
||||
entity: Option<Entity>,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ pub struct 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 {
|
||||
let key = MetricKey { entity, label };
|
||||
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 {
|
||||
let key = MetricKey { entity, label };
|
||||
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 entities = world.resource::<EntityRegistry>();
|
||||
|
||||
|
|
@ -100,8 +105,12 @@ pub(crate) fn consume_metrics(world: &World) -> (Vec<dto::Counter>, Vec<dto::His
|
|||
.try_write()
|
||||
.unwrap()
|
||||
.drain()
|
||||
.map(|(key, value)| dto::Counter {
|
||||
entity: entities.0.get(&key.entity).unwrap().clone(),
|
||||
.map(|(key, value)| proxisim_models::dto::metrics::Counter {
|
||||
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),
|
||||
label: key.label,
|
||||
})
|
||||
|
|
@ -112,8 +121,12 @@ pub(crate) fn consume_metrics(world: &World) -> (Vec<dto::Counter>, Vec<dto::His
|
|||
.try_write()
|
||||
.unwrap()
|
||||
.drain()
|
||||
.map(|(key, value)| dto::Histogram {
|
||||
entity: entities.0.get(&key.entity).unwrap().clone(),
|
||||
.map(|(key, value)| proxisim_models::dto::metrics::Histogram {
|
||||
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(),
|
||||
label: key.label,
|
||||
})
|
||||
|
|
|
|||
215
src/passives.rs
215
src/passives.rs
|
|
@ -1,217 +1,10 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::{
|
||||
effect::Effects,
|
||||
player::{
|
||||
stats::{AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength},
|
||||
BodyPart,
|
||||
},
|
||||
Stages,
|
||||
use proxisim_models::bundle::{
|
||||
passive::{DrugCooldown, Education, FactionUpgrades, Merits},
|
||||
stat::{AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength},
|
||||
};
|
||||
|
||||
#[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,
|
||||
}
|
||||
use crate::{effect::Effects, Stages};
|
||||
|
||||
fn spawn_permanent_effects(
|
||||
merit_q: Query<(
|
||||
|
|
|
|||
|
|
@ -1,224 +1,59 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
use proxisim_models::{
|
||||
bundle::{
|
||||
armour::{ArmourBodyPart, ArmourBodyParts},
|
||||
passive::{FactionUpgrades, Merits},
|
||||
player::{
|
||||
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated,
|
||||
Defender, FightEndType, Level, MaxHealth, PartDamageBonus, Player, PlayerStrategy,
|
||||
Weapons,
|
||||
},
|
||||
stat::{
|
||||
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health,
|
||||
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
|
||||
},
|
||||
weapon::{
|
||||
Ammo, DamageStat, NeedsReload, NonTargeted, RateOfFire, Usable, Uses, Weapon,
|
||||
WeaponSlot,
|
||||
},
|
||||
},
|
||||
hierarchy::Children,
|
||||
};
|
||||
use rand::Rng as _;
|
||||
use strum::Display;
|
||||
|
||||
use crate::{
|
||||
armour,
|
||||
FightStatus, Rng, Stages,
|
||||
effect::Effects,
|
||||
hierarchy::Children,
|
||||
log,
|
||||
log::Logger,
|
||||
metrics::Metrics,
|
||||
passives::{EducationPartDamageBonus, FactionUpgrades, Merits},
|
||||
weapon::{
|
||||
bonus::{BonusPartDamageBonus, MultiTurnBonus},
|
||||
temp::{NonTargeted, Uses},
|
||||
Ammo, DamageProcEffect, DamageStat, NeedsReload, RateOfFire, TurnTriggeredEffect, Usable,
|
||||
Weapon, WeaponSlot,
|
||||
},
|
||||
FightStatus, Id, Name, Rng, Stages,
|
||||
};
|
||||
|
||||
use self::stats::{
|
||||
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health,
|
||||
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
|
||||
weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus},
|
||||
};
|
||||
|
||||
pub mod stats;
|
||||
pub mod status_effect;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Attacker;
|
||||
fn select_weapon(
|
||||
weapons: &Weapons,
|
||||
slot: WeaponSlot,
|
||||
reload: bool,
|
||||
usable_q: &Query<(Has<NeedsReload>, Option<&Children>), With<Usable>>,
|
||||
) -> Option<(Entity, Option<Children>)> {
|
||||
let id = match slot {
|
||||
WeaponSlot::Primary => weapons.primary?,
|
||||
WeaponSlot::Secondary => weapons.secondary?,
|
||||
WeaponSlot::Melee => weapons.melee?,
|
||||
WeaponSlot::Temporary => weapons.temporary?,
|
||||
WeaponSlot::Fists => weapons.fists,
|
||||
WeaponSlot::Kick => weapons.kick,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Defender;
|
||||
let (needs_reload, children) = usable_q.get(id).ok()?;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Defeated;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Current;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct CurrentTarget;
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct Player;
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct Level(pub u16);
|
||||
|
||||
impl Default for Level {
|
||||
fn default() -> Self {
|
||||
Self(1)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
pub struct MaxHealth(pub u16);
|
||||
|
||||
impl Default for MaxHealth {
|
||||
fn default() -> Self {
|
||||
Self(100)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug, Default)]
|
||||
pub struct CombatTurns(pub u16);
|
||||
|
||||
#[derive(Component, Default, Debug)]
|
||||
pub struct Weapons {
|
||||
pub primary: Option<Entity>,
|
||||
pub secondary: Option<Entity>,
|
||||
pub melee: Option<Entity>,
|
||||
pub temporary: Option<Entity>,
|
||||
pub fists: Option<Entity>,
|
||||
pub kick: Option<Entity>,
|
||||
}
|
||||
|
||||
impl Weapons {
|
||||
fn select(
|
||||
&self,
|
||||
slot: WeaponSlot,
|
||||
reload: bool,
|
||||
usable_q: &Query<(Has<NeedsReload>, &Children), With<Usable>>,
|
||||
) -> Option<(Entity, Children)> {
|
||||
let id = match slot {
|
||||
WeaponSlot::Primary => self.primary?,
|
||||
WeaponSlot::Secondary => self.secondary?,
|
||||
WeaponSlot::Melee => self.melee?,
|
||||
WeaponSlot::Temporary => self.temporary?,
|
||||
WeaponSlot::Fists => self.fists?,
|
||||
WeaponSlot::Kick => self.kick?,
|
||||
};
|
||||
|
||||
let (needs_reload, children) = usable_q.get(id).ok()?;
|
||||
|
||||
if !reload && needs_reload {
|
||||
None
|
||||
} else {
|
||||
Some((id, children.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Debug)]
|
||||
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
|
||||
pub enum PlayerStrategy {
|
||||
AlwaysFists,
|
||||
AlwaysKicks,
|
||||
PrimaryMelee {
|
||||
reload: bool,
|
||||
},
|
||||
InOrder {
|
||||
order: Vec<WeaponSlot>,
|
||||
reload: bool,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for PlayerStrategy {
|
||||
fn default() -> Self {
|
||||
Self::AlwaysFists
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct ChooseWeapon(pub Entity);
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BodyPart {
|
||||
LeftHand,
|
||||
RightHand,
|
||||
LeftArm,
|
||||
RightArm,
|
||||
LeftFoot,
|
||||
RightFoot,
|
||||
LeftLeg,
|
||||
RightLeg,
|
||||
Stomach,
|
||||
Chest,
|
||||
Groin,
|
||||
Head,
|
||||
Throat,
|
||||
Heart,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for BodyPart {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::LeftHand => write!(f, "Left hand"),
|
||||
Self::RightHand => write!(f, "Right hand"),
|
||||
Self::LeftArm => write!(f, "Left arm"),
|
||||
Self::RightArm => write!(f, "Right arm"),
|
||||
Self::LeftFoot => write!(f, "Left foot"),
|
||||
Self::RightFoot => write!(f, "Right foot"),
|
||||
Self::LeftLeg => write!(f, "Left leg"),
|
||||
Self::RightLeg => write!(f, "Right leg"),
|
||||
Self::Stomach => write!(f, "Stomach"),
|
||||
Self::Chest => write!(f, "Chest"),
|
||||
Self::Groin => write!(f, "Groin"),
|
||||
Self::Head => write!(f, "Head"),
|
||||
Self::Throat => write!(f, "Throat"),
|
||||
Self::Heart => write!(f, "Heart"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Display)]
|
||||
pub enum FightEndType {
|
||||
Victory,
|
||||
Stalemate,
|
||||
Loss,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub enum PartDamageBonus {
|
||||
Education(EducationPartDamageBonus),
|
||||
WeaponBonus {
|
||||
value: f32,
|
||||
bonus: BonusPartDamageBonus,
|
||||
},
|
||||
}
|
||||
|
||||
impl PartDamageBonus {
|
||||
pub fn dmg_bonus(&self, part: BodyPart) -> Option<f32> {
|
||||
match self {
|
||||
Self::Education(edu) => edu.dmg_bonus(part),
|
||||
Self::WeaponBonus { value, bonus } => bonus.dmg_bonus(part, *value),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct PlayerBundle {
|
||||
pub name: Name,
|
||||
pub id: Id,
|
||||
pub player: Player,
|
||||
pub level: Level,
|
||||
pub crit_rate: SimpleStatBundle<CritRate>,
|
||||
// TODO: since these two need to be tracked here anyways it might be preferable to shift all
|
||||
// player specific passives here instead of tracking them on the weapons
|
||||
pub acc_bonus: SimpleStatBundle<WeaponAccuracy>,
|
||||
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
|
||||
|
||||
pub strategy: PlayerStrategy,
|
||||
pub combat_turns: CombatTurns,
|
||||
}
|
||||
|
||||
impl PlayerBundle {
|
||||
pub fn new(name: impl ToString, id: usize, level: u16, strategy: PlayerStrategy) -> Self {
|
||||
Self {
|
||||
name: Name(name.to_string()),
|
||||
id: Id(id),
|
||||
player: Player,
|
||||
level: Level(level),
|
||||
crit_rate: SimpleStatBundle::new(24),
|
||||
acc_bonus: SimpleStatBundle::new(0.0),
|
||||
dmg_bonus: SimpleStatBundle::new(0.0),
|
||||
strategy,
|
||||
combat_turns: Default::default(),
|
||||
}
|
||||
if !reload && needs_reload {
|
||||
None
|
||||
} else {
|
||||
Some((id, children.cloned()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,7 +85,7 @@ fn designate_first(
|
|||
mut commands: Commands,
|
||||
) {
|
||||
let attacker = attacker_q.iter().next().unwrap();
|
||||
let defender = defender_q.single();
|
||||
let defender = defender_q.single().unwrap();
|
||||
commands.entity(attacker).insert(Current);
|
||||
commands.entity(defender).insert(CurrentTarget);
|
||||
}
|
||||
|
|
@ -260,8 +95,8 @@ fn change_roles(
|
|||
target_q: Query<Entity, With<CurrentTarget>>,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
let current = current_q.single();
|
||||
let target = target_q.single();
|
||||
let current = current_q.single().unwrap();
|
||||
let target = target_q.single().unwrap();
|
||||
|
||||
// TODO: Group fights
|
||||
commands
|
||||
|
|
@ -291,36 +126,38 @@ pub fn pick_action(
|
|||
(With<Current>, With<Player>),
|
||||
>,
|
||||
target_q: Query<Entity, With<CurrentTarget>>,
|
||||
usable_q: Query<(Has<NeedsReload>, &Children), With<Usable>>,
|
||||
usable_q: Query<(Has<NeedsReload>, Option<&Children>), With<Usable>>,
|
||||
weapon_trigger_q: Query<&TurnTriggeredEffect>,
|
||||
mut commands: Commands,
|
||||
mut effects: Effects,
|
||||
metrics: Res<Metrics>,
|
||||
) {
|
||||
let (current, weapons, strat, mut turns) = p_query.single_mut();
|
||||
let (current, weapons, strat, mut turns) = p_query.single_mut().unwrap();
|
||||
let (weapon, children) = match strat {
|
||||
PlayerStrategy::AlwaysFists => (weapons.fists.unwrap(), Default::default()),
|
||||
PlayerStrategy::AlwaysKicks => weapons
|
||||
.select(WeaponSlot::Kick, true, &usable_q)
|
||||
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
|
||||
PlayerStrategy::PrimaryMelee { reload } => weapons
|
||||
.select(WeaponSlot::Primary, *reload, &usable_q)
|
||||
.or_else(|| weapons.select(WeaponSlot::Melee, true, &usable_q))
|
||||
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
|
||||
PlayerStrategy::AlwaysFists => (weapons.fists, None),
|
||||
PlayerStrategy::AlwaysKicks => select_weapon(weapons, WeaponSlot::Kick, true, &usable_q)
|
||||
.unwrap_or_else(|| (weapons.fists, Default::default())),
|
||||
PlayerStrategy::PrimaryMelee { reload } => {
|
||||
select_weapon(weapons, WeaponSlot::Primary, *reload, &usable_q)
|
||||
.or_else(|| select_weapon(weapons, WeaponSlot::Melee, true, &usable_q))
|
||||
.unwrap_or_else(|| (weapons.fists, Default::default()))
|
||||
}
|
||||
PlayerStrategy::InOrder { order, reload } => order
|
||||
.iter()
|
||||
.find_map(|slot| weapons.select(*slot, *reload, &usable_q))
|
||||
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
|
||||
.find_map(|slot| select_weapon(weapons, *slot, *reload, &usable_q))
|
||||
.unwrap_or_else(|| (weapons.fists, Default::default())),
|
||||
};
|
||||
metrics.increment_counter(current, "turn", 1);
|
||||
metrics.increment_counter(weapon, "turn", 1);
|
||||
metrics.increment_counter(Some(current), "turn", 1);
|
||||
metrics.increment_counter(Some(weapon), "turn", 1);
|
||||
|
||||
commands.entity(weapon).insert(Current);
|
||||
|
||||
let target = target_q.single();
|
||||
let target = target_q.single().unwrap();
|
||||
|
||||
for effect in weapon_trigger_q.iter_many(children.get()) {
|
||||
effect.trigger(&mut effects, current, target);
|
||||
if let Some(children) = children {
|
||||
for effect in weapon_trigger_q.iter_many(children.get()) {
|
||||
effect.trigger(&mut effects, current, target);
|
||||
}
|
||||
}
|
||||
|
||||
turns.0 += 1;
|
||||
|
|
@ -368,12 +205,12 @@ pub fn use_damaging_weapon(
|
|||
Entity,
|
||||
&EffectiveStat<Dexterity>,
|
||||
&EffectiveStat<Defence>,
|
||||
&armour::ArmourBodyParts,
|
||||
&ArmourBodyParts,
|
||||
&mut SimpleStatEffective<Health>,
|
||||
),
|
||||
With<CurrentTarget>,
|
||||
>,
|
||||
armour_q: Query<&armour::ArmourBodyPart>,
|
||||
armour_q: Query<&ArmourBodyPart>,
|
||||
(damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>),
|
||||
(mut ammo_q, mut temp_q): (
|
||||
Query<(
|
||||
|
|
@ -392,13 +229,13 @@ pub fn use_damaging_weapon(
|
|||
Effects,
|
||||
),
|
||||
) {
|
||||
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.get_single()
|
||||
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.single()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) =
|
||||
player_q.single();
|
||||
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut();
|
||||
player_q.single().unwrap();
|
||||
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut().unwrap();
|
||||
|
||||
if let Ok(mut uses) = temp_q.get_mut(weapon) {
|
||||
uses.0 -= 1;
|
||||
|
|
@ -448,10 +285,10 @@ pub fn use_damaging_weapon(
|
|||
+ 30.0;
|
||||
|
||||
loop {
|
||||
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| {
|
||||
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0);
|
||||
metrics.increment_counter(player, "rounds_fired", rounds.into());
|
||||
metrics.increment_counter(weapon, "rounds_fired", rounds.into());
|
||||
let rounds = ammo.as_mut().map(|(ammo, clips, rof)| {
|
||||
let rounds = (rng.random_range(rof.clone()).round() as u16).clamp(1, ammo.0);
|
||||
metrics.increment_counter(Some(player), "rounds_fired", rounds.into());
|
||||
metrics.increment_counter(Some(weapon), "rounds_fired", rounds.into());
|
||||
ammo.0 -= rounds;
|
||||
if ammo.0 == 0 {
|
||||
if clips.value == 0 {
|
||||
|
|
@ -469,7 +306,7 @@ pub fn use_damaging_weapon(
|
|||
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
|
||||
};
|
||||
|
||||
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||
if hit_chance <= 1.0 && !rng.random_bool(hit_chance as f64) {
|
||||
log!(logger, "miss_target", {
|
||||
weapon: weapon,
|
||||
actor: player,
|
||||
|
|
@ -477,8 +314,8 @@ pub fn use_damaging_weapon(
|
|||
rounds: rounds,
|
||||
hit_chance: hit_chance,
|
||||
});
|
||||
metrics.increment_counter(player, "miss", 1);
|
||||
metrics.increment_counter(weapon, "miss", 1);
|
||||
metrics.increment_counter(Some(player), "miss", 1);
|
||||
metrics.increment_counter(Some(weapon), "miss", 1);
|
||||
|
||||
if multi_attack_proc.is_none() {
|
||||
return;
|
||||
|
|
@ -492,8 +329,8 @@ pub fn use_damaging_weapon(
|
|||
|
||||
let mult = match body_part {
|
||||
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
|
||||
metrics.increment_counter(player, "crit", 1);
|
||||
metrics.increment_counter(weapon, "crit", 1);
|
||||
metrics.increment_counter(Some(player), "crit", 1);
|
||||
metrics.increment_counter(Some(weapon), "crit", 1);
|
||||
1.0
|
||||
}
|
||||
BodyPart::LeftHand
|
||||
|
|
@ -506,8 +343,8 @@ pub fn use_damaging_weapon(
|
|||
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
|
||||
};
|
||||
|
||||
metrics.increment_counter(player, "hit", 1);
|
||||
metrics.increment_counter(weapon, "hit", 1);
|
||||
metrics.increment_counter(Some(player), "hit", 1);
|
||||
metrics.increment_counter(Some(weapon), "hit", 1);
|
||||
|
||||
let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap();
|
||||
let piece = rng.sample(armour_parts);
|
||||
|
|
@ -530,15 +367,15 @@ pub fn use_damaging_weapon(
|
|||
|
||||
let dmg = dmg_intrinsic
|
||||
* w_dmg.0
|
||||
* dmg_bonus.value
|
||||
* (1.0 + dmg_bonus.value)
|
||||
* (1.0 - armour_mitigation)
|
||||
* (1.0 - def_mitigation)
|
||||
* mult
|
||||
* dmg_spread;
|
||||
let dmg = dmg.round() as u32;
|
||||
|
||||
metrics.record_histogram(player, "dmg", dmg);
|
||||
metrics.record_histogram(weapon, "dmg", dmg);
|
||||
metrics.record_histogram(Some(player), "dmg", dmg);
|
||||
metrics.record_histogram(Some(weapon), "dmg", dmg);
|
||||
|
||||
if dmg > 0 {
|
||||
for effect in damage_proc_q.iter_many(children.get()) {
|
||||
|
|
@ -548,7 +385,7 @@ pub fn use_damaging_weapon(
|
|||
continue;
|
||||
}
|
||||
let chance = (value / 100.0) as f64;
|
||||
if chance > 1.0 || rng.gen_bool(chance) {
|
||||
if chance > 1.0 || rng.random_bool(chance) {
|
||||
match bonus {
|
||||
MultiTurnBonus::Blindfire => {
|
||||
multi_attack_proc = Some(MultiAttack::Blindfire)
|
||||
|
|
@ -559,26 +396,26 @@ pub fn use_damaging_weapon(
|
|||
}
|
||||
MultiTurnBonus::Rage => {
|
||||
multi_attack_proc =
|
||||
Some(MultiAttack::Rage(rng.gen_range(2..=8)))
|
||||
Some(MultiAttack::Rage(rng.random_range(2..=8)))
|
||||
}
|
||||
MultiTurnBonus::DoubleTap => {
|
||||
multi_attack_proc =
|
||||
Some(MultiAttack::DoubleTap { first_shot: true })
|
||||
}
|
||||
};
|
||||
metrics.increment_counter(player, bonus.counter_label(), 1);
|
||||
metrics.increment_counter(weapon, bonus.counter_label(), 1);
|
||||
metrics.increment_counter(Some(player), bonus.counter_label(), 1);
|
||||
metrics.increment_counter(Some(weapon), bonus.counter_label(), 1);
|
||||
}
|
||||
}
|
||||
DamageProcEffect::SelfEffect { value, bonus } => {
|
||||
let chance = (value / 100.0) as f64;
|
||||
if chance > 1.0 || rng.gen_bool(chance) {
|
||||
if chance > 1.0 || rng.random_bool(chance) {
|
||||
bonus.spawn(player, &mut effects);
|
||||
}
|
||||
}
|
||||
DamageProcEffect::OpponentEffect { value, bonus } => {
|
||||
let chance = (value / 100.0) as f64;
|
||||
if chance > 1.0 || rng.gen_bool(chance) {
|
||||
if chance > 1.0 || rng.random_bool(chance) {
|
||||
bonus.spawn(target, &mut effects, &mut rng.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -592,19 +429,20 @@ pub fn use_damaging_weapon(
|
|||
|
||||
log!(logger, "hit_target", {
|
||||
actor: player,
|
||||
acc: acc_eff.value,
|
||||
recipient: target,
|
||||
weapon: weapon,
|
||||
weapon,
|
||||
part: %body_part,
|
||||
part_mult: mult,
|
||||
dmg: dmg,
|
||||
rounds: rounds,
|
||||
rounds,
|
||||
health_before: health_before,
|
||||
health_after: health.value,
|
||||
dmg_spread: dmg_spread,
|
||||
dmg_intrinsic: dmg_intrinsic,
|
||||
armour_mitigation: armour_mitigation,
|
||||
def_mitigation: def_mitigation,
|
||||
weapon_dmg: w_dmg.0,
|
||||
dmg,
|
||||
dmg_spread,
|
||||
dmg_intrinsic,
|
||||
dmg_weapon: w_dmg.0,
|
||||
armour_mitigation,
|
||||
def_mitigation,
|
||||
bonus_dmg: dmg_bonus.value,
|
||||
hit_chance: hit_chance,
|
||||
crit_rate: crit.value,
|
||||
|
|
@ -623,17 +461,23 @@ pub fn use_damaging_weapon(
|
|||
FightEndType::Loss
|
||||
},
|
||||
});
|
||||
metrics.increment_counter(player, "victory", 1);
|
||||
metrics.increment_counter(Some(player), "victory", 1);
|
||||
}
|
||||
}
|
||||
|
||||
// Technically only douple tap and blindfire have this condition, but we can run into with
|
||||
// invalid bonus/weapon combinations without checking this for all bonuses
|
||||
if ammo.as_ref().is_some_and(|(a, _, _)| a.0 == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
match multi_attack_proc {
|
||||
Some(MultiAttack::Blindfire) => {
|
||||
if !ammo.as_ref().map(|(a, _, _)| a.0 != 0).unwrap_or_default() {
|
||||
acc_eff.value -= 5.0 / 50.0;
|
||||
// Prevent infinite loop if used on a melee
|
||||
if ammo.is_none() {
|
||||
break;
|
||||
}
|
||||
|
||||
acc_eff.value -= 5.0 / 50.0;
|
||||
}
|
||||
Some(MultiAttack::Fury { first_hit: true }) => {
|
||||
multi_attack_proc = Some(MultiAttack::Fury { first_hit: false })
|
||||
|
|
@ -658,7 +502,7 @@ pub fn check_stalemate(
|
|||
mut logger: Logger,
|
||||
metrics: Res<Metrics>,
|
||||
) {
|
||||
let (current, current_turns, attacker) = current_q.single();
|
||||
let (current, current_turns, attacker) = current_q.single().unwrap();
|
||||
if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker {
|
||||
commands.entity(current).insert(Defeated);
|
||||
let target = target_q.single();
|
||||
|
|
@ -668,7 +512,7 @@ pub fn check_stalemate(
|
|||
recipient: target,
|
||||
fight_end_type: %FightEndType::Stalemate,
|
||||
});
|
||||
metrics.increment_counter(current, "stalemate", 1);
|
||||
metrics.increment_counter(Some(current), "stalemate", 1);
|
||||
|
||||
if other_attackers_q.is_empty() {
|
||||
*state = FightStatus::Over
|
||||
|
|
@ -705,7 +549,7 @@ fn record_post_fight_stats(
|
|||
metrics: Res<Metrics>,
|
||||
) {
|
||||
for (player, health) in player_q.iter() {
|
||||
metrics.record_histogram(player, "rem_health", health.value as u32);
|
||||
metrics.record_histogram(Some(player), "rem_health", health.value as u32);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,411 +1,17 @@
|
|||
use std::marker::PhantomData;
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use proxisim_models::{
|
||||
bundle::stat::{
|
||||
AdditiveBonus, AdditiveBonuses, AmmoControl, BaselineStat, ClipSize, Clips, CritRate,
|
||||
DamageBonus, Defence, Dexterity, EffectiveStat, Health, MultiplicativeBonus,
|
||||
MultiplicativeBonuses, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker,
|
||||
SimpleStatSnapshot, Speed, StatMarker, StatSnapshot, Strength, WeaponAccuracy,
|
||||
},
|
||||
hierarchy::Parent,
|
||||
};
|
||||
|
||||
use crate::{hierarchy::Parent, player::BodyPart, Stages};
|
||||
|
||||
pub trait SimpleStatMarker: Send + Sync + 'static {
|
||||
type ValueType: Send + Sync + Copy + std::fmt::Display + 'static;
|
||||
type BonusType: Send + Sync + Copy + 'static;
|
||||
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
|
||||
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
|
||||
value
|
||||
}
|
||||
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
|
||||
value
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SimpleStatBaseline<Stat: SimpleStatMarker> {
|
||||
pub value: Stat::ValueType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SimpleStatEffective<Stat: SimpleStatMarker> {
|
||||
pub value: Stat::ValueType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct SimpleStatBonus<Stat: SimpleStatMarker> {
|
||||
pub label: &'static str,
|
||||
pub value: Stat::BonusType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> SimpleStatBonus<Stat> {
|
||||
pub fn new(label: &'static str, value: Stat::BonusType) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct SimpleStatSnapshot<Stat: SimpleStatMarker> {
|
||||
value: Stat::ValueType,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct SimpleStatBundle<Stat: SimpleStatMarker> {
|
||||
baseline: SimpleStatBaseline<Stat>,
|
||||
effective: SimpleStatEffective<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> SimpleStatBundle<Stat> {
|
||||
pub fn new(value: Stat::ValueType) -> Self {
|
||||
Self {
|
||||
baseline: SimpleStatBaseline {
|
||||
value,
|
||||
marker: PhantomData,
|
||||
},
|
||||
effective: SimpleStatEffective {
|
||||
value,
|
||||
marker: PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> Clone for SimpleStatEffective<Stat> {
|
||||
fn clone(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl<Stat: SimpleStatMarker> Copy for SimpleStatEffective<Stat> where Stat::ValueType: Copy {}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CritRate;
|
||||
|
||||
impl SimpleStatMarker for CritRate {
|
||||
type ValueType = u16;
|
||||
type BonusType = u16;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
impl<Stat> std::ops::Add<&SimpleStatEffective<Stat>> for &SimpleStatEffective<Stat>
|
||||
where
|
||||
Stat: SimpleStatMarker,
|
||||
Stat::ValueType: std::ops::Add<Stat::ValueType, Output = Stat::ValueType>,
|
||||
{
|
||||
type Output = SimpleStatEffective<Stat>;
|
||||
|
||||
fn add(self, rhs: &SimpleStatEffective<Stat>) -> Self::Output {
|
||||
SimpleStatEffective {
|
||||
value: self.value + rhs.value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl rand::distributions::Distribution<BodyPart> for SimpleStatEffective<CritRate> {
|
||||
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> BodyPart {
|
||||
if rng.gen_ratio((self.value) as u32, 200) {
|
||||
match rng.gen_range(1..=10) {
|
||||
1 => BodyPart::Heart,
|
||||
2 => BodyPart::Throat,
|
||||
_ => BodyPart::Heart,
|
||||
}
|
||||
} else {
|
||||
match rng.gen_range(1..=20) {
|
||||
1 => BodyPart::LeftHand,
|
||||
2 => BodyPart::RightHand,
|
||||
3 => BodyPart::LeftArm,
|
||||
4 => BodyPart::RightArm,
|
||||
5 => BodyPart::LeftFoot,
|
||||
6 => BodyPart::RightFoot,
|
||||
7 | 8 => BodyPart::RightLeg,
|
||||
9 | 10 => BodyPart::LeftLeg,
|
||||
11..=15 => BodyPart::Chest,
|
||||
16 => BodyPart::Groin,
|
||||
_ => BodyPart::Stomach,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct AmmoControl;
|
||||
|
||||
impl SimpleStatMarker for AmmoControl {
|
||||
type ValueType = f32;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
|
||||
value * 100.0
|
||||
}
|
||||
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
|
||||
value * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct DamageBonus;
|
||||
|
||||
impl SimpleStatMarker for DamageBonus {
|
||||
type ValueType = f32;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
|
||||
value * 100.0
|
||||
}
|
||||
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
|
||||
value * 100.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct WeaponAccuracy;
|
||||
|
||||
impl SimpleStatMarker for WeaponAccuracy {
|
||||
type ValueType = f32;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
fn denormalise_value(value: Self::ValueType) -> Self::ValueType {
|
||||
value * 50.0 + 50.0
|
||||
}
|
||||
fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType {
|
||||
value * 50.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClipSize;
|
||||
|
||||
impl SimpleStatMarker for ClipSize {
|
||||
type ValueType = u16;
|
||||
type BonusType = f32;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as f32) * bonus).round() as u16
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as f32) / bonus).round() as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Clips;
|
||||
|
||||
impl SimpleStatMarker for Clips {
|
||||
type ValueType = u16;
|
||||
type BonusType = i16;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as i16) + bonus) as u16
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
((value as i16) - bonus) as u16
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct Health;
|
||||
|
||||
impl SimpleStatMarker for Health {
|
||||
type ValueType = u16;
|
||||
type BonusType = u16;
|
||||
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value + bonus
|
||||
}
|
||||
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||
value - bonus
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum StatType {
|
||||
Str,
|
||||
Def,
|
||||
Spd,
|
||||
Dex,
|
||||
}
|
||||
|
||||
pub trait StatMarker: Send + Sync + 'static {
|
||||
fn stat_type() -> StatType;
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Strength;
|
||||
impl StatMarker for Strength {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Str
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Defence;
|
||||
impl StatMarker for Defence {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Def
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Speed;
|
||||
impl StatMarker for Speed {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Spd
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Dexterity;
|
||||
impl StatMarker for Dexterity {
|
||||
fn stat_type() -> StatType {
|
||||
StatType::Dex
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct BaselineStat<Stat: StatMarker> {
|
||||
pub value: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> Default for BaselineStat<Stat> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
value: 10.0,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct EffectiveStat<Stat: StatMarker> {
|
||||
pub value: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct AdditiveBonuses<Stat: StatMarker> {
|
||||
pub factor: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> Default for AdditiveBonuses<Stat> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MultiplicativeBonuses<Stat: StatMarker> {
|
||||
pub factor: f32,
|
||||
pub marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> Default for MultiplicativeBonuses<Stat> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Bundle, Default)]
|
||||
pub struct StatBundle<Stat: StatMarker> {
|
||||
baseline: BaselineStat<Stat>,
|
||||
additive: AdditiveBonuses<Stat>,
|
||||
multiplicative: MultiplicativeBonuses<Stat>,
|
||||
effective: EffectiveStat<Stat>,
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct StatSnapshot<Stat: StatMarker> {
|
||||
additive_bonuses: f32,
|
||||
multiplicative_bonuses: f32,
|
||||
effective: f32,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> StatBundle<Stat> {
|
||||
pub fn new(baseline: f32) -> Self {
|
||||
Self {
|
||||
baseline: BaselineStat {
|
||||
value: baseline,
|
||||
marker: PhantomData,
|
||||
},
|
||||
effective: EffectiveStat {
|
||||
value: baseline,
|
||||
marker: PhantomData,
|
||||
},
|
||||
additive: AdditiveBonuses {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
},
|
||||
multiplicative: MultiplicativeBonuses {
|
||||
factor: 1.0,
|
||||
marker: PhantomData,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct AdditiveBonus<Stat: StatMarker> {
|
||||
pub label: &'static str,
|
||||
pub value: f32,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> AdditiveBonus<Stat> {
|
||||
pub fn new(label: &'static str, value: f32) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
pub struct MultiplicativeBonus<Stat: StatMarker> {
|
||||
pub label: &'static str,
|
||||
pub value: f32,
|
||||
marker: PhantomData<Stat>,
|
||||
}
|
||||
|
||||
impl<Stat: StatMarker> MultiplicativeBonus<Stat> {
|
||||
pub fn new(label: &'static str, value: f32) -> Self {
|
||||
Self {
|
||||
label,
|
||||
value,
|
||||
marker: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
use crate::Stages;
|
||||
|
||||
fn add_additive_bonus<Stat: StatMarker>(
|
||||
In(entities): In<Vec<Entity>>,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,20 @@
|
|||
use std::{collections::VecDeque, marker::PhantomData};
|
||||
|
||||
use bevy_ecs::prelude::*;
|
||||
use proxisim_models::{
|
||||
bundle::stat::{
|
||||
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
|
||||
},
|
||||
hierarchy::Parent,
|
||||
};
|
||||
use rand::Rng as _;
|
||||
|
||||
use crate::{
|
||||
Rng, Stages,
|
||||
effect::{Effects, TimeLimitedEffect},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
log,
|
||||
log::Logger,
|
||||
weapon::temp::AssociatedWeapon,
|
||||
Rng, Stages,
|
||||
};
|
||||
|
||||
use super::stats::{
|
||||
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
|
|
@ -101,19 +102,6 @@ impl DebuffingTempMarker for FlashGrenade {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Component, Default)]
|
||||
pub struct Sand;
|
||||
|
||||
impl DebuffingTempMarker for Sand {
|
||||
type Stat = Speed;
|
||||
fn factor() -> f32 {
|
||||
1.0 / 5.0
|
||||
}
|
||||
fn duration() -> std::ops::Range<f32> {
|
||||
15.0..20.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct LinkedComponents<const N: usize>([Entity; N]);
|
||||
|
||||
|
|
@ -136,7 +124,7 @@ where
|
|||
value: f32,
|
||||
label: &'static str,
|
||||
) -> [Entity; 1] {
|
||||
<(T,) as Stats<1>>::spawn_additive_effects(effects, target, value, label)
|
||||
[effects.spawn(AdditiveBonus::<T>::new(label, value), target)]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -155,7 +143,7 @@ macro_rules! impl_n_stats {
|
|||
};
|
||||
}
|
||||
|
||||
impl_n_stats!(1, A);
|
||||
// impl_n_stats!(1, A);
|
||||
impl_n_stats!(2, A, B);
|
||||
impl_n_stats!(3, A, B, C);
|
||||
impl_n_stats!(4, A, B, C, D);
|
||||
|
|
@ -407,10 +395,10 @@ fn remove_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N
|
|||
mut effects: Effects,
|
||||
) {
|
||||
for (effect, player) in effect_q.iter_many(entities) {
|
||||
if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap() {
|
||||
if stack.effects.front() == Some(&effect) {
|
||||
stack.effects.pop_front();
|
||||
}
|
||||
if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap()
|
||||
&& stack.effects.front() == Some(&effect)
|
||||
{
|
||||
stack.effects.pop_front();
|
||||
}
|
||||
|
||||
let linked = linked_q.get(effect).unwrap();
|
||||
|
|
@ -448,7 +436,7 @@ fn apply_temp_debuff_effect<Temp: DebuffingTempMarker>(
|
|||
continue;
|
||||
}
|
||||
|
||||
let duration = rng.gen_range(Temp::duration());
|
||||
let duration = rng.random_range(Temp::duration());
|
||||
commands.entity(effect).insert(TimeLimitedEffect(duration));
|
||||
|
||||
let stack_size = stack.as_ref().map_or(0, |s| s.effects.len()) as i32;
|
||||
|
|
@ -547,7 +535,6 @@ pub(crate) fn configure(stages: &mut Stages) {
|
|||
register_debuff_temp::<PepperSpray>(stages);
|
||||
register_debuff_temp::<ConcussionGrenade>(stages);
|
||||
register_debuff_temp::<FlashGrenade>(stages);
|
||||
register_debuff_temp::<Sand>(stages);
|
||||
|
||||
register_status_effect::<1, Withered>(stages);
|
||||
register_status_effect::<1, Weakened>(stages);
|
||||
|
|
|
|||
|
|
@ -1,102 +1,26 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
|
||||
use crate::{
|
||||
effect::{Effects, TurnLimitedEffect},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
player::{
|
||||
stats::{
|
||||
use proxisim_models::{
|
||||
bundle::{
|
||||
bonus::{BonusPartDamageBonus, BonusValue, WeaponBonusType},
|
||||
player::PartDamageBonus,
|
||||
stat::{
|
||||
AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus,
|
||||
SimpleStatEffective, Speed, Strength, WeaponAccuracy,
|
||||
},
|
||||
status_effect::{
|
||||
AdditiveStatusEffect, Crippled, Demoralise, Frozen, Motivate, Slow, Weakened, Withered,
|
||||
},
|
||||
BodyPart, PartDamageBonus,
|
||||
},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
Stages,
|
||||
effect::{Effects, TurnLimitedEffect},
|
||||
player::status_effect::{
|
||||
AdditiveStatusEffect, Crippled, Demoralise, Frozen, Motivate, Slow, Weakened, Withered,
|
||||
},
|
||||
};
|
||||
|
||||
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)]
|
||||
pub enum TurnTriggeredBonus {
|
||||
Empower,
|
||||
|
|
@ -188,7 +112,7 @@ impl OpponentStatusEffect {
|
|||
Self::Slow => {
|
||||
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),
|
||||
1 => OpponentStatusEffect::Slow.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 {
|
||||
Achilles,
|
||||
Crusher,
|
||||
|
|
@ -270,21 +194,17 @@ impl BonusPartDamageBonus {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} */
|
||||
|
||||
pub(crate) fn prepare_bonuses(
|
||||
bonus_q: Query<(
|
||||
&Parent,
|
||||
&WeaponBonus,
|
||||
&BonusValue,
|
||||
Option<&SimpleStatEffective<Clips>>,
|
||||
)>,
|
||||
bonus_q: Query<(&Parent, &WeaponBonusType, &BonusValue)>,
|
||||
clips_q: Query<&SimpleStatEffective<Clips>>,
|
||||
mut effects: Effects,
|
||||
mut commands: Commands,
|
||||
) {
|
||||
for (weapon, bonus, value, clips) in bonus_q.iter() {
|
||||
for (weapon, bonus, value) in bonus_q.iter() {
|
||||
match bonus {
|
||||
WeaponBonus::Berserk => {
|
||||
WeaponBonusType::Berserk => {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<DamageBonus>::new("beserk", value.0 / 100.0),
|
||||
weapon.get(),
|
||||
|
|
@ -294,19 +214,19 @@ pub(crate) fn prepare_bonuses(
|
|||
weapon.get(),
|
||||
);
|
||||
}
|
||||
WeaponBonus::Conserve => {
|
||||
WeaponBonusType::Conserve => {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<AmmoControl>::new("conserve", value.0 / 100.0),
|
||||
weapon.get(),
|
||||
);
|
||||
}
|
||||
WeaponBonus::Expose => {
|
||||
WeaponBonusType::Expose => {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<CritRate>::new("expose", (value.0 / 0.5) as u16),
|
||||
weapon.get(),
|
||||
);
|
||||
}
|
||||
WeaponBonus::Grace => {
|
||||
WeaponBonusType::Grace => {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<DamageBonus>::new("grace", -value.0 / 2.0 / 100.0),
|
||||
weapon.get(),
|
||||
|
|
@ -316,27 +236,27 @@ pub(crate) fn prepare_bonuses(
|
|||
weapon.get(),
|
||||
);
|
||||
}
|
||||
WeaponBonus::Powerful => {
|
||||
WeaponBonusType::Powerful => {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<DamageBonus>::new("powerful", value.0 / 100.0),
|
||||
weapon.get(),
|
||||
);
|
||||
}
|
||||
WeaponBonus::Specialist => {
|
||||
WeaponBonusType::Specialist => {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<DamageBonus>::new("specialist", value.0 / 100.0),
|
||||
weapon.get(),
|
||||
);
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<Clips>::new(
|
||||
"specialist",
|
||||
-clips.map(|c| c.value as i16).unwrap_or_default(),
|
||||
),
|
||||
weapon.get(),
|
||||
);
|
||||
|
||||
if let Ok(clips) = clips_q.get(weapon.get()) {
|
||||
effects.spawn(
|
||||
SimpleStatBonus::<Clips>::new("specialist", -(clips.value as i16)),
|
||||
weapon.get(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
WeaponBonus::Empower => {
|
||||
WeaponBonusType::Empower => {
|
||||
commands
|
||||
.spawn(TurnTriggeredEffect::Bonus {
|
||||
value: value.0,
|
||||
|
|
@ -344,7 +264,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Quicken => {
|
||||
WeaponBonusType::Quicken => {
|
||||
commands
|
||||
.spawn(TurnTriggeredEffect::Bonus {
|
||||
value: value.0,
|
||||
|
|
@ -353,7 +273,7 @@ pub(crate) fn prepare_bonuses(
|
|||
.set_parent(weapon.get());
|
||||
}
|
||||
|
||||
WeaponBonus::Assassinate => {
|
||||
WeaponBonusType::Assassinate => {
|
||||
commands
|
||||
.spawn(FirstTurnEffect::Bonus {
|
||||
value: value.0,
|
||||
|
|
@ -362,7 +282,7 @@ pub(crate) fn prepare_bonuses(
|
|||
.set_parent(weapon.get());
|
||||
}
|
||||
|
||||
WeaponBonus::Blindfire => {
|
||||
WeaponBonusType::Blindfire => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::MultiTurn {
|
||||
value: value.0,
|
||||
|
|
@ -370,7 +290,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Fury => {
|
||||
WeaponBonusType::Fury => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::MultiTurn {
|
||||
value: value.0,
|
||||
|
|
@ -378,7 +298,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Rage => {
|
||||
WeaponBonusType::Rage => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::MultiTurn {
|
||||
value: value.0,
|
||||
|
|
@ -386,7 +306,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::DoubleTap => {
|
||||
WeaponBonusType::DoubleTap => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::MultiTurn {
|
||||
value: value.0,
|
||||
|
|
@ -395,7 +315,7 @@ pub(crate) fn prepare_bonuses(
|
|||
.set_parent(weapon.get());
|
||||
}
|
||||
|
||||
WeaponBonus::Achilles => {
|
||||
WeaponBonusType::Achilles => {
|
||||
commands
|
||||
.spawn(PartDamageBonus::WeaponBonus {
|
||||
value: value.0 / 100.0,
|
||||
|
|
@ -403,7 +323,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Cupid => {
|
||||
WeaponBonusType::Cupid => {
|
||||
commands
|
||||
.spawn(PartDamageBonus::WeaponBonus {
|
||||
value: value.0 / 100.0,
|
||||
|
|
@ -411,7 +331,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Crusher => {
|
||||
WeaponBonusType::Crusher => {
|
||||
commands
|
||||
.spawn(PartDamageBonus::WeaponBonus {
|
||||
value: value.0 / 100.0,
|
||||
|
|
@ -419,7 +339,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Deadeye => {
|
||||
WeaponBonusType::Deadeye => {
|
||||
commands
|
||||
.spawn(PartDamageBonus::WeaponBonus {
|
||||
value: value.0 / 100.0,
|
||||
|
|
@ -427,7 +347,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Throttle => {
|
||||
WeaponBonusType::Throttle => {
|
||||
commands
|
||||
.spawn(PartDamageBonus::WeaponBonus {
|
||||
value: value.0 / 100.0,
|
||||
|
|
@ -435,7 +355,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Roshambo => {
|
||||
WeaponBonusType::Roshambo => {
|
||||
commands
|
||||
.spawn(PartDamageBonus::WeaponBonus {
|
||||
value: value.0 / 100.0,
|
||||
|
|
@ -444,7 +364,7 @@ pub(crate) fn prepare_bonuses(
|
|||
.set_parent(weapon.get());
|
||||
}
|
||||
|
||||
WeaponBonus::Cripple => {
|
||||
WeaponBonusType::Cripple => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -452,7 +372,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Demoralise => {
|
||||
WeaponBonusType::Demoralise => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -460,7 +380,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Freeze => {
|
||||
WeaponBonusType::Freeze => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -468,7 +388,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Slow => {
|
||||
WeaponBonusType::Slow => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -476,7 +396,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Toxin => {
|
||||
WeaponBonusType::Toxin => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -484,7 +404,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Weaken => {
|
||||
WeaponBonusType::Weaken => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -492,7 +412,7 @@ pub(crate) fn prepare_bonuses(
|
|||
})
|
||||
.set_parent(weapon.get());
|
||||
}
|
||||
WeaponBonus::Wither => {
|
||||
WeaponBonusType::Wither => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::OpponentEffect {
|
||||
value: value.0,
|
||||
|
|
@ -501,7 +421,7 @@ pub(crate) fn prepare_bonuses(
|
|||
.set_parent(weapon.get());
|
||||
}
|
||||
|
||||
WeaponBonus::Motivate => {
|
||||
WeaponBonusType::Motivate => {
|
||||
commands
|
||||
.spawn(DamageProcEffect::SelfEffect {
|
||||
value: value.0,
|
||||
|
|
|
|||
|
|
@ -1,19 +1,25 @@
|
|||
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::{
|
||||
effect::{Effects, TurnLimitedEffect},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
log,
|
||||
log::Logger,
|
||||
passives::{Education, EducationPartDamageBonus, FactionUpgrades, Merits},
|
||||
player::{
|
||||
stats::{
|
||||
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
|
||||
SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, WeaponAccuracy,
|
||||
},
|
||||
Current, PartDamageBonus, Weapons,
|
||||
},
|
||||
Id, Name, Stages,
|
||||
Stages,
|
||||
};
|
||||
|
||||
use self::bonus::{
|
||||
|
|
@ -23,102 +29,6 @@ use self::bonus::{
|
|||
pub mod bonus;
|
||||
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)]
|
||||
pub enum TurnTriggeredMod {
|
||||
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) {
|
||||
for (player, weapons) in weapons_q.iter() {
|
||||
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 {
|
||||
commands.entity(temp).set_parent(player);
|
||||
}
|
||||
commands.entity(weapons.fists.unwrap()).set_parent(player);
|
||||
if let Some(kick) = weapons.kick {
|
||||
commands.entity(kick).set_parent(player);
|
||||
}
|
||||
commands.entity(weapons.fists).set_parent(player);
|
||||
commands.entity(weapons.kick).set_parent(player);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,202 +1,33 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
use strum::Display;
|
||||
|
||||
use crate::{
|
||||
effect::Effects,
|
||||
player::{
|
||||
status_effect::{
|
||||
ConcussionGrenade, FlashGrenade, PepperSpray, Sand, SmokeGrenade, TearGas,
|
||||
TempDebuffEffect,
|
||||
},
|
||||
Current, CurrentTarget,
|
||||
},
|
||||
Stages,
|
||||
use proxisim_models::bundle::{
|
||||
player::{Current, CurrentTarget, Player},
|
||||
weapon::{BuffingTemp, DebuffingTemp, Usable, Uses},
|
||||
};
|
||||
|
||||
use super::{DamagingWeaponBundle, Usable, WeaponBundle, WeaponCategory, WeaponSlot, WeaponVerb};
|
||||
|
||||
#[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)
|
||||
}
|
||||
}
|
||||
use crate::{
|
||||
Stages,
|
||||
effect::Effects,
|
||||
log,
|
||||
log::Logger,
|
||||
player::status_effect::{
|
||||
AdditiveStatusEffect, ConcussionGrenade, FlashGrenade, Hardened, Hastened, PepperSpray,
|
||||
Sharpened, SmokeGrenade, Strengthened, TearGas, TempDebuffEffect,
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Component)]
|
||||
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(
|
||||
mut temp_q: Query<(Entity, &DebuffingTemp, &mut Uses), With<Current>>,
|
||||
target_q: Query<Entity, With<CurrentTarget>>,
|
||||
mut effects: Effects,
|
||||
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;
|
||||
};
|
||||
let target = target_q.single();
|
||||
let target = target_q.single().unwrap();
|
||||
|
||||
match temp {
|
||||
DebuffingTemp::TearGas => effects.spawn_and_insert(
|
||||
|
|
@ -224,13 +55,54 @@ fn use_debuffing_temp(
|
|||
target,
|
||||
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),
|
||||
),
|
||||
};
|
||||
|
||||
log!(logger, "used_buff_temp", {
|
||||
actor: current,
|
||||
weapon: weapon,
|
||||
});
|
||||
|
||||
uses.0 -= 1;
|
||||
if uses.0 == 0 {
|
||||
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) {
|
||||
stages.turn.add_systems(use_debuffing_temp);
|
||||
stages.turn.add_systems(use_buffing_temp);
|
||||
stages.restore.add_systems(restore_uses);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue