initial commit
This commit is contained in:
commit
86f9333aec
21 changed files with 6449 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
.DS_Store
|
||||||
|
Cargo.lock
|
||||||
23
Cargo.toml
Normal file
23
Cargo.toml
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[workspace]
|
||||||
|
resolver = "2"
|
||||||
|
members = [".", "macros"]
|
||||||
|
|
||||||
|
[package]
|
||||||
|
name = "proxisim"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["json"]
|
||||||
|
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 }
|
||||||
|
serde_json = { version = "1", optional = true }
|
||||||
|
|
||||||
|
[dependencies.macros]
|
||||||
|
path = "macros"
|
||||||
15
macros/Cargo.toml
Normal file
15
macros/Cargo.toml
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
[package]
|
||||||
|
name = "macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
syn = { version = "2", features = ["parsing"] }
|
||||||
|
proc-macro2 = "1"
|
||||||
|
quote = "1"
|
||||||
|
|
||||||
10
macros/src/lib.rs
Normal file
10
macros/src/lib.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
mod log_message;
|
||||||
|
|
||||||
|
#[proc_macro_derive(LogMessage, attributes(log))]
|
||||||
|
pub fn derive_log_message(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
|
||||||
|
let ast = syn::parse(input).unwrap();
|
||||||
|
|
||||||
|
log_message::impl_log_message(&ast)
|
||||||
|
.unwrap_or_else(syn::Error::into_compile_error)
|
||||||
|
.into()
|
||||||
|
}
|
||||||
114
macros/src/log_message.rs
Normal file
114
macros/src/log_message.rs
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
use proc_macro2::TokenStream;
|
||||||
|
use quote::quote;
|
||||||
|
use syn::{spanned::Spanned, Data, DeriveInput, Fields};
|
||||||
|
|
||||||
|
fn camel_to_snake_case(input: String) -> String {
|
||||||
|
let mut prev_lowercase = false;
|
||||||
|
let mut output = String::with_capacity(input.len());
|
||||||
|
|
||||||
|
for char in input.chars() {
|
||||||
|
if char.is_lowercase() {
|
||||||
|
prev_lowercase = true;
|
||||||
|
output.push(char);
|
||||||
|
} else {
|
||||||
|
if prev_lowercase {
|
||||||
|
output.push('_');
|
||||||
|
}
|
||||||
|
prev_lowercase = false;
|
||||||
|
output.push(char.to_lowercase().next().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
output
|
||||||
|
}
|
||||||
|
|
||||||
|
enum LogOption {
|
||||||
|
Default,
|
||||||
|
Display,
|
||||||
|
Debug,
|
||||||
|
Player,
|
||||||
|
Weapon,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn impl_log_message(ast: &DeriveInput) -> syn::Result<TokenStream> {
|
||||||
|
let Data::Struct(r#struct) = &ast.data else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
ast.ident.span(),
|
||||||
|
"LogMessage can only be derived for structs",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let Fields::Named(named_fields) = &r#struct.fields else {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
ast.span(),
|
||||||
|
"LogMessage cannot be derived for tuple structs",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut fields = Vec::with_capacity(named_fields.named.len());
|
||||||
|
for field in &named_fields.named {
|
||||||
|
let mut option = None;
|
||||||
|
for attr in &field.attrs {
|
||||||
|
if attr.path().is_ident("log") {
|
||||||
|
attr.parse_nested_meta(|meta| {
|
||||||
|
if meta.path.is_ident("display") {
|
||||||
|
option = Some(LogOption::Display);
|
||||||
|
Ok(())
|
||||||
|
} else if meta.path.is_ident("debug") {
|
||||||
|
option = Some(LogOption::Debug);
|
||||||
|
Ok(())
|
||||||
|
} else if meta.path.is_ident("player") {
|
||||||
|
option = Some(LogOption::Player);
|
||||||
|
Ok(())
|
||||||
|
} else if meta.path.is_ident("weapon") {
|
||||||
|
option = Some(LogOption::Weapon);
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
Err(meta.error("Unrecognised attribute"))
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let option = option.unwrap_or(LogOption::Default);
|
||||||
|
|
||||||
|
let field_name = field.ident.as_ref().unwrap();
|
||||||
|
let name = field_name.to_string();
|
||||||
|
|
||||||
|
let getter = match option {
|
||||||
|
LogOption::Default => quote! {
|
||||||
|
(#name, crate::log::LogValue::from(self.#field_name.clone()))
|
||||||
|
},
|
||||||
|
LogOption::Display => quote! {
|
||||||
|
(#name, crate::log::LogValue::Display(&self.#field_name))
|
||||||
|
},
|
||||||
|
LogOption::Debug => quote! {
|
||||||
|
(#name, crate::log::LogValue::Debug(&self.#field_name))
|
||||||
|
},
|
||||||
|
LogOption::Player => quote! {
|
||||||
|
(#name, crate::log::LogValue::Player(self.#field_name))
|
||||||
|
},
|
||||||
|
LogOption::Weapon => quote! {
|
||||||
|
(#name, crate::log::LogValue::Weapon(self.#field_name))
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fields.push(getter);
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = &ast.ident;
|
||||||
|
|
||||||
|
let tag = camel_to_snake_case(name.to_string());
|
||||||
|
|
||||||
|
let gen = quote! {
|
||||||
|
impl crate::log::LogMessage for #name {
|
||||||
|
fn tag(&self) -> &'static str { #tag }
|
||||||
|
|
||||||
|
fn entries<'a>(&'a self) -> Vec<(&'static str, crate::log::LogValue<'a>)> {
|
||||||
|
vec![#(#fields,)*]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(gen)
|
||||||
|
}
|
||||||
2
rust-toolchain.toml
Normal file
2
rust-toolchain.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
[toolchain]
|
||||||
|
channel = "nightly"
|
||||||
284
src/armour/mod.rs
Normal file
284
src/armour/mod.rs
Normal file
|
|
@ -0,0 +1,284 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use rand::distributions::Distribution;
|
||||||
|
use strum::IntoEnumIterator;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
hierarchy::HierarchyBuilder,
|
||||||
|
player::{
|
||||||
|
status_effect::{
|
||||||
|
ConcussionGrenade, FlashGrenade, PepperSpray, Sand, TearGas, TempDebuffImmunity,
|
||||||
|
},
|
||||||
|
BodyPart,
|
||||||
|
},
|
||||||
|
Name, 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)>,
|
||||||
|
armour_q: Query<(Entity, &ArmourCoverage, &ArmourValue, Option<&Immunities>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (player, equipped_armour) in equip_q.iter() {
|
||||||
|
let mut parts = ArmourVec::<ArmourBodyPart>::default();
|
||||||
|
|
||||||
|
for (armour, coverage, armour_value, immunities) in armour_q.iter_many(equipped_armour) {
|
||||||
|
commands.entity(armour).set_parent(player);
|
||||||
|
|
||||||
|
if let Some(immunities) = immunities {
|
||||||
|
let mut player = commands.entity(player);
|
||||||
|
for immunity in &immunities.0 {
|
||||||
|
match immunity {
|
||||||
|
// TODO: Going to need this if irradiate is ever added
|
||||||
|
Immunity::Radiation => (),
|
||||||
|
// NOTE: It's an unreleased DOT temp, so the exact effect is currently
|
||||||
|
// unknwown
|
||||||
|
Immunity::NerveGas => (),
|
||||||
|
Immunity::TearGas => {
|
||||||
|
player.insert(TempDebuffImmunity::<TearGas>::default());
|
||||||
|
}
|
||||||
|
Immunity::PepperSpray => {
|
||||||
|
player.insert(TempDebuffImmunity::<PepperSpray>::default());
|
||||||
|
}
|
||||||
|
Immunity::FlashGrenades => {
|
||||||
|
player.insert(TempDebuffImmunity::<FlashGrenade>::default());
|
||||||
|
}
|
||||||
|
Immunity::Sand => {
|
||||||
|
player.insert(TempDebuffImmunity::<Sand>::default());
|
||||||
|
}
|
||||||
|
Immunity::ConcussionGrenades => {
|
||||||
|
player.insert(TempDebuffImmunity::<ConcussionGrenade>::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for slot in ArmourBodyPartSlot::iter() {
|
||||||
|
if coverage.0[slot] > 0.0 {
|
||||||
|
parts[slot].armour_pieces.push(BodyPartCoverage {
|
||||||
|
armour,
|
||||||
|
coverage: coverage.0[slot] / 100.0,
|
||||||
|
armour_value: armour_value.0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts = parts.0.map(|p| commands.spawn(p).id());
|
||||||
|
|
||||||
|
commands
|
||||||
|
.entity(player)
|
||||||
|
.add_children(parts)
|
||||||
|
.insert(ArmourBodyParts(ArmourVec(parts)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.equip.add_systems(generate_body_parts);
|
||||||
|
}
|
||||||
361
src/dto.rs
Normal file
361
src/dto.rs
Normal file
|
|
@ -0,0 +1,361 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
|
use crate::entity_registry::EntityInfo;
|
||||||
|
pub use crate::passives::{DrugCooldown, Education, FactionUpgrades, Merits};
|
||||||
|
pub use crate::player::PlayerStrategy;
|
||||||
|
use crate::weapon::Japanese;
|
||||||
|
pub use crate::weapon::{bonus::WeaponBonus, temp::Temp, WeaponCategory, WeaponMod, WeaponSlot};
|
||||||
|
use crate::{
|
||||||
|
armour::EquippedArmour,
|
||||||
|
passives::PassiveBundle,
|
||||||
|
player::PlayerBundle,
|
||||||
|
weapon::{AmmoWeaponBundle, DamagingWeaponBundle, WeaponBundle, WeaponVerb},
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
armour::{ArmourBundle, Immunities, Immunity, Set},
|
||||||
|
weapon::bonus::WeaponBonusBundle,
|
||||||
|
};
|
||||||
|
use crate::{
|
||||||
|
hierarchy::HierarchyBuilder,
|
||||||
|
player::stats::{Defence, Dexterity, Speed, StatBundle, Strength},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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 WeaponAmmo {
|
||||||
|
pub clips: u16,
|
||||||
|
pub clip_size: u16,
|
||||||
|
pub rate_of_fire: [u16; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
pub struct WeaponBonusInfo {
|
||||||
|
pub bonus: WeaponBonus,
|
||||||
|
pub value: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
pub struct Weapon {
|
||||||
|
pub name: String,
|
||||||
|
pub cat: WeaponCategory,
|
||||||
|
pub dmg: f32,
|
||||||
|
pub acc: f32,
|
||||||
|
pub ammo: WeaponAmmo,
|
||||||
|
pub mods: Vec<WeaponMod>,
|
||||||
|
pub bonuses: Vec<WeaponBonusInfo>,
|
||||||
|
pub experience: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
pub struct MeleeWeapon {
|
||||||
|
pub name: String,
|
||||||
|
pub cat: WeaponCategory,
|
||||||
|
pub japanese: bool,
|
||||||
|
pub dmg: f32,
|
||||||
|
pub acc: f32,
|
||||||
|
pub bonuses: Vec<WeaponBonusInfo>,
|
||||||
|
pub experience: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Weapons {
|
||||||
|
pub primary: Option<Weapon>,
|
||||||
|
pub secondary: Option<Weapon>,
|
||||||
|
pub melee: Option<MeleeWeapon>,
|
||||||
|
pub temp: Option<Temp>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Armour {
|
||||||
|
pub armour_value: f32,
|
||||||
|
pub name: String,
|
||||||
|
pub coverage: [f32; 10],
|
||||||
|
pub immunities: Vec<Immunity>,
|
||||||
|
pub set: Option<Set>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ArmourPieces {
|
||||||
|
pub helmet: Option<Armour>,
|
||||||
|
pub body: Option<Armour>,
|
||||||
|
pub pants: Option<Armour>,
|
||||||
|
pub gloves: Option<Armour>,
|
||||||
|
pub boots: Option<Armour>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
pub struct Player {
|
||||||
|
pub name: String,
|
||||||
|
pub id: usize,
|
||||||
|
pub level: u16,
|
||||||
|
pub stats: Stats,
|
||||||
|
pub merits: Option<Merits>,
|
||||||
|
pub education: Option<Education>,
|
||||||
|
pub weapons: Weapons,
|
||||||
|
pub armour: ArmourPieces,
|
||||||
|
pub faction: Option<FactionUpgrades>,
|
||||||
|
pub drug: Option<DrugCooldown>,
|
||||||
|
pub strategy: PlayerStrategy,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Player {
|
||||||
|
pub(crate) fn spawn(self, world: &mut World) -> EntityWorldMut<'_> {
|
||||||
|
let primary = self.weapons.primary.map(|w| {
|
||||||
|
let primary = world
|
||||||
|
.spawn((
|
||||||
|
WeaponBundle::new(
|
||||||
|
w.name,
|
||||||
|
self.id * 100 + 3,
|
||||||
|
WeaponVerb::Fired,
|
||||||
|
WeaponSlot::Primary,
|
||||||
|
),
|
||||||
|
DamagingWeaponBundle::new(w.dmg, w.acc, w.mods, w.experience, w.cat),
|
||||||
|
AmmoWeaponBundle::new(w.ammo.clips, w.ammo.clip_size, w.ammo.rate_of_fire),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
if !w.bonuses.is_empty() {
|
||||||
|
for bonus in w.bonuses {
|
||||||
|
world
|
||||||
|
.spawn(WeaponBonusBundle::new(bonus.bonus, bonus.value))
|
||||||
|
.set_parent(primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
primary
|
||||||
|
});
|
||||||
|
|
||||||
|
let secondary = self.weapons.secondary.map(|w| {
|
||||||
|
let secondary = world
|
||||||
|
.spawn((
|
||||||
|
WeaponBundle::new(
|
||||||
|
w.name,
|
||||||
|
self.id * 100 + 4,
|
||||||
|
WeaponVerb::Fired,
|
||||||
|
WeaponSlot::Secondary,
|
||||||
|
),
|
||||||
|
DamagingWeaponBundle::new(w.dmg, w.acc, w.mods, w.experience, w.cat),
|
||||||
|
AmmoWeaponBundle::new(w.ammo.clips, w.ammo.clip_size, w.ammo.rate_of_fire),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
if !w.bonuses.is_empty() {
|
||||||
|
for bonus in w.bonuses {
|
||||||
|
world
|
||||||
|
.spawn(WeaponBonusBundle::new(bonus.bonus, bonus.value))
|
||||||
|
.set_parent(secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
secondary
|
||||||
|
});
|
||||||
|
|
||||||
|
let melee = self.weapons.melee.map(|w| {
|
||||||
|
let mut melee = world.spawn((
|
||||||
|
WeaponBundle::new(
|
||||||
|
w.name,
|
||||||
|
self.id * 100 + 5,
|
||||||
|
WeaponVerb::Hit,
|
||||||
|
WeaponSlot::Melee,
|
||||||
|
),
|
||||||
|
DamagingWeaponBundle::new(w.dmg, w.acc, Vec::default(), w.experience, w.cat),
|
||||||
|
));
|
||||||
|
if w.japanese {
|
||||||
|
melee.insert(Japanese);
|
||||||
|
}
|
||||||
|
let melee = melee.id();
|
||||||
|
|
||||||
|
if !w.bonuses.is_empty() {
|
||||||
|
for bonus in w.bonuses {
|
||||||
|
world
|
||||||
|
.spawn(WeaponBonusBundle::new(bonus.bonus, bonus.value))
|
||||||
|
.set_parent(melee);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
melee
|
||||||
|
});
|
||||||
|
|
||||||
|
let temporary = self
|
||||||
|
.weapons
|
||||||
|
.temp
|
||||||
|
.map(|t| t.spawn(world, self.id * 100 + 2).id());
|
||||||
|
|
||||||
|
let fists = world
|
||||||
|
.spawn((
|
||||||
|
WeaponBundle::fists(self.id * 100),
|
||||||
|
DamagingWeaponBundle::fists(),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let kick = world
|
||||||
|
.spawn((
|
||||||
|
WeaponBundle::kick(self.id * 100 + 1),
|
||||||
|
DamagingWeaponBundle::kick(),
|
||||||
|
))
|
||||||
|
.id();
|
||||||
|
|
||||||
|
let weapons = crate::player::Weapons {
|
||||||
|
primary,
|
||||||
|
secondary,
|
||||||
|
melee,
|
||||||
|
temporary,
|
||||||
|
fists: Some(fists),
|
||||||
|
kick: Some(kick),
|
||||||
|
};
|
||||||
|
|
||||||
|
let helmet = self.armour.helmet.map(|h| {
|
||||||
|
let mut helmet = world.spawn(ArmourBundle::new(h.name, h.coverage, h.armour_value));
|
||||||
|
if let Some(set) = h.set {
|
||||||
|
helmet.insert(set);
|
||||||
|
}
|
||||||
|
if !h.immunities.is_empty() {
|
||||||
|
helmet.insert(Immunities(h.immunities));
|
||||||
|
}
|
||||||
|
helmet.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let body = self.armour.body.map(|b| {
|
||||||
|
let mut body = world.spawn(ArmourBundle::new(b.name, b.coverage, b.armour_value));
|
||||||
|
if let Some(set) = b.set {
|
||||||
|
body.insert(set);
|
||||||
|
}
|
||||||
|
body.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let pants = self.armour.pants.map(|p| {
|
||||||
|
let mut pants = world.spawn(ArmourBundle::new(p.name, p.coverage, p.armour_value));
|
||||||
|
if let Some(set) = p.set {
|
||||||
|
pants.insert(set);
|
||||||
|
}
|
||||||
|
pants.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let gloves = self.armour.gloves.map(|g| {
|
||||||
|
let mut gloves = world.spawn(ArmourBundle::new(g.name, g.coverage, g.armour_value));
|
||||||
|
if let Some(set) = g.set {
|
||||||
|
gloves.insert(set);
|
||||||
|
}
|
||||||
|
gloves.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let boots = self.armour.boots.map(|b| {
|
||||||
|
let mut boots = world.spawn(ArmourBundle::new(b.name, b.coverage, b.armour_value));
|
||||||
|
if let Some(set) = b.set {
|
||||||
|
boots.insert(set);
|
||||||
|
}
|
||||||
|
boots.id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let armour = EquippedArmour {
|
||||||
|
head: helmet,
|
||||||
|
body,
|
||||||
|
legs: pants,
|
||||||
|
hands: gloves,
|
||||||
|
feet: boots,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut player = world.spawn((
|
||||||
|
PlayerBundle::new(self.name, self.id, 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),
|
||||||
|
weapons,
|
||||||
|
armour,
|
||||||
|
PassiveBundle {
|
||||||
|
merits: self.merits.unwrap_or_default(),
|
||||||
|
education: self.education.unwrap_or_default(),
|
||||||
|
faction: self.faction.unwrap_or_default(),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(drug) = self.drug {
|
||||||
|
player.insert(drug);
|
||||||
|
}
|
||||||
|
|
||||||
|
player
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_player() {
|
||||||
|
let mut world = World::new();
|
||||||
|
|
||||||
|
let player = Player {
|
||||||
|
name: "Test".to_owned(),
|
||||||
|
id: 0,
|
||||||
|
level: 10,
|
||||||
|
stats: Stats {
|
||||||
|
str: 10.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::default(),
|
||||||
|
experience: 100.0,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
armour: ArmourPieces {
|
||||||
|
helmet: Some(Armour {
|
||||||
|
name: "Test".to_owned(),
|
||||||
|
armour_value: 50.0,
|
||||||
|
coverage: [0.0, 0.0, 0.0, 0.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0],
|
||||||
|
immunities: Vec::default(),
|
||||||
|
set: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
player.spawn(&mut world);
|
||||||
|
}
|
||||||
|
}
|
||||||
299
src/effect.rs
Normal file
299
src/effect.rs
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
use std::{any::TypeId, collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
|
use bevy_ecs::{prelude::*, system::SystemParam};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
hierarchy::{HierarchyBuilder, Parent},
|
||||||
|
player::{Current, Defender, Player},
|
||||||
|
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 = ()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove need for unsafe code by splitting the system info from this struct
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct EffectRegistry {
|
||||||
|
system_info: HashMap<EffectId, EffectInfo>,
|
||||||
|
type_map: HashMap<TypeId, EffectId>,
|
||||||
|
id_counter: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
struct ScheduledEffects {
|
||||||
|
create: Mutex<HashMap<EffectId, Vec<Entity>>>,
|
||||||
|
teardown: Mutex<HashMap<EffectId, Vec<Entity>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct TurnLimitedEffect {
|
||||||
|
pub player: Entity,
|
||||||
|
pub turns: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurnLimitedEffect {
|
||||||
|
pub fn new(player: Entity, turns: u16) -> Self {
|
||||||
|
Self { player, turns }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct TimeLimitedEffect(pub f32);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct Timestamp(f32);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct Permanent;
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct Clock(pub f32);
|
||||||
|
|
||||||
|
/// Marker for effects that last until the beginning of the next round
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct FullRoundEffect;
|
||||||
|
|
||||||
|
#[derive(SystemParam)]
|
||||||
|
pub struct Effects<'w, 's> {
|
||||||
|
registry: Res<'w, EffectRegistry>,
|
||||||
|
scheduled: Res<'w, ScheduledEffects>,
|
||||||
|
effect_q: Query<'w, 's, &'static EffectId>,
|
||||||
|
commands: Commands<'w, 's>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Schedule effects using commands in order to avoid the need for having a separate
|
||||||
|
// `Schedule` dedicated to them.
|
||||||
|
impl<'w, 's> Effects<'w, 's> {
|
||||||
|
pub fn spawn_and_insert<T: Component + 'static>(
|
||||||
|
&mut self,
|
||||||
|
effect: T,
|
||||||
|
to: Entity,
|
||||||
|
addins: impl Bundle,
|
||||||
|
) -> Entity {
|
||||||
|
let id = self.registry.type_map.get(&TypeId::of::<T>()).unwrap();
|
||||||
|
|
||||||
|
let spawned_effect = self
|
||||||
|
.commands
|
||||||
|
.spawn(effect)
|
||||||
|
.insert(*id)
|
||||||
|
.insert(addins)
|
||||||
|
.set_parent(to)
|
||||||
|
.id();
|
||||||
|
|
||||||
|
self.scheduled
|
||||||
|
.create
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.entry(*id)
|
||||||
|
.or_default()
|
||||||
|
.push(spawned_effect);
|
||||||
|
|
||||||
|
spawned_effect
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn spawn<T: Component + 'static>(&mut self, effect: T, to: Entity) -> Entity {
|
||||||
|
self.spawn_and_insert(effect, to, ())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn remove(&mut self, entity: Entity) {
|
||||||
|
let id = self.effect_q.get(entity).unwrap();
|
||||||
|
self.scheduled
|
||||||
|
.teardown
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.entry(*id)
|
||||||
|
.or_default()
|
||||||
|
.push(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = ()>>>,
|
||||||
|
id: EffectId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'r> EffectBuilder<'r> {
|
||||||
|
#[must_use]
|
||||||
|
pub fn apply<A, S>(&mut self, system: S) -> &mut Self
|
||||||
|
where
|
||||||
|
S: IntoSystem<Vec<Entity>, (), A> + 'static,
|
||||||
|
{
|
||||||
|
let mut system = IntoSystem::into_system(system);
|
||||||
|
system.initialize(self.world);
|
||||||
|
self.apply = Some(Box::new(system));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn teardown<T, S>(&mut self, system: S) -> &mut Self
|
||||||
|
where
|
||||||
|
S: IntoSystem<Vec<Entity>, (), T> + 'static,
|
||||||
|
{
|
||||||
|
let mut system = IntoSystem::into_system(system);
|
||||||
|
system.initialize(self.world);
|
||||||
|
self.teardown = Some(Box::new(system));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn build(&mut self) {
|
||||||
|
let mut registry = self.world.resource_mut::<EffectRegistry>();
|
||||||
|
registry.system_info.insert(
|
||||||
|
self.id,
|
||||||
|
EffectInfo {
|
||||||
|
apply: self.apply.take().unwrap(),
|
||||||
|
teardown: self.teardown.take().unwrap(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn run_effects(world: &mut World) {
|
||||||
|
loop {
|
||||||
|
let (next_effect, entities) = {
|
||||||
|
let scheduled = world.resource_mut::<ScheduledEffects>();
|
||||||
|
let mut create = scheduled.create.lock().unwrap();
|
||||||
|
let Some(next_effect) = create.keys().next().copied() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let entities = create.remove(&next_effect).unwrap();
|
||||||
|
|
||||||
|
(next_effect, entities)
|
||||||
|
};
|
||||||
|
|
||||||
|
// safety: since the registry can't be mutably accessed by effect handlers this should be
|
||||||
|
// fine
|
||||||
|
// ...probably
|
||||||
|
unsafe {
|
||||||
|
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.apply_deferred(unsafe_world.world_mut());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let (next_effect, entities) = {
|
||||||
|
let scheduled = world.resource_mut::<ScheduledEffects>();
|
||||||
|
let mut teardown = scheduled.teardown.lock().unwrap();
|
||||||
|
let Some(next_effect) = teardown.keys().next().copied() else {
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
let entities = teardown.remove(&next_effect).unwrap();
|
||||||
|
|
||||||
|
(next_effect, entities)
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe {
|
||||||
|
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.teardown
|
||||||
|
.run(entities.clone(), unsafe_world.world_mut());
|
||||||
|
info.teardown.apply_deferred(unsafe_world.world_mut());
|
||||||
|
};
|
||||||
|
|
||||||
|
for entity in entities {
|
||||||
|
world.despawn(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
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;
|
||||||
|
registry.type_map.insert(TypeId::of::<Effect>(), id);
|
||||||
|
|
||||||
|
EffectBuilder {
|
||||||
|
world: &mut stages.world,
|
||||||
|
apply: None,
|
||||||
|
teardown: None,
|
||||||
|
id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_round_limited_effects(
|
||||||
|
mut effect_q: Query<(Entity, &mut TurnLimitedEffect)>,
|
||||||
|
current_q: Query<Has<Current>>,
|
||||||
|
mut effects: Effects,
|
||||||
|
) {
|
||||||
|
for (entity, mut effect) in effect_q.iter_mut() {
|
||||||
|
if !current_q.get(effect.player).unwrap() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if effect.turns == 0 {
|
||||||
|
effects.remove(entity);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
effect.turns -= 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_clock(
|
||||||
|
mut clock: ResMut<Clock>,
|
||||||
|
current_q: Query<Has<Defender>, (With<Current>, With<Player>)>,
|
||||||
|
) {
|
||||||
|
let is_defender = current_q.single();
|
||||||
|
if !is_defender {
|
||||||
|
clock.0 += SECONDS_PER_ACTION;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_time_limited_effects(
|
||||||
|
new_effects_q: Query<Entity, Added<TimeLimitedEffect>>,
|
||||||
|
active_effects_q: Query<(Entity, &Timestamp, &TimeLimitedEffect)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut effects: Effects,
|
||||||
|
clock: Res<Clock>,
|
||||||
|
) {
|
||||||
|
for entity in new_effects_q.iter() {
|
||||||
|
commands.entity(entity).insert(Timestamp(clock.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (entity, timestamp, effect) in active_effects_q.iter() {
|
||||||
|
if (timestamp.0 + effect.0) < clock.0 {
|
||||||
|
effects.remove(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn mark_permanent_effects(effect_q: Query<Entity, With<EffectId>>, mut commands: Commands) {
|
||||||
|
for effect in effect_q.iter() {
|
||||||
|
commands.entity(effect).insert(Permanent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_transient_effects(
|
||||||
|
effect_q: Query<(Entity, &Parent), (With<EffectId>, Without<Permanent>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (effect, target) in effect_q.iter() {
|
||||||
|
commands.entity(effect).despawn();
|
||||||
|
commands.entity(target.get()).remove_child(effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.world.init_resource::<EffectRegistry>();
|
||||||
|
stages.world.init_resource::<ScheduledEffects>();
|
||||||
|
stages.world.init_resource::<Clock>();
|
||||||
|
|
||||||
|
stages.snapshot.add_systems(mark_permanent_effects);
|
||||||
|
stages
|
||||||
|
.pre_turn
|
||||||
|
.add_systems((advance_clock, update_round_limited_effects));
|
||||||
|
stages.post_turn.add_systems(update_time_limited_effects);
|
||||||
|
stages.restore.add_systems(remove_transient_effects);
|
||||||
|
}
|
||||||
63
src/entity_registry.rs
Normal file
63
src/entity_registry.rs
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct EntityRegistry(pub HashMap<Entity, EntityInfo>);
|
||||||
|
|
||||||
|
fn read_entities(
|
||||||
|
player_q: Query<(Entity, &Name, &Id, Has<Attacker>), With<Player>>,
|
||||||
|
weapon_q: Query<(Entity, &Parent, &Name, &Id), With<Weapon>>,
|
||||||
|
mut registry: ResMut<EntityRegistry>,
|
||||||
|
) {
|
||||||
|
for (player, name, id, is_attacker) in player_q.iter() {
|
||||||
|
registry.0.insert(
|
||||||
|
player,
|
||||||
|
EntityInfo::Player {
|
||||||
|
name: name.0.clone(),
|
||||||
|
id: id.0,
|
||||||
|
is_attacker,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (weapon, player, name, id) in weapon_q.iter() {
|
||||||
|
let (_, _, player_id, _) = player_q.get(player.get()).unwrap();
|
||||||
|
registry.0.insert(
|
||||||
|
weapon,
|
||||||
|
EntityInfo::Weapon {
|
||||||
|
name: name.0.clone(),
|
||||||
|
owner: player_id.0,
|
||||||
|
id: id.0,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.world.init_resource::<EntityRegistry>();
|
||||||
|
stages.snapshot.add_systems(read_entities);
|
||||||
|
}
|
||||||
178
src/hierarchy.rs
Normal file
178
src/hierarchy.rs
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
use bevy_ecs::{
|
||||||
|
prelude::*,
|
||||||
|
system::{Command, EntityCommands},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Component, Clone, Copy)]
|
||||||
|
pub struct Parent(Entity);
|
||||||
|
|
||||||
|
impl Parent {
|
||||||
|
pub fn get(&self) -> Entity {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, 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<'w, 's, 'a> HierarchyBuilder for EntityCommands<'w, 's, 'a> {
|
||||||
|
fn add_child(&mut self, child: Entity) -> &mut Self {
|
||||||
|
let parent = self.id();
|
||||||
|
self.commands().add(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().add(AddChildren {
|
||||||
|
parent,
|
||||||
|
children: children.to_owned(),
|
||||||
|
});
|
||||||
|
self.commands().insert_or_spawn_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().add(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().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
|
||||||
|
}
|
||||||
|
}
|
||||||
444
src/lib.rs
Normal file
444
src/lib.rs
Normal file
|
|
@ -0,0 +1,444 @@
|
||||||
|
#![warn(clippy::perf, clippy::style, clippy::all)]
|
||||||
|
#![allow(clippy::type_complexity)]
|
||||||
|
use bevy_ecs::{prelude::*, schedule::ScheduleLabel};
|
||||||
|
use effect::{register_effect, EffectBuilder};
|
||||||
|
use metrics::Metrics;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
log::{Log, Logging},
|
||||||
|
player::{Attacker, Current, Defender},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod armour;
|
||||||
|
pub mod dto;
|
||||||
|
mod effect;
|
||||||
|
mod entity_registry;
|
||||||
|
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)]
|
||||||
|
struct Rng(pub rand::rngs::SmallRng);
|
||||||
|
|
||||||
|
impl std::ops::Deref for Rng {
|
||||||
|
type Target = rand::rngs::SmallRng;
|
||||||
|
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::ops::DerefMut for Rng {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Debug, PartialEq, Eq)]
|
||||||
|
enum FightStatus {
|
||||||
|
Ongoing,
|
||||||
|
Over,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ScheduleLabel)]
|
||||||
|
enum Stage {
|
||||||
|
Equip,
|
||||||
|
Snapshot,
|
||||||
|
PreFight,
|
||||||
|
PreTurn,
|
||||||
|
Turn,
|
||||||
|
PostTurn,
|
||||||
|
PostFight,
|
||||||
|
Restore,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Stages {
|
||||||
|
equip: Schedule,
|
||||||
|
snapshot: Schedule,
|
||||||
|
pre_fight: Schedule,
|
||||||
|
pre_turn: Schedule,
|
||||||
|
turn: Schedule,
|
||||||
|
post_turn: Schedule,
|
||||||
|
post_fight: Schedule,
|
||||||
|
restore: Schedule,
|
||||||
|
world: World,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Stages {
|
||||||
|
fn add_event<T>(&mut self) -> &mut Self
|
||||||
|
where
|
||||||
|
T: Event,
|
||||||
|
{
|
||||||
|
if !self.world.contains_resource::<Events<T>>() {
|
||||||
|
self.world.init_resource::<Events<T>>();
|
||||||
|
self.pre_turn.add_systems(
|
||||||
|
bevy_ecs::event::event_update_system::<T>
|
||||||
|
.run_if(bevy_ecs::event::event_update_condition::<T>),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_effect<Effect: 'static>(&mut self) -> EffectBuilder {
|
||||||
|
register_effect::<Effect>(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Simulation(Stages);
|
||||||
|
|
||||||
|
impl Simulation {
|
||||||
|
pub fn new(attacker: dto::Player, defender: dto::Player) -> Self {
|
||||||
|
let world = World::new();
|
||||||
|
let mut stages = Stages {
|
||||||
|
equip: Schedule::new(Stage::Equip),
|
||||||
|
snapshot: Schedule::new(Stage::Snapshot),
|
||||||
|
pre_fight: Schedule::new(Stage::PreFight),
|
||||||
|
pre_turn: Schedule::new(Stage::PreTurn),
|
||||||
|
turn: Schedule::new(Stage::Turn),
|
||||||
|
post_turn: Schedule::new(Stage::PostTurn),
|
||||||
|
post_fight: Schedule::new(Stage::PostFight),
|
||||||
|
restore: Schedule::new(Stage::Restore),
|
||||||
|
world,
|
||||||
|
};
|
||||||
|
|
||||||
|
metrics::configure(&mut stages);
|
||||||
|
effect::configure(&mut stages);
|
||||||
|
player::configure(&mut stages);
|
||||||
|
passives::configure(&mut stages);
|
||||||
|
weapon::configure(&mut stages);
|
||||||
|
armour::configure(&mut stages);
|
||||||
|
log::configure(&mut stages);
|
||||||
|
entity_registry::configure(&mut stages);
|
||||||
|
|
||||||
|
stages.world.insert_resource(FightStatus::Ongoing);
|
||||||
|
stages
|
||||||
|
.world
|
||||||
|
.insert_resource(Rng(rand::rngs::SmallRng::from_entropy()));
|
||||||
|
|
||||||
|
stages.world.insert_resource(Logging(true));
|
||||||
|
|
||||||
|
attacker
|
||||||
|
.spawn(&mut stages.world)
|
||||||
|
.insert((Attacker, Current));
|
||||||
|
|
||||||
|
defender.spawn(&mut stages.world).insert(Defender);
|
||||||
|
|
||||||
|
stages.equip.run(&mut stages.world);
|
||||||
|
stages.pre_fight.run(&mut stages.world);
|
||||||
|
effect::run_effects(&mut stages.world);
|
||||||
|
stages.snapshot.run(&mut stages.world);
|
||||||
|
|
||||||
|
Self(stages)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_logging(&mut self, logging: bool) {
|
||||||
|
self.0.world.insert_resource(Logging(logging));
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn truncate_log(&mut self) {
|
||||||
|
let mut log = self.0.world.resource_mut::<Log>();
|
||||||
|
log.entries.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_metrics(&mut self, recording: bool) {
|
||||||
|
let mut metrics = self.0.world.resource_mut::<Metrics>();
|
||||||
|
metrics.active = recording;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn consume_metrics(&mut self) -> (Vec<dto::Counter>, Vec<dto::Histogram>) {
|
||||||
|
metrics::consume_metrics(&self.0.world)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_once(&mut self) {
|
||||||
|
loop {
|
||||||
|
self.0.pre_turn.run(&mut self.0.world);
|
||||||
|
effect::run_effects(&mut self.0.world);
|
||||||
|
self.0.turn.run(&mut self.0.world);
|
||||||
|
effect::run_effects(&mut self.0.world);
|
||||||
|
self.0.post_turn.run(&mut self.0.world);
|
||||||
|
effect::run_effects(&mut self.0.world);
|
||||||
|
let state = self.0.world.resource::<FightStatus>();
|
||||||
|
if *state == FightStatus::Over {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.0.post_fight.run(&mut self.0.world);
|
||||||
|
self.0.restore.run(&mut self.0.world);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub fn read_log(&self) -> serde_json::Value {
|
||||||
|
let log = self.0.world.resource::<Log>();
|
||||||
|
log.to_value()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fn main() {
|
||||||
|
let attacker = dto::Player {
|
||||||
|
name: "Pyrit".to_owned(),
|
||||||
|
level: 100,
|
||||||
|
stats: dto::Stats {
|
||||||
|
str: 1_035_562_970.0,
|
||||||
|
def: 1_309_681_178.0,
|
||||||
|
spd: 1_035_547_487.0,
|
||||||
|
dex: 339_651_454.0,
|
||||||
|
},
|
||||||
|
merits: dto::Merits {
|
||||||
|
life: 10,
|
||||||
|
crits: 10,
|
||||||
|
brawn: 10,
|
||||||
|
protection: 10,
|
||||||
|
sharpness: 10,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
|
||||||
|
education: dto::Education::default(),
|
||||||
|
faction: dto::FactionUpgrades {
|
||||||
|
def: 14,
|
||||||
|
dex: 14,
|
||||||
|
side_effects: 10,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
drug: Some(dto::DrugCooldown::Xanax),
|
||||||
|
weapons: dto::Weapons {
|
||||||
|
primary: Some(dto::Weapon {
|
||||||
|
name: "ArmaLite M-15A4".to_owned(),
|
||||||
|
cat: dto::WeaponCategory::Rifle,
|
||||||
|
dmg: 68.73,
|
||||||
|
acc: 59.41,
|
||||||
|
mods: vec![dto::WeaponMod::AcogSight],
|
||||||
|
ammo: dto::WeaponAmmo {
|
||||||
|
clips: 3,
|
||||||
|
clip_size: 15,
|
||||||
|
rate_of_fire: [3, 5],
|
||||||
|
},
|
||||||
|
experience: 100.0,
|
||||||
|
}),
|
||||||
|
melee: Some(dto::MeleeWeapon {
|
||||||
|
name: "Pillow".to_owned(),
|
||||||
|
cat: dto::WeaponCategory::Club,
|
||||||
|
japanese: false,
|
||||||
|
dmg: 1.18,
|
||||||
|
acc: 64.41,
|
||||||
|
experience: 100.0,
|
||||||
|
}),
|
||||||
|
temp: Some(dto::Temp::PepperSpray),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
strategy: dto::PlayerStrategy::InOrder {
|
||||||
|
order: vec![
|
||||||
|
// crate::dto::WeaponSlot::Melee,
|
||||||
|
// crate::dto::WeaponSlot::Temporary,
|
||||||
|
// crate::dto::WeaponSlot::Primary,
|
||||||
|
],
|
||||||
|
reload: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let defender = dto::Player {
|
||||||
|
name: "olesien".to_owned(),
|
||||||
|
level: 100,
|
||||||
|
stats: dto::Stats {
|
||||||
|
str: 1_101_841_257.0,
|
||||||
|
def: 745_915_274.0,
|
||||||
|
spd: 1_218_894_919.0,
|
||||||
|
dex: 1_301_489_826.0,
|
||||||
|
},
|
||||||
|
merits: dto::Merits {
|
||||||
|
life: 10,
|
||||||
|
crits: 10,
|
||||||
|
brawn: 10,
|
||||||
|
protection: 9,
|
||||||
|
sharpness: 10,
|
||||||
|
evasion: 10,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
education: dto::Education::default(),
|
||||||
|
faction: dto::FactionUpgrades {
|
||||||
|
str: 14,
|
||||||
|
spd: 13,
|
||||||
|
acc: 5,
|
||||||
|
dmg: 1,
|
||||||
|
side_effects: 10,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
drug: Some(dto::DrugCooldown::Xanax),
|
||||||
|
weapons: dto::Weapons {
|
||||||
|
primary: Some(dto::Weapon {
|
||||||
|
name: "ArmaLite M-15A4".to_owned(),
|
||||||
|
cat: dto::WeaponCategory::Rifle,
|
||||||
|
dmg: 68.73,
|
||||||
|
acc: 59.41,
|
||||||
|
mods: vec![dto::WeaponMod::Laser100mw],
|
||||||
|
ammo: dto::WeaponAmmo {
|
||||||
|
clips: 3,
|
||||||
|
clip_size: 15,
|
||||||
|
rate_of_fire: [3, 5],
|
||||||
|
},
|
||||||
|
experience: 100.0,
|
||||||
|
}),
|
||||||
|
melee: Some(dto::MeleeWeapon {
|
||||||
|
name: "Pillow".to_owned(),
|
||||||
|
cat: dto::WeaponCategory::Club,
|
||||||
|
japanese: false,
|
||||||
|
dmg: 1.54,
|
||||||
|
acc: 64.41,
|
||||||
|
experience: 100.0,
|
||||||
|
}),
|
||||||
|
temp: Some(crate::dto::Temp::Heg),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
strategy: dto::PlayerStrategy::InOrder {
|
||||||
|
order: vec![
|
||||||
|
// crate::dto::WeaponSlot::Melee,
|
||||||
|
// crate::dto::WeaponSlot::Temporary,
|
||||||
|
// crate::dto::WeaponSlot::Primary,
|
||||||
|
],
|
||||||
|
reload: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut simulation = Simulation::new(attacker, defender);
|
||||||
|
|
||||||
|
let log_value = simulation.run_once();
|
||||||
|
println!("{log_value:?}");
|
||||||
|
} */
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
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::default(),
|
||||||
|
bonuses: vec![WeaponBonusInfo {
|
||||||
|
bonus: WeaponBonus::Expose,
|
||||||
|
value: 9.0,
|
||||||
|
}],
|
||||||
|
experience: 100.0,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
armour: ArmourPieces {
|
||||||
|
helmet: Some(Armour {
|
||||||
|
name: "Test".to_owned(),
|
||||||
|
armour_value: 50.0,
|
||||||
|
coverage: [
|
||||||
|
100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0, 100.0,
|
||||||
|
],
|
||||||
|
immunities: Vec::default(),
|
||||||
|
set: None,
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn defender() -> dto::Player {
|
||||||
|
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()
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_simulator() {
|
||||||
|
let mut sim = Simulation::new(attacker(), defender());
|
||||||
|
sim.run_once();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn metrics() {
|
||||||
|
let mut sim = Simulation::new(attacker(), defender());
|
||||||
|
sim.set_metrics(true);
|
||||||
|
for _ in 0..20 {
|
||||||
|
sim.run_once();
|
||||||
|
}
|
||||||
|
sim.consume_metrics();
|
||||||
|
}
|
||||||
|
}
|
||||||
565
src/log.rs
Normal file
565
src/log.rs
Normal file
|
|
@ -0,0 +1,565 @@
|
||||||
|
use std::{collections::HashMap, sync::Mutex};
|
||||||
|
|
||||||
|
use bevy_ecs::{prelude::*, system::SystemParam};
|
||||||
|
use macros::LogMessage;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
hierarchy::Children,
|
||||||
|
player::{
|
||||||
|
stats::{
|
||||||
|
AdditiveBonus, BaselineStat, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat,
|
||||||
|
MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective,
|
||||||
|
SimpleStatMarker, Speed, StatMarker, StatType, Strength, WeaponAccuracy,
|
||||||
|
},
|
||||||
|
Player,
|
||||||
|
},
|
||||||
|
weapon::{Weapon, WeaponVerb},
|
||||||
|
Name, Stages,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct Logging(pub bool);
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
struct LogEvent(Mutex<Option<Box<dyn LogMessage>>>);
|
||||||
|
|
||||||
|
impl<T> From<T> for LogEvent
|
||||||
|
where
|
||||||
|
T: LogMessage,
|
||||||
|
{
|
||||||
|
fn from(value: T) -> Self {
|
||||||
|
Self(Mutex::new(Some(Box::new(value))))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PlayerInfo {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct WeaponInfo {
|
||||||
|
pub name: String,
|
||||||
|
pub verb: WeaponVerb,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub enum LogValue<'a> {
|
||||||
|
Float(f32),
|
||||||
|
Unsigned(u32),
|
||||||
|
Bool(bool),
|
||||||
|
String(String),
|
||||||
|
OptionNone,
|
||||||
|
Display(&'a (dyn std::fmt::Display + Send + Sync)),
|
||||||
|
Debug(&'a (dyn std::fmt::Debug + Send + Sync)),
|
||||||
|
Player(Entity),
|
||||||
|
Weapon(Entity),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<String> for LogValue<'a> {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Self::String(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<f32> for LogValue<'a> {
|
||||||
|
fn from(value: f32) -> Self {
|
||||||
|
Self::Float(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<u32> for LogValue<'a> {
|
||||||
|
fn from(value: u32) -> Self {
|
||||||
|
Self::Unsigned(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<u16> for LogValue<'a> {
|
||||||
|
fn from(value: u16) -> Self {
|
||||||
|
Self::Unsigned(value.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<bool> for LogValue<'static> {
|
||||||
|
fn from(value: bool) -> Self {
|
||||||
|
Self::Bool(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, T> From<Option<T>> for LogValue<'a>
|
||||||
|
where
|
||||||
|
T: Into<LogValue<'a>>,
|
||||||
|
{
|
||||||
|
fn from(value: Option<T>) -> Self {
|
||||||
|
match value {
|
||||||
|
Some(val) => val.into(),
|
||||||
|
None => LogValue::OptionNone,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
impl<'a> LogValue<'a> {
|
||||||
|
fn to_value(
|
||||||
|
&self,
|
||||||
|
player_registry: &HashMap<Entity, PlayerInfo>,
|
||||||
|
weapon_registry: &HashMap<Entity, WeaponInfo>,
|
||||||
|
) -> serde_json::Value {
|
||||||
|
match self {
|
||||||
|
LogValue::OptionNone => serde_json::Value::Null,
|
||||||
|
LogValue::Float(val) => {
|
||||||
|
serde_json::Value::Number(serde_json::Number::from_f64(*val as f64).unwrap())
|
||||||
|
}
|
||||||
|
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::Player(id) => serde_json::json!({
|
||||||
|
"type": "player",
|
||||||
|
"name": player_registry.get(id).unwrap().name,
|
||||||
|
}),
|
||||||
|
LogValue::Weapon(id) => serde_json::json!({
|
||||||
|
"type": "weapon",
|
||||||
|
"name": weapon_registry.get(id).unwrap().name,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait LogMessage: Send + Sync + 'static {
|
||||||
|
fn torn_style(
|
||||||
|
&self,
|
||||||
|
_player_registry: HashMap<Entity, PlayerInfo>,
|
||||||
|
_weapon_registry: HashMap<Entity, WeaponInfo>,
|
||||||
|
) -> Option<String> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tag(&self) -> &'static str;
|
||||||
|
|
||||||
|
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct Log {
|
||||||
|
pub player_registry: HashMap<Entity, PlayerInfo>,
|
||||||
|
pub weapon_registry: HashMap<Entity, WeaponInfo>,
|
||||||
|
|
||||||
|
pub entries: Vec<Box<dyn LogMessage>>,
|
||||||
|
|
||||||
|
pub expanded: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Log {
|
||||||
|
#[cfg(feature = "json")]
|
||||||
|
pub fn to_value(&self) -> serde_json::Value {
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
serde_json::json!({
|
||||||
|
"entries": self.entries.iter().map(|e|
|
||||||
|
json!({
|
||||||
|
"type": e.tag(),
|
||||||
|
"values": serde_json::Value::Object(
|
||||||
|
e.entries().iter().map(|e| (e.0.to_owned(), e.1.to_value(&self.player_registry, &self.weapon_registry))).collect()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
).collect::<Vec<_>>()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Log {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let mut iter = self.entries.iter().peekable();
|
||||||
|
while let Some(entry) = iter.next() {
|
||||||
|
write!(f, "{}: {{ ", entry.tag())?;
|
||||||
|
|
||||||
|
let mut fields = entry.entries().into_iter().peekable();
|
||||||
|
while let Some(field) = fields.next() {
|
||||||
|
if self.expanded {
|
||||||
|
write!(f, "\n ")?;
|
||||||
|
}
|
||||||
|
write!(f, "{} = ", field.0)?;
|
||||||
|
|
||||||
|
match field.1 {
|
||||||
|
LogValue::String(val) => write!(f, "\"{val}\"")?,
|
||||||
|
LogValue::Float(val) => write!(f, "{val}")?,
|
||||||
|
LogValue::Bool(val) => write!(f, "{val}")?,
|
||||||
|
LogValue::Unsigned(val) => write!(f, "{val}")?,
|
||||||
|
LogValue::OptionNone => write!(f, "None")?,
|
||||||
|
LogValue::Display(val) => write!(f, "\"{val}\"")?,
|
||||||
|
LogValue::Debug(val) => write!(f, "\"{val:?}\"")?,
|
||||||
|
LogValue::Player(id) => {
|
||||||
|
write!(f, "\"{}\"", self.player_registry.get(&id).unwrap().name)?
|
||||||
|
}
|
||||||
|
LogValue::Weapon(id) => {
|
||||||
|
write!(f, "\"{}\"", self.weapon_registry.get(&id).unwrap().name)?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if fields.peek().is_some() {
|
||||||
|
write!(f, ", ")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.expanded {
|
||||||
|
writeln!(f)?;
|
||||||
|
} else {
|
||||||
|
write!(f, " ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
if iter.peek().is_some() {
|
||||||
|
writeln!(f, "}}")?;
|
||||||
|
} else {
|
||||||
|
write!(f, "}}")?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* impl std::fmt::Display for Log {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
for entry in &self.entries {
|
||||||
|
match entry {
|
||||||
|
LogEntry::Hit {
|
||||||
|
actor,
|
||||||
|
recipient,
|
||||||
|
weapon,
|
||||||
|
dmg,
|
||||||
|
rounds,
|
||||||
|
crit,
|
||||||
|
part,
|
||||||
|
} => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let recipient_info = self.player_registry.get(recipient).unwrap();
|
||||||
|
let weapon_info = self.weapon_registry.get(weapon).unwrap();
|
||||||
|
|
||||||
|
write!(f, "{} ", actor_info.name)?;
|
||||||
|
|
||||||
|
match weapon_info.verb {
|
||||||
|
WeaponVerb::Fired => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"fired {} rounds from of their {} ",
|
||||||
|
rounds.unwrap(),
|
||||||
|
weapon_info.name
|
||||||
|
)?;
|
||||||
|
if *crit {
|
||||||
|
write!(f, "critically ")?;
|
||||||
|
}
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"hitting {} in the {} for {}",
|
||||||
|
recipient_info.name, part, dmg
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
WeaponVerb::Hit => {
|
||||||
|
if *crit {
|
||||||
|
write!(f, "critically ")?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pronouns and weapon verbs
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"hit {} with their {} in the {} for {}",
|
||||||
|
recipient_info.name, weapon_info.name, part, dmg
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
WeaponVerb::Exploded => {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{} threw a {} at {}, it exploded for {}",
|
||||||
|
actor_info.name, weapon_info.name, recipient_info.name, dmg
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogEntry::Miss {
|
||||||
|
actor,
|
||||||
|
recipient,
|
||||||
|
weapon,
|
||||||
|
rounds,
|
||||||
|
} => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let recipient_info = self.player_registry.get(recipient).unwrap();
|
||||||
|
let weapon_info = self.weapon_registry.get(weapon).unwrap();
|
||||||
|
|
||||||
|
match weapon_info.verb {
|
||||||
|
WeaponVerb::Hit => {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{} missed {} with their {}",
|
||||||
|
actor_info.name, recipient_info.name, weapon_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
WeaponVerb::Fired => {
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{} fired {} rounds of their {} missing {}",
|
||||||
|
actor_info.name,
|
||||||
|
rounds.unwrap(),
|
||||||
|
weapon_info.name,
|
||||||
|
recipient_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LogEntry::Defeat { actor, recipient } => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let recipient_info = self.player_registry.get(recipient).unwrap();
|
||||||
|
|
||||||
|
writeln!(f, "{} defeated {}", actor_info.name, recipient_info.name)?;
|
||||||
|
}
|
||||||
|
LogEntry::Stalemate { actor, recipient } => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let recipient_info = self.player_registry.get(recipient).unwrap();
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{} stalemated against {}",
|
||||||
|
actor_info.name, recipient_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
LogEntry::Loss { actor, recipient } => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let recipient_info = self.player_registry.get(recipient).unwrap();
|
||||||
|
|
||||||
|
writeln!(
|
||||||
|
f,
|
||||||
|
"{} lost against {}",
|
||||||
|
recipient_info.name, actor_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
LogEntry::Reload { actor, weapon } => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let weapon_info = self.weapon_registry.get(weapon).unwrap();
|
||||||
|
|
||||||
|
writeln!(f, "{} reloaded their {}", actor_info.name, weapon_info.name)?;
|
||||||
|
}
|
||||||
|
LogEntry::UsedDebuffTemp {
|
||||||
|
actor,
|
||||||
|
recipient,
|
||||||
|
temp,
|
||||||
|
weapon,
|
||||||
|
immune,
|
||||||
|
} => {
|
||||||
|
let actor_info = self.player_registry.get(actor).unwrap();
|
||||||
|
let recipient_info = self.player_registry.get(recipient).unwrap();
|
||||||
|
let weapon_info = self.weapon_registry.get(weapon).unwrap();
|
||||||
|
|
||||||
|
match temp {
|
||||||
|
DebuffingTemp::SmokeGrenade => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} threw a Smoke Grenade, smoke clouds around {}",
|
||||||
|
actor_info.name, recipient_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
DebuffingTemp::TearGas => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} threw a Tear Gas Grenade near {}",
|
||||||
|
actor_info.name, recipient_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
DebuffingTemp::PepperSpray => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} sprayed Pepper Spray in {}'s face",
|
||||||
|
actor_info.name, recipient_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"{} threw a {} at {}",
|
||||||
|
actor_info.name, recipient_info.name, weapon_info.name
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if *immune {
|
||||||
|
writeln!(f, " but it was ineffective")?;
|
||||||
|
} else {
|
||||||
|
writeln!(f)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
} */
|
||||||
|
|
||||||
|
#[derive(SystemParam)]
|
||||||
|
pub struct Logger<'w> {
|
||||||
|
event_writer: EventWriter<'w, LogEvent>,
|
||||||
|
logging: Res<'w, Logging>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'w> Logger<'w> {
|
||||||
|
pub fn log<B, M>(&mut self, body: B)
|
||||||
|
where
|
||||||
|
B: FnOnce() -> M,
|
||||||
|
M: LogMessage,
|
||||||
|
{
|
||||||
|
if self.logging.0 {
|
||||||
|
self.event_writer.send(body().into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn logging_enabled(logging: Res<Logging>) -> bool {
|
||||||
|
logging.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_entities(
|
||||||
|
player_q: Query<(Entity, &Name), With<Player>>,
|
||||||
|
weapon_q: Query<(Entity, &Name, &WeaponVerb), With<Weapon>>,
|
||||||
|
mut log: ResMut<Log>,
|
||||||
|
) {
|
||||||
|
for (player, name) in player_q.iter() {
|
||||||
|
log.player_registry.insert(
|
||||||
|
player,
|
||||||
|
PlayerInfo {
|
||||||
|
name: name.0.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (weapon, name, verb) in weapon_q.iter() {
|
||||||
|
log.weapon_registry.insert(
|
||||||
|
weapon,
|
||||||
|
WeaponInfo {
|
||||||
|
name: name.0.clone(),
|
||||||
|
verb: *verb,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn append_log_messages(mut events: EventReader<LogEvent>, mut log: ResMut<Log>) {
|
||||||
|
for event in events.read() {
|
||||||
|
log.entries.push(event.0.lock().unwrap().take().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(macros::LogMessage)]
|
||||||
|
struct StatChange {
|
||||||
|
#[log(player)]
|
||||||
|
target: Entity,
|
||||||
|
#[log(debug)]
|
||||||
|
stat: StatType,
|
||||||
|
|
||||||
|
#[log(debug)]
|
||||||
|
effects_add: Vec<(&'static str, f32)>,
|
||||||
|
|
||||||
|
#[log(debug)]
|
||||||
|
effects_mult: Vec<(&'static str, f32)>,
|
||||||
|
|
||||||
|
baseline: f32,
|
||||||
|
|
||||||
|
effective: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_stat_changes<Stat: StatMarker>(
|
||||||
|
stat_q: Query<
|
||||||
|
(Entity, &BaselineStat<Stat>, &EffectiveStat<Stat>, &Children),
|
||||||
|
Changed<EffectiveStat<Stat>>,
|
||||||
|
>,
|
||||||
|
add_q: Query<&AdditiveBonus<Stat>>,
|
||||||
|
mult_q: Query<&MultiplicativeBonus<Stat>>,
|
||||||
|
mut logger: Logger,
|
||||||
|
) {
|
||||||
|
for (player, baseline, effective, children) in stat_q.iter() {
|
||||||
|
let effects_add = add_q
|
||||||
|
.iter_many(children.get())
|
||||||
|
.map(|eff| (eff.label, eff.value))
|
||||||
|
.collect();
|
||||||
|
let effects_mult = mult_q
|
||||||
|
.iter_many(children.get())
|
||||||
|
.map(|eff| (eff.label, eff.value))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
logger.log(|| StatChange {
|
||||||
|
target: player,
|
||||||
|
stat: Stat::stat_type(),
|
||||||
|
effects_add,
|
||||||
|
effects_mult,
|
||||||
|
baseline: baseline.value,
|
||||||
|
effective: effective.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn log_simple_stat_changes<Stat: SimpleStatMarker>(
|
||||||
|
stat_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&SimpleStatBaseline<Stat>,
|
||||||
|
&SimpleStatEffective<Stat>,
|
||||||
|
&Children,
|
||||||
|
),
|
||||||
|
Changed<SimpleStatEffective<Stat>>,
|
||||||
|
>,
|
||||||
|
bonus_q: Query<&SimpleStatBonus<Stat>>,
|
||||||
|
mut logger: Logger,
|
||||||
|
) where
|
||||||
|
Stat::ValueType: Into<LogValue<'static>>,
|
||||||
|
Stat::BonusType: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
struct AppliedBonus {
|
||||||
|
#[log(debug)]
|
||||||
|
target: Entity,
|
||||||
|
#[log(display)]
|
||||||
|
stat: &'static str,
|
||||||
|
baseline: LogValue<'static>,
|
||||||
|
effective: LogValue<'static>,
|
||||||
|
#[log(debug)]
|
||||||
|
bonuses: Vec<(&'static str, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (target, baseline, effective, children) in stat_q.iter() {
|
||||||
|
let bonuses = bonus_q
|
||||||
|
.iter_many(children.get())
|
||||||
|
.map(|eff| (eff.label, format!("{:?}", eff.value)))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
logger.log(|| AppliedBonus {
|
||||||
|
target,
|
||||||
|
stat: std::any::type_name::<Stat>(),
|
||||||
|
baseline: baseline.value.into(),
|
||||||
|
effective: effective.value.into(),
|
||||||
|
bonuses,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.world.insert_resource(Log::default());
|
||||||
|
stages.world.insert_resource(Logging(false));
|
||||||
|
stages.add_event::<LogEvent>();
|
||||||
|
stages
|
||||||
|
.equip
|
||||||
|
.add_systems(register_entities.run_if(logging_enabled));
|
||||||
|
|
||||||
|
stages.post_turn.add_systems(append_log_messages);
|
||||||
|
stages.turn.add_systems(
|
||||||
|
(
|
||||||
|
log_stat_changes::<Strength>,
|
||||||
|
log_stat_changes::<Defence>,
|
||||||
|
log_stat_changes::<Speed>,
|
||||||
|
log_stat_changes::<Dexterity>,
|
||||||
|
log_simple_stat_changes::<CritRate>,
|
||||||
|
log_simple_stat_changes::<WeaponAccuracy>,
|
||||||
|
log_simple_stat_changes::<DamageBonus>,
|
||||||
|
)
|
||||||
|
.run_if(logging_enabled),
|
||||||
|
);
|
||||||
|
}
|
||||||
130
src/metrics.rs
Normal file
130
src/metrics.rs
Normal file
|
|
@ -0,0 +1,130 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{atomic, Mutex, RwLock},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::{dto, entity_registry::EntityRegistry, Stages};
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Histogram<T>
|
||||||
|
where
|
||||||
|
T: Copy + Send + Sync,
|
||||||
|
{
|
||||||
|
inner: Mutex<Vec<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Histogram<T>
|
||||||
|
where
|
||||||
|
T: Copy + Send + Sync,
|
||||||
|
{
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn record(&self, val: T) {
|
||||||
|
self.inner.lock().unwrap().push(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Counter {
|
||||||
|
inner: atomic::AtomicU64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Counter {
|
||||||
|
#[inline(always)]
|
||||||
|
pub fn increment(&self, value: u64) {
|
||||||
|
self.inner.fetch_add(value, atomic::Ordering::Relaxed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
pub struct MetricKey {
|
||||||
|
entity: Entity,
|
||||||
|
label: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource, Default)]
|
||||||
|
pub struct Metrics {
|
||||||
|
pub(crate) active: bool,
|
||||||
|
counters: RwLock<HashMap<MetricKey, Counter>>,
|
||||||
|
histograms: RwLock<HashMap<MetricKey, Histogram<u32>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Metrics {
|
||||||
|
pub fn record_histogram(&self, entity: Entity, label: &'static str, value: u32) {
|
||||||
|
if self.active {
|
||||||
|
let key = MetricKey { entity, label };
|
||||||
|
let r_hist = self.histograms.read().unwrap();
|
||||||
|
if let Some(hist) = r_hist.get(&key) {
|
||||||
|
hist.record(value);
|
||||||
|
} else {
|
||||||
|
std::mem::drop(r_hist);
|
||||||
|
|
||||||
|
let mut histograms = self.histograms.write().unwrap();
|
||||||
|
histograms.insert(
|
||||||
|
key,
|
||||||
|
Histogram {
|
||||||
|
inner: vec![value].into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn increment_counter(&self, entity: Entity, label: &'static str, value: u64) {
|
||||||
|
if self.active {
|
||||||
|
let key = MetricKey { entity, label };
|
||||||
|
let r_counters = self.counters.read().unwrap();
|
||||||
|
if let Some(counter) = r_counters.get(&key) {
|
||||||
|
counter.increment(value);
|
||||||
|
} else {
|
||||||
|
std::mem::drop(r_counters);
|
||||||
|
|
||||||
|
let mut counters = self.counters.write().unwrap();
|
||||||
|
counters.insert(
|
||||||
|
key,
|
||||||
|
Counter {
|
||||||
|
inner: value.into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn consume_metrics(world: &World) -> (Vec<dto::Counter>, Vec<dto::Histogram>) {
|
||||||
|
let metrics = world.resource::<Metrics>();
|
||||||
|
let entities = world.resource::<EntityRegistry>();
|
||||||
|
|
||||||
|
let counters = metrics
|
||||||
|
.counters
|
||||||
|
.try_write()
|
||||||
|
.unwrap()
|
||||||
|
.drain()
|
||||||
|
.map(|(key, value)| dto::Counter {
|
||||||
|
entity: entities.0.get(&key.entity).unwrap().clone(),
|
||||||
|
value: value.inner.load(atomic::Ordering::Relaxed),
|
||||||
|
label: key.label,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let histograms = metrics
|
||||||
|
.histograms
|
||||||
|
.try_write()
|
||||||
|
.unwrap()
|
||||||
|
.drain()
|
||||||
|
.map(|(key, value)| dto::Histogram {
|
||||||
|
entity: entities.0.get(&key.entity).unwrap().clone(),
|
||||||
|
values: value.inner.into_inner().unwrap(),
|
||||||
|
label: key.label,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
(counters, histograms)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.world.init_resource::<Metrics>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {}
|
||||||
405
src/passives.rs
Normal file
405
src/passives.rs
Normal file
|
|
@ -0,0 +1,405 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
effect::Effects,
|
||||||
|
player::stats::{
|
||||||
|
AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength,
|
||||||
|
},
|
||||||
|
Stages,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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(Bundle, Default)]
|
||||||
|
pub(crate) struct PassiveBundle {
|
||||||
|
pub merits: Merits,
|
||||||
|
pub education: Education,
|
||||||
|
pub faction: FactionUpgrades,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_permanent_effects(
|
||||||
|
merit_q: Query<(
|
||||||
|
Entity,
|
||||||
|
&Merits,
|
||||||
|
&Education,
|
||||||
|
&FactionUpgrades,
|
||||||
|
Option<&DrugCooldown>,
|
||||||
|
)>,
|
||||||
|
mut effects: Effects,
|
||||||
|
) {
|
||||||
|
for (player, merits, edu, faction, drug_cd) in merit_q.iter() {
|
||||||
|
if merits.brawn > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Strength>::new("brawn", (merits.brawn as f32) * 0.03),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if merits.protection > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Defence>::new("protection", (merits.protection as f32) * 0.03),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if merits.sharpness > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Speed>::new("sharpness", (merits.sharpness as f32) * 0.03),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if merits.evasion > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Dexterity>::new("evasion", (merits.evasion as f32) * 0.03),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if merits.crits > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<CritRate>::new("merits", merits.crits),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if faction.spd > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Speed>::new("faction", (faction.spd as f32) * 0.01),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if faction.str > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Strength>::new("faction", (faction.str as f32) * 0.01),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if faction.def > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Defence>::new("faction", (faction.def as f32) * 0.01),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if faction.dex > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Dexterity>::new("faction", (faction.dex as f32) * 0.01),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
fn spawn_drug_bonuses(
|
||||||
|
label: &'static str,
|
||||||
|
player: Entity,
|
||||||
|
effects: &mut Effects,
|
||||||
|
str: f32,
|
||||||
|
def: f32,
|
||||||
|
spd: f32,
|
||||||
|
dex: f32,
|
||||||
|
mit: f32,
|
||||||
|
) {
|
||||||
|
fn mitigate(val: f32, mit: f32) -> f32 {
|
||||||
|
if val.is_sign_negative() {
|
||||||
|
// NOTE: The rounding here is pure speculation
|
||||||
|
(val * mit).floor() / 100.0
|
||||||
|
} else {
|
||||||
|
val / 100.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Strength>::new(label, mitigate(str, mit)),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Defence>::new(label, mitigate(def, mit)),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Speed>::new(label, mitigate(spd, mit)),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
AdditiveBonus::<Dexterity>::new(label, mitigate(dex, mit)),
|
||||||
|
player,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mit = 1.0 - (faction.side_effects as f32) * 0.03;
|
||||||
|
|
||||||
|
match drug_cd {
|
||||||
|
Some(DrugCooldown::Xanax) => {
|
||||||
|
spawn_drug_bonuses(
|
||||||
|
"xanax",
|
||||||
|
player,
|
||||||
|
&mut effects,
|
||||||
|
-35.0,
|
||||||
|
-35.0,
|
||||||
|
-35.0,
|
||||||
|
-35.0,
|
||||||
|
mit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Some(DrugCooldown::Vicodin) => {
|
||||||
|
spawn_drug_bonuses("vicodin", player, &mut effects, 25.0, 25.0, 25.0, 25.0, mit);
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
|
||||||
|
if edu.bio2410 {
|
||||||
|
effects.spawn(SimpleStatBonus::<CritRate>::new("BIO2410", 6), player);
|
||||||
|
}
|
||||||
|
if edu.cbt2790 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("CBT2790", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.haf2104 {
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("HAF2104", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.haf2105 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("HAF2105", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.haf2106 {
|
||||||
|
effects.spawn(AdditiveBonus::<Strength>::new("HAF2106", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.haf2107 {
|
||||||
|
effects.spawn(AdditiveBonus::<Strength>::new("HAF2107", 0.02), player);
|
||||||
|
}
|
||||||
|
if edu.haf2108 {
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("HAF2108", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.haf2109 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("HAF2109", 0.03), player);
|
||||||
|
}
|
||||||
|
if edu.mth2240 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("MTH2240", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.mth2250 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("MTH2250", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.mth2260 {
|
||||||
|
effects.spawn(AdditiveBonus::<Defence>::new("MTH2260", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.mth2320 {
|
||||||
|
effects.spawn(AdditiveBonus::<Defence>::new("MTH2320", 0.02), player);
|
||||||
|
}
|
||||||
|
if edu.psy2640 {
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("PSY2640", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.psy2650 {
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("PSY2650", 0.02), player);
|
||||||
|
}
|
||||||
|
if edu.psy2660 {
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("PSY2660", 0.04), player);
|
||||||
|
}
|
||||||
|
if edu.psy2670 {
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("PSY2670", 0.08), player);
|
||||||
|
}
|
||||||
|
if edu.def2710 {
|
||||||
|
effects.spawn(AdditiveBonus::<Defence>::new("DEF2710", 0.01), player);
|
||||||
|
}
|
||||||
|
if edu.def2730 {
|
||||||
|
effects.spawn(AdditiveBonus::<Defence>::new("DEF2730", 0.02), player);
|
||||||
|
}
|
||||||
|
if edu.def2740 {
|
||||||
|
effects.spawn(AdditiveBonus::<Defence>::new("DEF2740", 0.03), player);
|
||||||
|
}
|
||||||
|
if edu.def2750 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("DEF2750", 0.02), player);
|
||||||
|
}
|
||||||
|
if edu.def2760 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("DEF2760", 0.03), player);
|
||||||
|
}
|
||||||
|
if edu.spt2490 {
|
||||||
|
effects.spawn(AdditiveBonus::<Speed>::new("SPT2490", 0.02), player);
|
||||||
|
effects.spawn(AdditiveBonus::<Strength>::new("SPT2490", 0.02), player);
|
||||||
|
}
|
||||||
|
if edu.spt2500 {
|
||||||
|
effects.spawn(AdditiveBonus::<Defence>::new("SPT2500", 0.02), player);
|
||||||
|
effects.spawn(AdditiveBonus::<Dexterity>::new("SPT2500", 0.02), player);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.equip.add_systems(spawn_permanent_effects);
|
||||||
|
}
|
||||||
916
src/player/mod.rs
Normal file
916
src/player/mod.rs
Normal file
|
|
@ -0,0 +1,916 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use macros::LogMessage;
|
||||||
|
use rand::Rng as _;
|
||||||
|
use strum::Display;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
armour,
|
||||||
|
effect::Effects,
|
||||||
|
hierarchy::Children,
|
||||||
|
log::Logger,
|
||||||
|
metrics::Metrics,
|
||||||
|
passives::{Education, FactionUpgrades, Merits},
|
||||||
|
weapon::{
|
||||||
|
bonus::MultiTurnBonus,
|
||||||
|
temp::{NonTargeted, Uses},
|
||||||
|
Ammo, DamageProcEffect, DamageStat, NeedsReload, RateOfFire, TurnTriggeredEffect, Usable,
|
||||||
|
Weapon, WeaponSlot,
|
||||||
|
},
|
||||||
|
FightStatus, Id, Name, Rng, Stages,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::stats::{
|
||||||
|
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health,
|
||||||
|
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub mod stats;
|
||||||
|
pub mod status_effect;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Attacker;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Defender;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Defeated;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Current;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct CurrentTarget;
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct Player;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Level(pub u16);
|
||||||
|
|
||||||
|
impl Default for Level {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
pub struct MaxHealth(pub u16);
|
||||||
|
|
||||||
|
impl Default for MaxHealth {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug, Default)]
|
||||||
|
pub struct CombatTurns(pub u16);
|
||||||
|
|
||||||
|
#[derive(Component, Default, Debug)]
|
||||||
|
pub struct Weapons {
|
||||||
|
pub primary: Option<Entity>,
|
||||||
|
pub secondary: Option<Entity>,
|
||||||
|
pub melee: Option<Entity>,
|
||||||
|
pub temporary: Option<Entity>,
|
||||||
|
pub fists: Option<Entity>,
|
||||||
|
pub kick: Option<Entity>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Weapons {
|
||||||
|
fn select(
|
||||||
|
&self,
|
||||||
|
slot: WeaponSlot,
|
||||||
|
reload: bool,
|
||||||
|
usable_q: &Query<(Has<NeedsReload>, &Children), With<Usable>>,
|
||||||
|
) -> Option<(Entity, Children)> {
|
||||||
|
let id = match slot {
|
||||||
|
WeaponSlot::Primary => self.primary?,
|
||||||
|
WeaponSlot::Secondary => self.secondary?,
|
||||||
|
WeaponSlot::Melee => self.melee?,
|
||||||
|
WeaponSlot::Temporary => self.temporary?,
|
||||||
|
WeaponSlot::Fists => self.fists?,
|
||||||
|
WeaponSlot::Kick => self.kick?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let (needs_reload, children) = usable_q.get(id).ok()?;
|
||||||
|
|
||||||
|
if !reload && needs_reload {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
Some((id, children.clone()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Debug)]
|
||||||
|
#[cfg_attr(feature = "json", derive(serde::Deserialize))]
|
||||||
|
#[cfg_attr(feature = "json", serde(tag = "type", rename_all = "snake_case"))]
|
||||||
|
pub enum PlayerStrategy {
|
||||||
|
AlwaysFists,
|
||||||
|
AlwaysKicks,
|
||||||
|
PrimaryMelee {
|
||||||
|
reload: bool,
|
||||||
|
},
|
||||||
|
InOrder {
|
||||||
|
order: Vec<WeaponSlot>,
|
||||||
|
reload: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlayerStrategy {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::AlwaysFists
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct ChooseWeapon(pub Entity);
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum BodyPart {
|
||||||
|
LeftHand,
|
||||||
|
RightHand,
|
||||||
|
LeftArm,
|
||||||
|
RightArm,
|
||||||
|
LeftFoot,
|
||||||
|
RightFoot,
|
||||||
|
LeftLeg,
|
||||||
|
RightLeg,
|
||||||
|
Stomach,
|
||||||
|
Chest,
|
||||||
|
Groin,
|
||||||
|
Head,
|
||||||
|
Throat,
|
||||||
|
Heart,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for BodyPart {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::LeftHand => write!(f, "Left hand"),
|
||||||
|
Self::RightHand => write!(f, "Right hand"),
|
||||||
|
Self::LeftArm => write!(f, "Left arm"),
|
||||||
|
Self::RightArm => write!(f, "Right arm"),
|
||||||
|
Self::LeftFoot => write!(f, "Left foot"),
|
||||||
|
Self::RightFoot => write!(f, "Right foot"),
|
||||||
|
Self::LeftLeg => write!(f, "Left leg"),
|
||||||
|
Self::RightLeg => write!(f, "Right leg"),
|
||||||
|
Self::Stomach => write!(f, "Stomach"),
|
||||||
|
Self::Chest => write!(f, "Chest"),
|
||||||
|
Self::Groin => write!(f, "Groin"),
|
||||||
|
Self::Head => write!(f, "Head"),
|
||||||
|
Self::Throat => write!(f, "Throat"),
|
||||||
|
Self::Heart => write!(f, "Heart"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Event)]
|
||||||
|
pub struct InitiateHit {
|
||||||
|
pub body_part: BodyPart,
|
||||||
|
pub weapon: Entity,
|
||||||
|
pub rounds: Option<u16>,
|
||||||
|
pub dmg: f32,
|
||||||
|
pub dmg_bonus_weapon: f32,
|
||||||
|
pub dmg_bonus_player: f32,
|
||||||
|
pub hit_chance: f32,
|
||||||
|
pub crit_rate: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Display)]
|
||||||
|
pub enum FightEndType {
|
||||||
|
Victory,
|
||||||
|
Stalemate,
|
||||||
|
Loss,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
struct FightEnd {
|
||||||
|
#[log(player)]
|
||||||
|
actor: Entity,
|
||||||
|
#[log(player)]
|
||||||
|
recipient: Entity,
|
||||||
|
#[log(display)]
|
||||||
|
fight_end_type: FightEndType,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct PlayerBundle {
|
||||||
|
pub name: Name,
|
||||||
|
pub id: Id,
|
||||||
|
pub player: Player,
|
||||||
|
pub level: Level,
|
||||||
|
pub crit_rate: SimpleStatBundle<CritRate>,
|
||||||
|
// TODO: since these two need to be tracked here anyways it might be preferable to shift all
|
||||||
|
// player specific passives here instead of tracking them on the weapons
|
||||||
|
pub acc_bonus: SimpleStatBundle<WeaponAccuracy>,
|
||||||
|
pub dmg_bonus: SimpleStatBundle<DamageBonus>,
|
||||||
|
|
||||||
|
pub strategy: PlayerStrategy,
|
||||||
|
pub combat_turns: CombatTurns,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerBundle {
|
||||||
|
pub fn new(name: impl ToString, id: usize, level: u16, strategy: PlayerStrategy) -> Self {
|
||||||
|
Self {
|
||||||
|
name: Name(name.to_string()),
|
||||||
|
id: Id(id),
|
||||||
|
player: Player,
|
||||||
|
level: Level(level),
|
||||||
|
crit_rate: SimpleStatBundle::new(24),
|
||||||
|
acc_bonus: SimpleStatBundle::new(0.0),
|
||||||
|
dmg_bonus: SimpleStatBundle::new(0.0),
|
||||||
|
strategy,
|
||||||
|
combat_turns: Default::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn derive_max_health(
|
||||||
|
level_query: Query<(Entity, &Level, &Merits, &FactionUpgrades)>,
|
||||||
|
mut cmd: Commands,
|
||||||
|
) {
|
||||||
|
for (entity, level, merits, faction) in level_query.iter() {
|
||||||
|
let base_life = match level.0 {
|
||||||
|
1..=8 => 100 + (level.0 - 1) * 25,
|
||||||
|
9..=95 => 275 + (level.0 - 8) * 50,
|
||||||
|
96.. => 4625 + (level.0 - 95) * 75,
|
||||||
|
0 => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
|
let max_health =
|
||||||
|
((base_life as f32) * (1.0 + ((merits.life * 5 + faction.life) as f32) / 100.0)) as u16;
|
||||||
|
|
||||||
|
cmd.entity(entity).insert((
|
||||||
|
MaxHealth(max_health),
|
||||||
|
SimpleStatBundle::<Health>::new(max_health),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn designate_first(
|
||||||
|
attacker_q: Query<Entity, With<Attacker>>,
|
||||||
|
defender_q: Query<Entity, With<Defender>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
let attacker = attacker_q.iter().next().unwrap();
|
||||||
|
let defender = defender_q.single();
|
||||||
|
commands.entity(attacker).insert(Current);
|
||||||
|
commands.entity(defender).insert(CurrentTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_roles(
|
||||||
|
current_q: Query<Entity, (With<Current>, With<Player>)>,
|
||||||
|
target_q: Query<Entity, With<CurrentTarget>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
let current = current_q.single();
|
||||||
|
let target = target_q.single();
|
||||||
|
|
||||||
|
// TODO: Group fights
|
||||||
|
commands
|
||||||
|
.entity(current)
|
||||||
|
.remove::<Current>()
|
||||||
|
.insert(CurrentTarget);
|
||||||
|
// TODO: Distraction
|
||||||
|
commands
|
||||||
|
.entity(target)
|
||||||
|
.insert(Current)
|
||||||
|
.remove::<CurrentTarget>();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check_term_condition(
|
||||||
|
mut state: ResMut<FightStatus>,
|
||||||
|
defender_q: Query<(), (With<Defender>, Without<Defeated>)>,
|
||||||
|
attacker_q: Query<(), (With<Attacker>, Without<Defeated>)>,
|
||||||
|
) {
|
||||||
|
if defender_q.is_empty() || attacker_q.is_empty() {
|
||||||
|
*state = FightStatus::Over;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pick_action(
|
||||||
|
mut p_query: Query<
|
||||||
|
(Entity, &Weapons, &PlayerStrategy, &mut CombatTurns),
|
||||||
|
(With<Current>, With<Player>),
|
||||||
|
>,
|
||||||
|
target_q: Query<Entity, With<CurrentTarget>>,
|
||||||
|
usable_q: Query<(Has<NeedsReload>, &Children), With<Usable>>,
|
||||||
|
weapon_trigger_q: Query<&TurnTriggeredEffect>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut effects: Effects,
|
||||||
|
metrics: Res<Metrics>,
|
||||||
|
) {
|
||||||
|
let (current, weapons, strat, mut turns) = p_query.single_mut();
|
||||||
|
let (weapon, children) = match strat {
|
||||||
|
PlayerStrategy::AlwaysFists => (weapons.fists.unwrap(), Default::default()),
|
||||||
|
PlayerStrategy::AlwaysKicks => weapons
|
||||||
|
.select(WeaponSlot::Kick, true, &usable_q)
|
||||||
|
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
|
||||||
|
PlayerStrategy::PrimaryMelee { reload } => weapons
|
||||||
|
.select(WeaponSlot::Primary, *reload, &usable_q)
|
||||||
|
.or_else(|| weapons.select(WeaponSlot::Melee, true, &usable_q))
|
||||||
|
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
|
||||||
|
PlayerStrategy::InOrder { order, reload } => order
|
||||||
|
.iter()
|
||||||
|
.find_map(|slot| weapons.select(*slot, *reload, &usable_q))
|
||||||
|
.unwrap_or_else(|| (weapons.fists.unwrap(), Default::default())),
|
||||||
|
};
|
||||||
|
metrics.increment_counter(current, "turn", 1);
|
||||||
|
metrics.increment_counter(weapon, "turn", 1);
|
||||||
|
|
||||||
|
commands.entity(weapon).insert(Current);
|
||||||
|
|
||||||
|
let target = target_q.single();
|
||||||
|
|
||||||
|
for effect in weapon_trigger_q.iter_many(children.get()) {
|
||||||
|
effect.trigger(&mut effects, current, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
turns.0 += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DamageSpread(rand_distr::Beta<f32>);
|
||||||
|
|
||||||
|
impl FromWorld for DamageSpread {
|
||||||
|
fn from_world(_world: &mut World) -> Self {
|
||||||
|
Self(rand_distr::Beta::new(3.0, 3.0).unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn receive_hit(
|
||||||
|
(mut rng, spread): (ResMut<crate::Rng>, Local<DamageSpread>),
|
||||||
|
mut hit_init_events: EventReader<InitiateHit>,
|
||||||
|
current_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&Education,
|
||||||
|
Option<&Attacker>,
|
||||||
|
&EffectiveStat<Strength>,
|
||||||
|
),
|
||||||
|
(With<Current>, With<Player>),
|
||||||
|
>,
|
||||||
|
mut target_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&mut SimpleStatEffective<Health>,
|
||||||
|
&armour::ArmourBodyParts,
|
||||||
|
&EffectiveStat<Defence>,
|
||||||
|
),
|
||||||
|
With<CurrentTarget>,
|
||||||
|
>,
|
||||||
|
armour_q: Query<&armour::ArmourBodyPart>,
|
||||||
|
(mut commands, mut logger): (Commands, Logger),
|
||||||
|
metrics: Res<Metrics>,
|
||||||
|
) {
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
struct HitTarget {
|
||||||
|
#[log(player)]
|
||||||
|
actor: Entity,
|
||||||
|
#[log(player)]
|
||||||
|
recipient: Entity,
|
||||||
|
#[log(weapon)]
|
||||||
|
weapon: Entity,
|
||||||
|
|
||||||
|
#[log(display)]
|
||||||
|
part: BodyPart,
|
||||||
|
part_mult: f32,
|
||||||
|
rounds: Option<u16>,
|
||||||
|
|
||||||
|
dmg: u32,
|
||||||
|
health_before: u16,
|
||||||
|
health_after: u16,
|
||||||
|
|
||||||
|
dmg_intrinsic: f32,
|
||||||
|
dmg_spread: f32,
|
||||||
|
|
||||||
|
armour_mitigation: f32,
|
||||||
|
def_mitigation: f32,
|
||||||
|
|
||||||
|
weapon_dmg: f32,
|
||||||
|
bonus_dmg: f32,
|
||||||
|
hit_chance: f32,
|
||||||
|
|
||||||
|
crit_rate: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
if hit_init_events.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (target, mut health, body_parts, target_def) = target_q.single_mut();
|
||||||
|
let (current, edu, attacker, current_str) = current_q.single();
|
||||||
|
|
||||||
|
let def_str_ratio = (target_def.value / current_str.value).clamp(1.0 / 32.0, 14.0);
|
||||||
|
let def_mitigation = if def_str_ratio < 1.0 {
|
||||||
|
0.5 * def_str_ratio.log(32.0) + 0.5
|
||||||
|
} else {
|
||||||
|
0.5 * def_str_ratio.log(14.0) + 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
let dmg_intrinsic = 7.0 * (current_str.value / 10.0).log10().powi(2)
|
||||||
|
+ 27.0 * (current_str.value / 10.0).log10()
|
||||||
|
+ 30.0;
|
||||||
|
|
||||||
|
for event in hit_init_events.read() {
|
||||||
|
let mult = match event.body_part {
|
||||||
|
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
|
||||||
|
metrics.increment_counter(current, "crit", 1);
|
||||||
|
metrics.increment_counter(event.weapon, "crit", 1);
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
BodyPart::LeftHand | BodyPart::RightHand | BodyPart::LeftFoot | BodyPart::RightFoot => {
|
||||||
|
0.2
|
||||||
|
}
|
||||||
|
BodyPart::LeftArm | BodyPart::RightArm | BodyPart::LeftLeg | BodyPart::RightLeg => {
|
||||||
|
1.0 / 3.5
|
||||||
|
}
|
||||||
|
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
|
||||||
|
};
|
||||||
|
|
||||||
|
metrics.increment_counter(current, "hit", 1);
|
||||||
|
metrics.increment_counter(event.weapon, "hit", 1);
|
||||||
|
|
||||||
|
let armour_parts = armour_q.get(body_parts.0[event.body_part.into()]).unwrap();
|
||||||
|
let piece = rng.sample(armour_parts);
|
||||||
|
let armour_mitigation = piece.map_or(0.0, |p| p.armour_value);
|
||||||
|
|
||||||
|
// NOTE: The beta distribution is defined on [0,1], so we rescale here
|
||||||
|
let dmg_spread = rng.sample(spread.0) / 10.0 + 1.0;
|
||||||
|
|
||||||
|
let mut dmg_bonus = event.dmg_bonus_weapon + event.dmg_bonus_player;
|
||||||
|
|
||||||
|
if edu.bio2380 && event.body_part == BodyPart::Throat {
|
||||||
|
dmg_bonus += 0.10;
|
||||||
|
}
|
||||||
|
|
||||||
|
let dmg = dmg_intrinsic
|
||||||
|
* event.dmg
|
||||||
|
* dmg_bonus
|
||||||
|
* (1.0 - armour_mitigation)
|
||||||
|
* (1.0 - def_mitigation)
|
||||||
|
* mult
|
||||||
|
* dmg_spread;
|
||||||
|
let dmg = dmg.round() as u32;
|
||||||
|
|
||||||
|
metrics.record_histogram(current, "dmg", dmg);
|
||||||
|
metrics.record_histogram(event.weapon, "dmg", dmg);
|
||||||
|
|
||||||
|
let health_before = health.value;
|
||||||
|
|
||||||
|
health.value = health.value.saturating_sub(dmg as u16);
|
||||||
|
|
||||||
|
logger.log(|| HitTarget {
|
||||||
|
actor: current,
|
||||||
|
recipient: target,
|
||||||
|
weapon: event.weapon,
|
||||||
|
part: event.body_part,
|
||||||
|
part_mult: mult,
|
||||||
|
rounds: event.rounds,
|
||||||
|
dmg,
|
||||||
|
health_before,
|
||||||
|
health_after: health.value,
|
||||||
|
dmg_spread,
|
||||||
|
dmg_intrinsic,
|
||||||
|
armour_mitigation,
|
||||||
|
def_mitigation,
|
||||||
|
weapon_dmg: event.dmg,
|
||||||
|
bonus_dmg: dmg_bonus,
|
||||||
|
hit_chance: event.hit_chance,
|
||||||
|
crit_rate: event.crit_rate,
|
||||||
|
});
|
||||||
|
|
||||||
|
if health.value == 0 {
|
||||||
|
commands.entity(target).insert(Defeated);
|
||||||
|
logger.log(|| FightEnd {
|
||||||
|
actor: current,
|
||||||
|
recipient: target,
|
||||||
|
fight_end_type: if attacker.is_some() {
|
||||||
|
FightEndType::Victory
|
||||||
|
} else {
|
||||||
|
FightEndType::Loss
|
||||||
|
},
|
||||||
|
});
|
||||||
|
metrics.increment_counter(current, "victory", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: unfortunately this function can't really be split into smaller parts due to the existence
|
||||||
|
// of multi turn bonuses
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
pub fn use_damaging_weapon(
|
||||||
|
mut rng: ResMut<Rng>,
|
||||||
|
weapon_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&DamageStat,
|
||||||
|
&SimpleStatEffective<WeaponAccuracy>,
|
||||||
|
&SimpleStatEffective<DamageBonus>,
|
||||||
|
&SimpleStatEffective<CritRate>,
|
||||||
|
&Children,
|
||||||
|
Has<NonTargeted>,
|
||||||
|
),
|
||||||
|
(With<Weapon>, With<Current>, Without<NeedsReload>),
|
||||||
|
>,
|
||||||
|
player_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&EffectiveStat<Speed>,
|
||||||
|
&EffectiveStat<Strength>,
|
||||||
|
&SimpleStatEffective<CritRate>,
|
||||||
|
&SimpleStatEffective<WeaponAccuracy>,
|
||||||
|
&SimpleStatEffective<DamageBonus>,
|
||||||
|
&Education,
|
||||||
|
Has<Attacker>,
|
||||||
|
),
|
||||||
|
(With<Player>, With<Current>),
|
||||||
|
>,
|
||||||
|
mut target_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&EffectiveStat<Dexterity>,
|
||||||
|
&EffectiveStat<Defence>,
|
||||||
|
&armour::ArmourBodyParts,
|
||||||
|
&mut SimpleStatEffective<Health>,
|
||||||
|
),
|
||||||
|
With<CurrentTarget>,
|
||||||
|
>,
|
||||||
|
armour_q: Query<&armour::ArmourBodyPart>,
|
||||||
|
damage_proc_q: Query<&DamageProcEffect>,
|
||||||
|
(mut ammo_q, mut temp_q): (
|
||||||
|
Query<(
|
||||||
|
&mut Ammo,
|
||||||
|
&SimpleStatEffective<Clips>,
|
||||||
|
&RateOfFire,
|
||||||
|
&SimpleStatEffective<AmmoControl>,
|
||||||
|
)>,
|
||||||
|
Query<&mut Uses>,
|
||||||
|
),
|
||||||
|
(mut logger, mut commands, dmg_spread, metrics): (
|
||||||
|
Logger,
|
||||||
|
Commands,
|
||||||
|
Local<DamageSpread>,
|
||||||
|
Res<Metrics>,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
pub struct MissTarget {
|
||||||
|
#[log(player)]
|
||||||
|
pub actor: Entity,
|
||||||
|
#[log(player)]
|
||||||
|
pub recipient: Entity,
|
||||||
|
#[log(weapon)]
|
||||||
|
pub weapon: Entity,
|
||||||
|
pub rounds: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
struct HitTarget {
|
||||||
|
#[log(player)]
|
||||||
|
actor: Entity,
|
||||||
|
#[log(player)]
|
||||||
|
recipient: Entity,
|
||||||
|
#[log(weapon)]
|
||||||
|
weapon: Entity,
|
||||||
|
|
||||||
|
#[log(display)]
|
||||||
|
part: BodyPart,
|
||||||
|
part_mult: f32,
|
||||||
|
rounds: Option<u16>,
|
||||||
|
|
||||||
|
dmg: u32,
|
||||||
|
health_before: u16,
|
||||||
|
health_after: u16,
|
||||||
|
|
||||||
|
dmg_intrinsic: f32,
|
||||||
|
dmg_spread: f32,
|
||||||
|
|
||||||
|
armour_mitigation: f32,
|
||||||
|
def_mitigation: f32,
|
||||||
|
|
||||||
|
weapon_dmg: f32,
|
||||||
|
bonus_dmg: f32,
|
||||||
|
hit_chance: f32,
|
||||||
|
|
||||||
|
crit_rate: u16,
|
||||||
|
}
|
||||||
|
|
||||||
|
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.get_single()
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, edu, attacker) =
|
||||||
|
player_q.single();
|
||||||
|
let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut();
|
||||||
|
|
||||||
|
if let Ok(mut uses) = temp_q.get_mut(weapon) {
|
||||||
|
uses.0 -= 1;
|
||||||
|
if uses.0 == 0 {
|
||||||
|
commands.entity(weapon).remove::<Usable>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let spd_dex_ratio = (player_spd.value / target_dex.value).clamp(1.0 / 64.0, 64.0);
|
||||||
|
let base_hit_chance = if spd_dex_ratio < 1.0 {
|
||||||
|
0.5 * (8.0 * spd_dex_ratio.sqrt() - 1.0) / 7.0
|
||||||
|
} else {
|
||||||
|
1.0 - 0.5 * (8.0 / spd_dex_ratio.sqrt() - 1.0) / 7.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut acc_eff = acc + acc_bonus;
|
||||||
|
|
||||||
|
let mut ammo = ammo_q
|
||||||
|
.get_mut(weapon)
|
||||||
|
.ok()
|
||||||
|
.map(|(ammo, clips, rof, ammo_ctrl)| {
|
||||||
|
let ammo_ctrl = 1.0 - (ammo_ctrl).value;
|
||||||
|
let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..((rof.0[1] as f32) * ammo_ctrl);
|
||||||
|
(ammo, clips, rof_eff)
|
||||||
|
});
|
||||||
|
|
||||||
|
enum MultiAttack {
|
||||||
|
Blindfire,
|
||||||
|
Rage(u16),
|
||||||
|
Fury { first_hit: bool },
|
||||||
|
DoubleTap { first_shot: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut multi_attack_proc = None;
|
||||||
|
let mut defeated = false;
|
||||||
|
let crit = player_crit + crit;
|
||||||
|
|
||||||
|
let def_str_ratio = (target_def.value / player_str.value).clamp(1.0 / 32.0, 14.0);
|
||||||
|
let def_mitigation = if def_str_ratio < 1.0 {
|
||||||
|
0.5 * def_str_ratio.log(32.0) + 0.5
|
||||||
|
} else {
|
||||||
|
0.5 * def_str_ratio.log(14.0) + 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
let dmg_intrinsic = 7.0 * (player_str.value / 10.0).log10().powi(2)
|
||||||
|
+ 27.0 * (player_str.value / 10.0).log10()
|
||||||
|
+ 30.0;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| {
|
||||||
|
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0);
|
||||||
|
metrics.increment_counter(player, "rounds_fired", rounds.into());
|
||||||
|
metrics.increment_counter(weapon, "rounds_fired", rounds.into());
|
||||||
|
ammo.0 -= rounds;
|
||||||
|
if ammo.0 == 0 {
|
||||||
|
if clips.value == 0 {
|
||||||
|
commands.entity(weapon).remove::<Usable>();
|
||||||
|
} else {
|
||||||
|
commands.entity(weapon).insert(NeedsReload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rounds
|
||||||
|
});
|
||||||
|
|
||||||
|
let hit_chance = if base_hit_chance < 0.5 {
|
||||||
|
base_hit_chance + acc_eff.value * base_hit_chance
|
||||||
|
} else {
|
||||||
|
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
|
||||||
|
};
|
||||||
|
|
||||||
|
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||||
|
logger.log(|| MissTarget {
|
||||||
|
weapon,
|
||||||
|
actor: player,
|
||||||
|
recipient: target,
|
||||||
|
rounds,
|
||||||
|
});
|
||||||
|
metrics.increment_counter(player, "miss", 1);
|
||||||
|
metrics.increment_counter(weapon, "miss", 1);
|
||||||
|
|
||||||
|
if multi_attack_proc.is_none() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let body_part = if !non_targeted {
|
||||||
|
rng.sample(crit)
|
||||||
|
} else {
|
||||||
|
BodyPart::Stomach
|
||||||
|
};
|
||||||
|
|
||||||
|
let mult = match body_part {
|
||||||
|
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
|
||||||
|
metrics.increment_counter(player, "crit", 1);
|
||||||
|
metrics.increment_counter(weapon, "crit", 1);
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
BodyPart::LeftHand
|
||||||
|
| BodyPart::RightHand
|
||||||
|
| BodyPart::LeftFoot
|
||||||
|
| BodyPart::RightFoot => 0.2,
|
||||||
|
BodyPart::LeftArm | BodyPart::RightArm | BodyPart::LeftLeg | BodyPart::RightLeg => {
|
||||||
|
1.0 / 3.5
|
||||||
|
}
|
||||||
|
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
|
||||||
|
};
|
||||||
|
|
||||||
|
metrics.increment_counter(player, "hit", 1);
|
||||||
|
metrics.increment_counter(weapon, "hit", 1);
|
||||||
|
|
||||||
|
let armour_parts = armour_q.get(armour_parts.0[body_part.into()]).unwrap();
|
||||||
|
let piece = rng.sample(armour_parts);
|
||||||
|
let armour_mitigation = piece.map_or(0.0, |p| p.armour_value);
|
||||||
|
|
||||||
|
// NOTE: The beta distribution is defined on [0,1], so we rescale here
|
||||||
|
let dmg_spread = rng.sample(dmg_spread.0) / 10.0 + 1.0;
|
||||||
|
|
||||||
|
let mut dmg_bonus = dmg_bonus + p_dmg_bonus;
|
||||||
|
|
||||||
|
if edu.bio2380 && body_part == BodyPart::Throat {
|
||||||
|
dmg_bonus.value += 0.10;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: special ammo
|
||||||
|
|
||||||
|
let dmg = dmg_intrinsic
|
||||||
|
* w_dmg.0
|
||||||
|
* dmg_bonus.value
|
||||||
|
* (1.0 - armour_mitigation)
|
||||||
|
* (1.0 - def_mitigation)
|
||||||
|
* mult
|
||||||
|
* dmg_spread;
|
||||||
|
let dmg = dmg.round() as u32;
|
||||||
|
|
||||||
|
metrics.record_histogram(player, "dmg", dmg);
|
||||||
|
metrics.record_histogram(weapon, "dmg", dmg);
|
||||||
|
|
||||||
|
if dmg > 0 {
|
||||||
|
for effect in damage_proc_q.iter_many(children.get()) {
|
||||||
|
match effect {
|
||||||
|
DamageProcEffect::MultiTurn { value, bonus }
|
||||||
|
if multi_attack_proc.is_none() =>
|
||||||
|
{
|
||||||
|
if rng.gen_bool(*value as f64) {
|
||||||
|
match bonus {
|
||||||
|
MultiTurnBonus::Blindfire => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::Blindfire)
|
||||||
|
}
|
||||||
|
MultiTurnBonus::Fury => {
|
||||||
|
multi_attack_proc =
|
||||||
|
Some(MultiAttack::Fury { first_hit: true })
|
||||||
|
}
|
||||||
|
MultiTurnBonus::Rage => {
|
||||||
|
multi_attack_proc =
|
||||||
|
Some(MultiAttack::Rage(rng.gen_range(2..=8)))
|
||||||
|
}
|
||||||
|
MultiTurnBonus::DoubleTap => {
|
||||||
|
multi_attack_proc =
|
||||||
|
Some(MultiAttack::DoubleTap { first_shot: true })
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let health_before = health.value;
|
||||||
|
|
||||||
|
health.value = health.value.saturating_sub(dmg as u16);
|
||||||
|
|
||||||
|
logger.log(|| HitTarget {
|
||||||
|
actor: player,
|
||||||
|
recipient: target,
|
||||||
|
weapon,
|
||||||
|
part: body_part,
|
||||||
|
part_mult: mult,
|
||||||
|
rounds,
|
||||||
|
dmg,
|
||||||
|
health_before,
|
||||||
|
health_after: health.value,
|
||||||
|
dmg_spread,
|
||||||
|
dmg_intrinsic,
|
||||||
|
armour_mitigation,
|
||||||
|
def_mitigation,
|
||||||
|
weapon_dmg: w_dmg.0,
|
||||||
|
bonus_dmg: dmg_bonus.value,
|
||||||
|
hit_chance,
|
||||||
|
crit_rate: crit.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if health.value == 0 && !defeated {
|
||||||
|
defeated = true;
|
||||||
|
|
||||||
|
commands.entity(target).insert(Defeated);
|
||||||
|
logger.log(|| FightEnd {
|
||||||
|
actor: player,
|
||||||
|
recipient: target,
|
||||||
|
fight_end_type: if attacker {
|
||||||
|
FightEndType::Victory
|
||||||
|
} else {
|
||||||
|
FightEndType::Loss
|
||||||
|
},
|
||||||
|
});
|
||||||
|
metrics.increment_counter(player, "victory", 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match multi_attack_proc {
|
||||||
|
Some(MultiAttack::Blindfire) => {
|
||||||
|
if !ammo.as_ref().map(|(a, _, _)| a.0 != 0).unwrap_or_default() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
acc_eff.value -= 5.0 / 50.0;
|
||||||
|
}
|
||||||
|
Some(MultiAttack::Fury { first_hit: true }) => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::Fury { first_hit: false })
|
||||||
|
}
|
||||||
|
Some(MultiAttack::Rage(turns @ 1..)) => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::Rage(turns - 1))
|
||||||
|
}
|
||||||
|
Some(MultiAttack::DoubleTap { first_shot: true }) => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::DoubleTap { first_shot: false })
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn check_stalemate(
|
||||||
|
current_q: Query<(Entity, &CombatTurns, Option<&Attacker>), (With<Current>, With<Player>)>,
|
||||||
|
target_q: Query<Entity, With<CurrentTarget>>,
|
||||||
|
other_attackers_q: Query<(), (With<Attacker>, Without<Current>)>,
|
||||||
|
mut state: ResMut<FightStatus>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut logger: Logger,
|
||||||
|
metrics: Res<Metrics>,
|
||||||
|
) {
|
||||||
|
let (current, current_turns, attacker) = current_q.single();
|
||||||
|
if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker.is_some() {
|
||||||
|
commands.entity(current).insert(Defeated);
|
||||||
|
let target = target_q.single();
|
||||||
|
logger.log(|| FightEnd {
|
||||||
|
actor: current,
|
||||||
|
recipient: target,
|
||||||
|
fight_end_type: FightEndType::Stalemate,
|
||||||
|
});
|
||||||
|
|
||||||
|
metrics.increment_counter(current, "stalemate", 1);
|
||||||
|
|
||||||
|
if other_attackers_q.is_empty() {
|
||||||
|
*state = FightStatus::Over
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn restore_initial_state(
|
||||||
|
mut state: ResMut<FightStatus>,
|
||||||
|
mut player_q: Query<(Entity, &mut CombatTurns, Has<Attacker>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
*state = FightStatus::Ongoing;
|
||||||
|
|
||||||
|
for (player, mut turns, attacker) in player_q.iter_mut() {
|
||||||
|
turns.0 = 0;
|
||||||
|
commands.entity(player).remove::<Defeated>();
|
||||||
|
if attacker {
|
||||||
|
commands
|
||||||
|
.entity(player)
|
||||||
|
.remove::<CurrentTarget>()
|
||||||
|
.insert(Current);
|
||||||
|
} else {
|
||||||
|
commands
|
||||||
|
.entity(player)
|
||||||
|
.remove::<Current>()
|
||||||
|
.insert(CurrentTarget);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn record_post_fight_stats(
|
||||||
|
player_q: Query<(Entity, &SimpleStatEffective<Health>)>,
|
||||||
|
metrics: Res<Metrics>,
|
||||||
|
) {
|
||||||
|
for (player, health) in player_q.iter() {
|
||||||
|
metrics.record_histogram(player, "rem_health", health.value as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stats::configure(stages);
|
||||||
|
status_effect::configure(stages);
|
||||||
|
|
||||||
|
stages.add_event::<ChooseWeapon>();
|
||||||
|
stages.add_event::<InitiateHit>();
|
||||||
|
stages.equip.add_systems(designate_first);
|
||||||
|
stages.pre_fight.add_systems(derive_max_health);
|
||||||
|
stages.pre_turn.add_systems(pick_action);
|
||||||
|
stages.turn.add_systems(use_damaging_weapon);
|
||||||
|
stages
|
||||||
|
.post_turn
|
||||||
|
.add_systems((check_term_condition, change_roles))
|
||||||
|
.add_systems(
|
||||||
|
check_stalemate
|
||||||
|
.after(check_term_condition)
|
||||||
|
.before(change_roles),
|
||||||
|
);
|
||||||
|
stages.post_fight.add_systems(record_post_fight_stats);
|
||||||
|
stages.restore.add_systems(restore_initial_state);
|
||||||
|
}
|
||||||
572
src/player/stats.rs
Normal file
572
src/player/stats.rs
Normal file
|
|
@ -0,0 +1,572 @@
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
|
use crate::{hierarchy::Parent, player::BodyPart, Stages};
|
||||||
|
|
||||||
|
pub trait SimpleStatMarker: Send + Sync + 'static {
|
||||||
|
type ValueType: Send + Sync + Copy + std::fmt::Display + 'static;
|
||||||
|
type BonusType: Send + Sync + Copy + 'static;
|
||||||
|
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SimpleStatBaseline<Stat: SimpleStatMarker> {
|
||||||
|
pub value: Stat::ValueType,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SimpleStatEffective<Stat: SimpleStatMarker> {
|
||||||
|
pub value: Stat::ValueType,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct SimpleStatBonus<Stat: SimpleStatMarker> {
|
||||||
|
pub label: &'static str,
|
||||||
|
pub value: Stat::BonusType,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: SimpleStatMarker> SimpleStatBonus<Stat> {
|
||||||
|
pub fn new(label: &'static str, value: Stat::BonusType) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct SimpleStatSnapshot<Stat: SimpleStatMarker> {
|
||||||
|
value: Stat::ValueType,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle)]
|
||||||
|
pub struct SimpleStatBundle<Stat: SimpleStatMarker> {
|
||||||
|
baseline: SimpleStatBaseline<Stat>,
|
||||||
|
effective: SimpleStatEffective<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: SimpleStatMarker> SimpleStatBundle<Stat> {
|
||||||
|
pub fn new(value: Stat::ValueType) -> Self {
|
||||||
|
Self {
|
||||||
|
baseline: SimpleStatBaseline {
|
||||||
|
value,
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
effective: SimpleStatEffective {
|
||||||
|
value,
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: SimpleStatMarker> Clone for SimpleStatEffective<Stat> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
*self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: SimpleStatMarker> Copy for SimpleStatEffective<Stat> where Stat::ValueType: Copy {}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct CritRate;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for CritRate {
|
||||||
|
type ValueType = u16;
|
||||||
|
type BonusType = u16;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value + bonus
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value - bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat> std::ops::Add<&SimpleStatEffective<Stat>> for &SimpleStatEffective<Stat>
|
||||||
|
where
|
||||||
|
Stat: SimpleStatMarker,
|
||||||
|
Stat::ValueType: std::ops::Add<Stat::ValueType, Output = Stat::ValueType>,
|
||||||
|
{
|
||||||
|
type Output = SimpleStatEffective<Stat>;
|
||||||
|
|
||||||
|
fn add(self, rhs: &SimpleStatEffective<Stat>) -> Self::Output {
|
||||||
|
SimpleStatEffective {
|
||||||
|
value: self.value + rhs.value,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl rand::distributions::Distribution<BodyPart> for SimpleStatEffective<CritRate> {
|
||||||
|
fn sample<R: rand::prelude::Rng + ?Sized>(&self, rng: &mut R) -> BodyPart {
|
||||||
|
if rng.gen_ratio((self.value) as u32, 200) {
|
||||||
|
match rng.gen_range(1..=10) {
|
||||||
|
1 => BodyPart::Heart,
|
||||||
|
2 => BodyPart::Throat,
|
||||||
|
_ => BodyPart::Heart,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match rng.gen_range(1..=20) {
|
||||||
|
1 => BodyPart::LeftHand,
|
||||||
|
2 => BodyPart::RightHand,
|
||||||
|
3 => BodyPart::LeftArm,
|
||||||
|
4 => BodyPart::RightArm,
|
||||||
|
5 => BodyPart::LeftFoot,
|
||||||
|
6 => BodyPart::RightFoot,
|
||||||
|
7 | 8 => BodyPart::RightLeg,
|
||||||
|
9 | 10 => BodyPart::LeftLeg,
|
||||||
|
11..=15 => BodyPart::Chest,
|
||||||
|
16 => BodyPart::Groin,
|
||||||
|
_ => BodyPart::Stomach,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct AmmoControl;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for AmmoControl {
|
||||||
|
type ValueType = f32;
|
||||||
|
type BonusType = f32;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value + bonus
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value - bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct DamageBonus;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for DamageBonus {
|
||||||
|
type ValueType = f32;
|
||||||
|
type BonusType = f32;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value + bonus
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value - bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct WeaponAccuracy;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for WeaponAccuracy {
|
||||||
|
type ValueType = f32;
|
||||||
|
type BonusType = f32;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value + bonus
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value - bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct ClipSize;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for ClipSize {
|
||||||
|
type ValueType = u16;
|
||||||
|
type BonusType = f32;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
((value as f32) * bonus).round() as u16
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
((value as f32) / bonus).round() as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Clips;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for Clips {
|
||||||
|
type ValueType = u16;
|
||||||
|
type BonusType = i16;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
((value as i16) + bonus) as u16
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
((value as i16) - bonus) as u16
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Health;
|
||||||
|
|
||||||
|
impl SimpleStatMarker for Health {
|
||||||
|
type ValueType = u16;
|
||||||
|
type BonusType = u16;
|
||||||
|
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value + bonus
|
||||||
|
}
|
||||||
|
fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {
|
||||||
|
value - bonus
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum StatType {
|
||||||
|
Str,
|
||||||
|
Def,
|
||||||
|
Spd,
|
||||||
|
Dex,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait StatMarker: Send + Sync + 'static {
|
||||||
|
fn stat_type() -> StatType;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Strength;
|
||||||
|
impl StatMarker for Strength {
|
||||||
|
fn stat_type() -> StatType {
|
||||||
|
StatType::Str
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Defence;
|
||||||
|
impl StatMarker for Defence {
|
||||||
|
fn stat_type() -> StatType {
|
||||||
|
StatType::Def
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Speed;
|
||||||
|
impl StatMarker for Speed {
|
||||||
|
fn stat_type() -> StatType {
|
||||||
|
StatType::Spd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct Dexterity;
|
||||||
|
impl StatMarker for Dexterity {
|
||||||
|
fn stat_type() -> StatType {
|
||||||
|
StatType::Dex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct BaselineStat<Stat: StatMarker> {
|
||||||
|
pub value: f32,
|
||||||
|
pub marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: StatMarker> Default for BaselineStat<Stat> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
value: 10.0,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct EffectiveStat<Stat: StatMarker> {
|
||||||
|
pub value: f32,
|
||||||
|
pub marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct AdditiveBonuses<Stat: StatMarker> {
|
||||||
|
pub factor: f32,
|
||||||
|
pub marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: StatMarker> Default for AdditiveBonuses<Stat> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
factor: 1.0,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct MultiplicativeBonuses<Stat: StatMarker> {
|
||||||
|
pub factor: f32,
|
||||||
|
pub marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: StatMarker> Default for MultiplicativeBonuses<Stat> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
factor: 1.0,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Bundle, Default)]
|
||||||
|
pub struct StatBundle<Stat: StatMarker> {
|
||||||
|
baseline: BaselineStat<Stat>,
|
||||||
|
additive: AdditiveBonuses<Stat>,
|
||||||
|
multiplicative: MultiplicativeBonuses<Stat>,
|
||||||
|
effective: EffectiveStat<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct StatSnapshot<Stat: StatMarker> {
|
||||||
|
additive_bonuses: f32,
|
||||||
|
multiplicative_bonuses: f32,
|
||||||
|
effective: f32,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: StatMarker> StatBundle<Stat> {
|
||||||
|
pub fn new(baseline: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
baseline: BaselineStat {
|
||||||
|
value: baseline,
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
effective: EffectiveStat {
|
||||||
|
value: baseline,
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
additive: AdditiveBonuses {
|
||||||
|
factor: 1.0,
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
multiplicative: MultiplicativeBonuses {
|
||||||
|
factor: 1.0,
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct AdditiveBonus<Stat: StatMarker> {
|
||||||
|
pub label: &'static str,
|
||||||
|
pub value: f32,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: StatMarker> AdditiveBonus<Stat> {
|
||||||
|
pub fn new(label: &'static str, value: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct MultiplicativeBonus<Stat: StatMarker> {
|
||||||
|
pub label: &'static str,
|
||||||
|
pub value: f32,
|
||||||
|
marker: PhantomData<Stat>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Stat: StatMarker> MultiplicativeBonus<Stat> {
|
||||||
|
pub fn new(label: &'static str, value: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_additive_bonus<Stat: StatMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(&AdditiveBonus<Stat>, &Parent)>,
|
||||||
|
mut stat_q: Query<(
|
||||||
|
&BaselineStat<Stat>,
|
||||||
|
&mut AdditiveBonuses<Stat>,
|
||||||
|
&MultiplicativeBonuses<Stat>,
|
||||||
|
&mut EffectiveStat<Stat>,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
for (bonus, player) in effect_q.iter_many(entities) {
|
||||||
|
let (baseline, mut add, mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||||
|
add.factor += bonus.value;
|
||||||
|
eff.value = baseline.value * add.factor * mult.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revert_additive_bonus<Stat: StatMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(&AdditiveBonus<Stat>, &Parent)>,
|
||||||
|
mut stat_q: Query<(
|
||||||
|
&BaselineStat<Stat>,
|
||||||
|
&mut AdditiveBonuses<Stat>,
|
||||||
|
&MultiplicativeBonuses<Stat>,
|
||||||
|
&mut EffectiveStat<Stat>,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
for (bonus, player) in effect_q.iter_many(entities) {
|
||||||
|
let (baseline, mut add, mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||||
|
add.factor -= bonus.value;
|
||||||
|
eff.value = baseline.value * add.factor * mult.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_multiplicative_bonus<Stat: StatMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(&MultiplicativeBonus<Stat>, &Parent)>,
|
||||||
|
mut stat_q: Query<(
|
||||||
|
&BaselineStat<Stat>,
|
||||||
|
&AdditiveBonuses<Stat>,
|
||||||
|
&mut MultiplicativeBonuses<Stat>,
|
||||||
|
&mut EffectiveStat<Stat>,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
for (bonus, player) in effect_q.iter_many(entities) {
|
||||||
|
let (baseline, add, mut mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||||
|
mult.factor *= bonus.value;
|
||||||
|
eff.value = baseline.value * add.factor * mult.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revert_multiplicative_bonus<Stat: StatMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(&MultiplicativeBonus<Stat>, &Parent)>,
|
||||||
|
mut stat_q: Query<(
|
||||||
|
&BaselineStat<Stat>,
|
||||||
|
&AdditiveBonuses<Stat>,
|
||||||
|
&mut MultiplicativeBonuses<Stat>,
|
||||||
|
&mut EffectiveStat<Stat>,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
for (bonus, player) in effect_q.iter_many(entities) {
|
||||||
|
let (baseline, add, mut mult, mut eff) = stat_q.get_mut(player.get()).unwrap();
|
||||||
|
mult.factor /= bonus.value;
|
||||||
|
eff.value = baseline.value * add.factor * mult.factor;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_stat_snapshots<Stat: StatMarker>(
|
||||||
|
stat_q: Query<(
|
||||||
|
Entity,
|
||||||
|
&AdditiveBonuses<Stat>,
|
||||||
|
&MultiplicativeBonuses<Stat>,
|
||||||
|
&EffectiveStat<Stat>,
|
||||||
|
)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (stat, add, mult, eff) in stat_q.iter() {
|
||||||
|
commands.entity(stat).insert(StatSnapshot::<Stat> {
|
||||||
|
additive_bonuses: add.factor,
|
||||||
|
multiplicative_bonuses: mult.factor,
|
||||||
|
effective: eff.value,
|
||||||
|
marker: PhantomData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_stats<Stat: StatMarker>(
|
||||||
|
mut stat_q: Query<(
|
||||||
|
&StatSnapshot<Stat>,
|
||||||
|
&mut AdditiveBonuses<Stat>,
|
||||||
|
&mut MultiplicativeBonuses<Stat>,
|
||||||
|
&mut EffectiveStat<Stat>,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
for (snapshot, mut add, mut mult, mut eff) in stat_q.iter_mut() {
|
||||||
|
add.factor = snapshot.additive_bonuses;
|
||||||
|
mult.factor = snapshot.multiplicative_bonuses;
|
||||||
|
eff.value = snapshot.effective;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_simple_stat_bonus<Stat: SimpleStatMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(&SimpleStatBonus<Stat>, &Parent)>,
|
||||||
|
mut stat_q: Query<&mut SimpleStatEffective<Stat>>,
|
||||||
|
) {
|
||||||
|
for (bonus, target) in effect_q.iter_many(entities) {
|
||||||
|
let mut effective = stat_q.get_mut(target.get()).unwrap();
|
||||||
|
effective.value = Stat::apply_bonus(effective.value, bonus.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn revert_simple_stat_bonus<Stat: SimpleStatMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(&SimpleStatBonus<Stat>, &Parent)>,
|
||||||
|
mut stat_q: Query<&mut SimpleStatEffective<Stat>>,
|
||||||
|
) {
|
||||||
|
for (bonus, target) in effect_q.iter_many(entities) {
|
||||||
|
let mut effective = stat_q.get_mut(target.get()).unwrap();
|
||||||
|
effective.value = Stat::revert_bonus(effective.value, bonus.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_simple_stat_snapshots<Stat: SimpleStatMarker>(
|
||||||
|
stat_q: Query<(Entity, &SimpleStatEffective<Stat>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (stat, eff) in stat_q.iter() {
|
||||||
|
commands.entity(stat).insert(SimpleStatSnapshot::<Stat> {
|
||||||
|
value: eff.value,
|
||||||
|
marker: PhantomData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_simple_stats<Stat: SimpleStatMarker>(
|
||||||
|
mut stat_q: Query<(&mut SimpleStatEffective<Stat>, &SimpleStatSnapshot<Stat>)>,
|
||||||
|
) {
|
||||||
|
for (mut eff, snapshot) in stat_q.iter_mut() {
|
||||||
|
eff.value = snapshot.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
fn register_stat_effects<Stat: StatMarker>(stages: &mut Stages) {
|
||||||
|
stages
|
||||||
|
.register_effect::<AdditiveBonus<Stat>>()
|
||||||
|
.apply(add_additive_bonus::<Stat>)
|
||||||
|
.teardown(revert_additive_bonus::<Stat>)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
stages
|
||||||
|
.register_effect::<MultiplicativeBonus<Stat>>()
|
||||||
|
.apply(add_multiplicative_bonus::<Stat>)
|
||||||
|
.teardown(revert_multiplicative_bonus::<Stat>)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
stages.snapshot.add_systems(create_stat_snapshots::<Stat>);
|
||||||
|
stages.restore.add_systems(restore_stats::<Stat>);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_simple_stat_effects<Stat: SimpleStatMarker>(stages: &mut Stages) {
|
||||||
|
stages
|
||||||
|
.register_effect::<SimpleStatBonus<Stat>>()
|
||||||
|
.apply(apply_simple_stat_bonus::<Stat>)
|
||||||
|
.teardown(revert_simple_stat_bonus::<Stat>)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
stages
|
||||||
|
.snapshot
|
||||||
|
.add_systems(create_simple_stat_snapshots::<Stat>);
|
||||||
|
stages.restore.add_systems(restore_simple_stats::<Stat>);
|
||||||
|
}
|
||||||
|
|
||||||
|
register_stat_effects::<Strength>(stages);
|
||||||
|
register_stat_effects::<Defence>(stages);
|
||||||
|
register_stat_effects::<Speed>(stages);
|
||||||
|
register_stat_effects::<Dexterity>(stages);
|
||||||
|
|
||||||
|
register_simple_stat_effects::<CritRate>(stages);
|
||||||
|
register_simple_stat_effects::<AmmoControl>(stages);
|
||||||
|
register_simple_stat_effects::<DamageBonus>(stages);
|
||||||
|
register_simple_stat_effects::<WeaponAccuracy>(stages);
|
||||||
|
register_simple_stat_effects::<ClipSize>(stages);
|
||||||
|
register_simple_stat_effects::<Health>(stages);
|
||||||
|
}
|
||||||
583
src/player/status_effect.rs
Normal file
583
src/player/status_effect.rs
Normal file
|
|
@ -0,0 +1,583 @@
|
||||||
|
use std::{collections::VecDeque, marker::PhantomData};
|
||||||
|
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use macros::LogMessage;
|
||||||
|
use rand::Rng as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
effect::{Effects, TimeLimitedEffect},
|
||||||
|
hierarchy::{HierarchyBuilder, Parent},
|
||||||
|
log::Logger,
|
||||||
|
weapon::temp::AssociatedWeapon,
|
||||||
|
Rng, Stages,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::stats::{
|
||||||
|
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct StatusEffectStack<T> {
|
||||||
|
pub effects: VecDeque<Entity>,
|
||||||
|
pub bonus: Entity,
|
||||||
|
pub marker: std::marker::PhantomData<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct TempDebuffEffect<T>(std::marker::PhantomData<T>);
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct TempDebuffImmunity<T>(std::marker::PhantomData<T>);
|
||||||
|
|
||||||
|
pub trait DebuffingTempMarker: Send + Sync + 'static {
|
||||||
|
type Stat: StatMarker;
|
||||||
|
|
||||||
|
fn factor() -> f32;
|
||||||
|
fn duration() -> std::ops::Range<f32>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct TearGas;
|
||||||
|
|
||||||
|
impl DebuffingTempMarker for TearGas {
|
||||||
|
type Stat = Dexterity;
|
||||||
|
fn factor() -> f32 {
|
||||||
|
1.0 / 3.0
|
||||||
|
}
|
||||||
|
fn duration() -> std::ops::Range<f32> {
|
||||||
|
120.0..180.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct SmokeGrenade;
|
||||||
|
|
||||||
|
impl DebuffingTempMarker for SmokeGrenade {
|
||||||
|
type Stat = Speed;
|
||||||
|
fn factor() -> f32 {
|
||||||
|
1.0 / 3.0
|
||||||
|
}
|
||||||
|
fn duration() -> std::ops::Range<f32> {
|
||||||
|
120.0..180.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct PepperSpray;
|
||||||
|
|
||||||
|
impl DebuffingTempMarker for PepperSpray {
|
||||||
|
type Stat = Dexterity;
|
||||||
|
fn factor() -> f32 {
|
||||||
|
1.0 / 5.0
|
||||||
|
}
|
||||||
|
fn duration() -> std::ops::Range<f32> {
|
||||||
|
15.0..20.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct ConcussionGrenade;
|
||||||
|
|
||||||
|
impl DebuffingTempMarker for ConcussionGrenade {
|
||||||
|
type Stat = Dexterity;
|
||||||
|
fn factor() -> f32 {
|
||||||
|
1.0 / 5.0
|
||||||
|
}
|
||||||
|
fn duration() -> std::ops::Range<f32> {
|
||||||
|
15.0..20.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct FlashGrenade;
|
||||||
|
|
||||||
|
impl DebuffingTempMarker for FlashGrenade {
|
||||||
|
type Stat = Speed;
|
||||||
|
fn factor() -> f32 {
|
||||||
|
1.0 / 5.0
|
||||||
|
}
|
||||||
|
fn duration() -> std::ops::Range<f32> {
|
||||||
|
15.0..20.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component, Default)]
|
||||||
|
pub struct Sand;
|
||||||
|
|
||||||
|
impl DebuffingTempMarker for Sand {
|
||||||
|
type Stat = Speed;
|
||||||
|
fn factor() -> f32 {
|
||||||
|
1.0 / 5.0
|
||||||
|
}
|
||||||
|
fn duration() -> std::ops::Range<f32> {
|
||||||
|
15.0..20.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct LinkedComponents<const N: usize>([Entity; N]);
|
||||||
|
|
||||||
|
trait Stats<const N: usize> {
|
||||||
|
fn spawn_additive_effects(
|
||||||
|
effects: &mut Effects,
|
||||||
|
target: Entity,
|
||||||
|
value: f32,
|
||||||
|
label: &'static str,
|
||||||
|
) -> [Entity; N];
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Stats<1> for T
|
||||||
|
where
|
||||||
|
T: StatMarker,
|
||||||
|
{
|
||||||
|
fn spawn_additive_effects(
|
||||||
|
effects: &mut Effects,
|
||||||
|
target: Entity,
|
||||||
|
value: f32,
|
||||||
|
label: &'static str,
|
||||||
|
) -> [Entity; 1] {
|
||||||
|
<(T,) as Stats<1>>::spawn_additive_effects(effects, target, value, label)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! impl_n_stats {
|
||||||
|
($n:expr, $($t:tt),+) => {
|
||||||
|
impl<$($t,)+> Stats<$n> for ($($t,)+) where $($t: StatMarker,)+ {
|
||||||
|
fn spawn_additive_effects(
|
||||||
|
effects: &mut Effects,
|
||||||
|
target: Entity,
|
||||||
|
value: f32,
|
||||||
|
label: &'static str,
|
||||||
|
) -> [Entity; $n] {
|
||||||
|
[$(effects.spawn(AdditiveBonus::<$t>::new(label, value), target),)+]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
impl_n_stats!(1, A);
|
||||||
|
impl_n_stats!(2, A, B);
|
||||||
|
impl_n_stats!(3, A, B, C);
|
||||||
|
impl_n_stats!(4, A, B, C, D);
|
||||||
|
|
||||||
|
trait AdditiveStatusEffectMarker<const N: usize>: Send + Sync + 'static {
|
||||||
|
type AffectedStats: Stats<N>;
|
||||||
|
fn max_stack() -> usize;
|
||||||
|
fn factor() -> f32;
|
||||||
|
fn duration() -> f32;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct AdditiveStatusEffect<const N: usize, M>
|
||||||
|
where
|
||||||
|
M: AdditiveStatusEffectMarker<N>,
|
||||||
|
{
|
||||||
|
marker: PhantomData<M>,
|
||||||
|
extra_effectiveness: f32,
|
||||||
|
extra_duration: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, M: AdditiveStatusEffectMarker<N>> Default for AdditiveStatusEffect<N, M> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
marker: PhantomData,
|
||||||
|
extra_effectiveness: 0.0,
|
||||||
|
extra_duration: 0.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, M: AdditiveStatusEffectMarker<N>> AdditiveStatusEffect<N, M> {
|
||||||
|
pub fn new(extra_effectiveness: f32, extra_duration: f32) -> Self {
|
||||||
|
Self {
|
||||||
|
marker: PhantomData,
|
||||||
|
extra_effectiveness,
|
||||||
|
extra_duration,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Withered;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Withered {
|
||||||
|
type AffectedStats = Strength;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
-0.25
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Weakened;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Weakened {
|
||||||
|
type AffectedStats = Defence;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
-0.25
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Slow;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Slow {
|
||||||
|
type AffectedStats = Speed;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
-0.25
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Crippled;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Crippled {
|
||||||
|
type AffectedStats = Dexterity;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
3
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
-0.25
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Motivate;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<4> for Motivate {
|
||||||
|
type AffectedStats = (Strength, Defence, Speed, Dexterity);
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
0.1
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Strengthened;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Strengthened {
|
||||||
|
type AffectedStats = Strength;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
5.0
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
120.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Hardened;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Hardened {
|
||||||
|
type AffectedStats = Defence;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
3.0
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
120.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Hastened;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Hastened {
|
||||||
|
type AffectedStats = Speed;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
5.0
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
120.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Sharpened;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<1> for Sharpened {
|
||||||
|
type AffectedStats = Dexterity;
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
5.0
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
120.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Demoralise;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<4> for Demoralise {
|
||||||
|
type AffectedStats = (Strength, Defence, Speed, Dexterity);
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
5
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
-0.1
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Frozen;
|
||||||
|
|
||||||
|
impl AdditiveStatusEffectMarker<2> for Frozen {
|
||||||
|
type AffectedStats = (Speed, Dexterity);
|
||||||
|
fn max_stack() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
fn factor() -> f32 {
|
||||||
|
-0.5
|
||||||
|
}
|
||||||
|
fn duration() -> f32 {
|
||||||
|
300.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(Entity, &Parent, &AdditiveStatusEffect<N, M>)>,
|
||||||
|
mut parent_q: Query<Option<&mut StatusEffectStack<M>>>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut effects: Effects,
|
||||||
|
) {
|
||||||
|
for (entity, player, effect) in effect_q.iter_many(entities) {
|
||||||
|
let stack = parent_q.get_mut(player.get()).unwrap();
|
||||||
|
|
||||||
|
let new_effects = <M::AffectedStats as Stats<N>>::spawn_additive_effects(
|
||||||
|
&mut effects,
|
||||||
|
player.get(),
|
||||||
|
M::factor() * (1.0 + effect.extra_effectiveness),
|
||||||
|
std::any::type_name::<M>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
commands.entity(entity).insert((
|
||||||
|
LinkedComponents(new_effects),
|
||||||
|
TimeLimitedEffect(M::duration() * (1.0 + effect.extra_duration)),
|
||||||
|
));
|
||||||
|
|
||||||
|
if let Some(mut stack) = stack {
|
||||||
|
stack.effects.push_back(entity);
|
||||||
|
if stack.effects.len() > M::max_stack() {
|
||||||
|
let first = stack.effects.pop_front().unwrap();
|
||||||
|
effects.remove(first);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
commands.spawn(StatusEffectStack::<M> {
|
||||||
|
effects: VecDeque::from([entity]),
|
||||||
|
bonus: entity,
|
||||||
|
marker: PhantomData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
effect_q: Query<(Entity, &Parent)>,
|
||||||
|
mut parent_q: Query<Option<&mut StatusEffectStack<M>>>,
|
||||||
|
linked_q: Query<&LinkedComponents<N>>,
|
||||||
|
mut effects: Effects,
|
||||||
|
) {
|
||||||
|
for (effect, player) in effect_q.iter_many(entities) {
|
||||||
|
if let Some(mut stack) = parent_q.get_mut(player.get()).unwrap() {
|
||||||
|
if stack.effects.front() == Some(&effect) {
|
||||||
|
stack.effects.pop_front();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let linked = linked_q.get(effect).unwrap();
|
||||||
|
|
||||||
|
for linked_effect in linked.0 {
|
||||||
|
effects.remove(linked_effect);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_temp_debuff_effect<Temp: DebuffingTempMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
mut rng: ResMut<Rng>,
|
||||||
|
temp_q: Query<(Entity, &Parent, &AssociatedWeapon)>,
|
||||||
|
weapon_q: Query<&Parent>,
|
||||||
|
mut parent_q: Query<(
|
||||||
|
Option<&mut StatusEffectStack<Temp>>,
|
||||||
|
Has<TempDebuffImmunity<Temp>>,
|
||||||
|
)>,
|
||||||
|
(mut commands, mut effects): (Commands, Effects),
|
||||||
|
mut logger: Logger,
|
||||||
|
) {
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
pub struct UsedDebuffTemp {
|
||||||
|
#[log(player)]
|
||||||
|
pub actor: Entity,
|
||||||
|
#[log(player)]
|
||||||
|
pub recipient: Entity,
|
||||||
|
#[log(weapon)]
|
||||||
|
pub weapon: Entity,
|
||||||
|
pub immune: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (effect, player, weapon) in temp_q.iter_many(entities) {
|
||||||
|
let (stack, immunity) = parent_q.get_mut(player.get()).unwrap();
|
||||||
|
let user = weapon_q.get(weapon.0).unwrap();
|
||||||
|
if immunity {
|
||||||
|
commands.entity(effect).despawn();
|
||||||
|
commands.entity(player.get()).remove_child(effect);
|
||||||
|
logger.log(|| UsedDebuffTemp {
|
||||||
|
actor: user.get(),
|
||||||
|
recipient: player.get(),
|
||||||
|
weapon: weapon.0,
|
||||||
|
immune: true,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let duration = rng.gen_range(Temp::duration());
|
||||||
|
commands.entity(effect).insert(TimeLimitedEffect(duration));
|
||||||
|
|
||||||
|
let stack_size = stack.as_ref().map_or(0, |s| s.effects.len()) as i32;
|
||||||
|
|
||||||
|
let effective_factor = (0..=stack_size).fold(1.0, |acc, i| {
|
||||||
|
acc * (1.0 - (1.0 - Temp::factor()) * 2.0f32.powi(-i))
|
||||||
|
});
|
||||||
|
|
||||||
|
let bonus = effects.spawn(
|
||||||
|
MultiplicativeBonus::<Temp::Stat>::new(std::any::type_name::<Temp>(), effective_factor),
|
||||||
|
player.get(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if let Some(mut stack) = stack {
|
||||||
|
effects.remove(stack.bonus);
|
||||||
|
stack.bonus = bonus;
|
||||||
|
stack.effects.push_back(effect);
|
||||||
|
} else {
|
||||||
|
commands
|
||||||
|
.entity(player.get())
|
||||||
|
.insert(StatusEffectStack::<Temp> {
|
||||||
|
effects: VecDeque::from([effect]),
|
||||||
|
bonus,
|
||||||
|
marker: PhantomData,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.log(|| UsedDebuffTemp {
|
||||||
|
actor: user.get(),
|
||||||
|
recipient: player.get(),
|
||||||
|
weapon: weapon.0,
|
||||||
|
immune: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove_temp_debuff_effect<Temp: DebuffingTempMarker>(
|
||||||
|
In(entities): In<Vec<Entity>>,
|
||||||
|
temp_q: Query<&Parent>,
|
||||||
|
mut parent_q: Query<(&mut StatusEffectStack<Temp>, Has<TempDebuffImmunity<Temp>>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
_logger: Logger,
|
||||||
|
mut effects: Effects,
|
||||||
|
) {
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
struct RemovedDebuffTemp {
|
||||||
|
#[log(player)]
|
||||||
|
recipient: Entity,
|
||||||
|
factor: f32,
|
||||||
|
factor_remaining: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
for player in temp_q.iter_many(entities) {
|
||||||
|
let (mut stack, immunity) = parent_q.get_mut(player.get()).unwrap();
|
||||||
|
if immunity {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
effects.remove(stack.bonus);
|
||||||
|
|
||||||
|
let stack_size = (stack.effects.len() - 1) as i32;
|
||||||
|
|
||||||
|
if stack_size > 0 {
|
||||||
|
let effective_factor = (0..=stack_size).fold(1.0, |acc, i| {
|
||||||
|
acc * (1.0 - (1.0 - Temp::factor()) * 2.0f32.powi(-i))
|
||||||
|
});
|
||||||
|
|
||||||
|
stack.bonus = effects.spawn(
|
||||||
|
MultiplicativeBonus::<Temp::Stat>::new(
|
||||||
|
std::any::type_name::<Temp>(),
|
||||||
|
effective_factor,
|
||||||
|
),
|
||||||
|
player.get(),
|
||||||
|
);
|
||||||
|
|
||||||
|
stack.effects.pop_front();
|
||||||
|
} else {
|
||||||
|
commands
|
||||||
|
.entity(player.get())
|
||||||
|
.remove::<StatusEffectStack<Temp>>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_debuff_temp<Temp: DebuffingTempMarker>(stages: &mut Stages) {
|
||||||
|
stages
|
||||||
|
.register_effect::<TempDebuffEffect<Temp>>()
|
||||||
|
.apply(apply_temp_debuff_effect::<Temp>)
|
||||||
|
.teardown(remove_temp_debuff_effect::<Temp>)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(stages: &mut Stages) {
|
||||||
|
stages
|
||||||
|
.register_effect::<AdditiveStatusEffect<N, M>>()
|
||||||
|
.apply(apply_additive_status_effect::<N, M>)
|
||||||
|
.teardown(remove_additive_status_effect::<N, M>)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
register_debuff_temp::<TearGas>(stages);
|
||||||
|
register_debuff_temp::<SmokeGrenade>(stages);
|
||||||
|
register_debuff_temp::<PepperSpray>(stages);
|
||||||
|
register_debuff_temp::<ConcussionGrenade>(stages);
|
||||||
|
register_debuff_temp::<FlashGrenade>(stages);
|
||||||
|
register_debuff_temp::<Sand>(stages);
|
||||||
|
|
||||||
|
register_status_effect::<1, Withered>(stages);
|
||||||
|
register_status_effect::<1, Weakened>(stages);
|
||||||
|
register_status_effect::<1, Slow>(stages);
|
||||||
|
register_status_effect::<1, Crippled>(stages);
|
||||||
|
register_status_effect::<1, Strengthened>(stages);
|
||||||
|
register_status_effect::<1, Hardened>(stages);
|
||||||
|
register_status_effect::<1, Hastened>(stages);
|
||||||
|
register_status_effect::<1, Sharpened>(stages);
|
||||||
|
register_status_effect::<4, Motivate>(stages);
|
||||||
|
register_status_effect::<4, Demoralise>(stages);
|
||||||
|
register_status_effect::<2, Frozen>(stages);
|
||||||
|
}
|
||||||
278
src/weapon/bonus.rs
Normal file
278
src/weapon/bonus.rs
Normal file
|
|
@ -0,0 +1,278 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
effect::{Effects, TurnLimitedEffect},
|
||||||
|
hierarchy::{HierarchyBuilder, Parent},
|
||||||
|
player::stats::{
|
||||||
|
AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus,
|
||||||
|
SimpleStatEffective, Speed, Strength, WeaponAccuracy,
|
||||||
|
},
|
||||||
|
Stages,
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
Quicken,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurnTriggeredBonus {
|
||||||
|
pub fn trigger(self, value: f32, effects: &mut Effects, current: Entity, _target: Entity) {
|
||||||
|
match self {
|
||||||
|
Self::Empower => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
AdditiveBonus::<Strength>::new("empower", value / 100.0),
|
||||||
|
current,
|
||||||
|
TurnLimitedEffect::new(current, 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::Quicken => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
AdditiveBonus::<Speed>::new("empower", value / 100.0),
|
||||||
|
current,
|
||||||
|
TurnLimitedEffect::new(current, 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum FirstTurnBonus {
|
||||||
|
Assassinate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FirstTurnBonus {
|
||||||
|
pub(crate) fn spawn(self, effects: &mut Effects, weapon: Entity, owner: Entity, value: f32) {
|
||||||
|
match self {
|
||||||
|
Self::Assassinate => effects.spawn_and_insert(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("assassinate", value / 100.0),
|
||||||
|
weapon,
|
||||||
|
TurnLimitedEffect::new(owner, 1),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum MultiTurnBonus {
|
||||||
|
Blindfire,
|
||||||
|
Fury,
|
||||||
|
DoubleTap,
|
||||||
|
Rage,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepare_bonuses(
|
||||||
|
bonus_q: Query<(
|
||||||
|
&Parent,
|
||||||
|
&WeaponBonus,
|
||||||
|
&BonusValue,
|
||||||
|
Option<&SimpleStatEffective<Clips>>,
|
||||||
|
)>,
|
||||||
|
mut effects: Effects,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (weapon, bonus, value, clips) in bonus_q.iter() {
|
||||||
|
match bonus {
|
||||||
|
WeaponBonus::Berserk => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("beserk", value.0 / 100.0),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("beserk", -value.0 / 2.0 / 50.0),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponBonus::Conserve => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<AmmoControl>::new("conserve", value.0 / 100.0),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponBonus::Expose => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<CritRate>::new("expose", (value.0 / 0.5) as u16),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponBonus::Grace => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("grace", -value.0 / 2.0 / 100.0),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("grace", value.0 / 50.0),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponBonus::Powerful => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("powerful", value.0 / 100.0),
|
||||||
|
weapon.get(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponBonus::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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
WeaponBonus::Empower => {
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Bonus {
|
||||||
|
value: value.0,
|
||||||
|
bonus: TurnTriggeredBonus::Empower,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
WeaponBonus::Quicken => {
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Bonus {
|
||||||
|
value: value.0,
|
||||||
|
bonus: TurnTriggeredBonus::Quicken,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
WeaponBonus::Assassinate => {
|
||||||
|
commands
|
||||||
|
.spawn(FirstTurnEffect::Bonus {
|
||||||
|
value: value.0,
|
||||||
|
bonus: FirstTurnBonus::Assassinate,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
WeaponBonus::Blindfire => {
|
||||||
|
commands
|
||||||
|
.spawn(DamageProcEffect::MultiTurn {
|
||||||
|
value: value.0,
|
||||||
|
bonus: MultiTurnBonus::Blindfire,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
WeaponBonus::Fury => {
|
||||||
|
commands
|
||||||
|
.spawn(DamageProcEffect::MultiTurn {
|
||||||
|
value: value.0,
|
||||||
|
bonus: MultiTurnBonus::Fury,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
WeaponBonus::Rage => {
|
||||||
|
commands
|
||||||
|
.spawn(DamageProcEffect::MultiTurn {
|
||||||
|
value: value.0,
|
||||||
|
bonus: MultiTurnBonus::Rage,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
WeaponBonus::DoubleTap => {
|
||||||
|
commands
|
||||||
|
.spawn(DamageProcEffect::MultiTurn {
|
||||||
|
value: value.0,
|
||||||
|
bonus: MultiTurnBonus::DoubleTap,
|
||||||
|
})
|
||||||
|
.set_parent(weapon.get());
|
||||||
|
}
|
||||||
|
|
||||||
|
val => unimplemented!("{val:?}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages
|
||||||
|
.pre_fight
|
||||||
|
.add_systems(prepare_bonuses.after(super::apply_passives));
|
||||||
|
}
|
||||||
955
src/weapon/mod.rs
Normal file
955
src/weapon/mod.rs
Normal file
|
|
@ -0,0 +1,955 @@
|
||||||
|
use bevy_ecs::prelude::*;
|
||||||
|
use macros::LogMessage;
|
||||||
|
use rand::Rng as _;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
effect::{Effects, TurnLimitedEffect},
|
||||||
|
hierarchy::{HierarchyBuilder, Parent},
|
||||||
|
log::Logger,
|
||||||
|
metrics::Metrics,
|
||||||
|
passives::{Education, FactionUpgrades, Merits},
|
||||||
|
player::{
|
||||||
|
stats::{
|
||||||
|
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
|
||||||
|
EffectiveStat, SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, Speed,
|
||||||
|
WeaponAccuracy,
|
||||||
|
},
|
||||||
|
BodyPart, Current, CurrentTarget, InitiateHit, Player, Weapons,
|
||||||
|
},
|
||||||
|
Id, Name, Rng, Stages,
|
||||||
|
};
|
||||||
|
|
||||||
|
use self::{
|
||||||
|
bonus::{FirstTurnBonus, MultiTurnBonus, TurnTriggeredBonus},
|
||||||
|
temp::{NonTargeted, Uses},
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
Club,
|
||||||
|
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,
|
||||||
|
HeavyDutyBreak,
|
||||||
|
TacticalBrake,
|
||||||
|
SmallLight,
|
||||||
|
PrecisionLight,
|
||||||
|
TacticalIlluminator,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum TurnTriggeredMod {
|
||||||
|
Bipod,
|
||||||
|
Tripod,
|
||||||
|
SmallLight,
|
||||||
|
PrecisionLight,
|
||||||
|
TacticalIlluminator,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurnTriggeredMod {
|
||||||
|
pub fn trigger(self, effects: &mut Effects, current: Entity, target: Entity) {
|
||||||
|
match self {
|
||||||
|
Self::Bipod => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
AdditiveBonus::<Dexterity>::new("bipod", -0.3),
|
||||||
|
current,
|
||||||
|
TurnLimitedEffect::new(current, 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::Tripod => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
AdditiveBonus::<Dexterity>::new("tripod", -0.3),
|
||||||
|
current,
|
||||||
|
TurnLimitedEffect::new(current, 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::SmallLight => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("small light", -3.0 / 50.0),
|
||||||
|
target,
|
||||||
|
TurnLimitedEffect::new(target, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::PrecisionLight => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("precision light", -4.0 / 50.0),
|
||||||
|
target,
|
||||||
|
TurnLimitedEffect::new(target, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Self::TacticalIlluminator => {
|
||||||
|
effects.spawn_and_insert(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("tactical illuminator", -5.0 / 50.0),
|
||||||
|
target,
|
||||||
|
TurnLimitedEffect::new(target, 1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Effects that are triggered by selecting a weapon
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum TurnTriggeredEffect {
|
||||||
|
Mod(TurnTriggeredMod),
|
||||||
|
Bonus {
|
||||||
|
value: f32,
|
||||||
|
bonus: TurnTriggeredBonus,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TurnTriggeredEffect {
|
||||||
|
pub fn trigger(&self, effects: &mut Effects, current: Entity, target: Entity) {
|
||||||
|
match self {
|
||||||
|
TurnTriggeredEffect::Mod(weapon_mod) => {
|
||||||
|
weapon_mod.trigger(effects, current, target);
|
||||||
|
}
|
||||||
|
TurnTriggeredEffect::Bonus { value, bonus } => {
|
||||||
|
bonus.trigger(*value, effects, current, target)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub enum FirstTurnMod {
|
||||||
|
AdjustableTrigger,
|
||||||
|
HairTrigger,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FirstTurnMod {
|
||||||
|
fn spawn(self, effects: &mut Effects, weapon: Entity, owner: Entity) {
|
||||||
|
match self {
|
||||||
|
Self::AdjustableTrigger => effects.spawn_and_insert(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("adjustable trigger", 5.0 / 50.0),
|
||||||
|
weapon,
|
||||||
|
TurnLimitedEffect::new(owner, 1),
|
||||||
|
),
|
||||||
|
Self::HairTrigger => effects.spawn_and_insert(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("hair trigger", 7.5 / 50.0),
|
||||||
|
weapon,
|
||||||
|
TurnLimitedEffect::new(owner, 1),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum FirstTurnEffect {
|
||||||
|
Mod(FirstTurnMod),
|
||||||
|
Bonus { value: f32, bonus: FirstTurnBonus },
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub enum DamageProcEffect {
|
||||||
|
MultiTurn { value: f32, bonus: MultiTurnBonus },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FirstTurnEffect {
|
||||||
|
fn spawn(&self, effects: &mut Effects, weapon: Entity, owner: Entity) {
|
||||||
|
match self {
|
||||||
|
Self::Mod(weapon_mod) => weapon_mod.spawn(effects, weapon, owner),
|
||||||
|
Self::Bonus { value, bonus } => bonus.spawn(effects, weapon, owner, *value),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct EquippedMods(pub Vec<WeaponMod>);
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
pub struct Experience(pub f32);
|
||||||
|
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
pub struct ReloadWeapon {
|
||||||
|
#[log(player)]
|
||||||
|
pub actor: Entity,
|
||||||
|
#[log(weapon)]
|
||||||
|
pub weapon: Entity,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(LogMessage)]
|
||||||
|
pub struct MissTarget {
|
||||||
|
#[log(player)]
|
||||||
|
pub actor: Entity,
|
||||||
|
#[log(player)]
|
||||||
|
pub recipient: Entity,
|
||||||
|
#[log(weapon)]
|
||||||
|
pub weapon: Entity,
|
||||||
|
pub rounds: Option<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(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 {
|
||||||
|
commands.entity(primary).set_parent(player);
|
||||||
|
}
|
||||||
|
if let Some(secondary) = weapons.secondary {
|
||||||
|
commands.entity(secondary).set_parent(player);
|
||||||
|
}
|
||||||
|
if let Some(melee) = weapons.melee {
|
||||||
|
commands.entity(melee).set_parent(player);
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_passives(
|
||||||
|
weapon_q: Query<(
|
||||||
|
Entity,
|
||||||
|
&EquippedMods,
|
||||||
|
&Experience,
|
||||||
|
&WeaponCategory,
|
||||||
|
&WeaponSlot,
|
||||||
|
Has<Japanese>,
|
||||||
|
&Parent,
|
||||||
|
)>,
|
||||||
|
player_q: Query<(&Merits, &Education, &FactionUpgrades)>,
|
||||||
|
mut effects: Effects,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (weapon, mods, exp, cat, slot, japanese, player) in weapon_q.iter() {
|
||||||
|
let exp = (exp.0 / 10.0).round();
|
||||||
|
if exp > 0.0 {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("experience", exp * 0.2 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("experience", exp / 100.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for w_mod in &mods.0 {
|
||||||
|
match w_mod {
|
||||||
|
WeaponMod::ReflexSight => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("reflex sight", 1.0 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::HolographicSight => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("holographic sight", 1.25 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::AcogSight => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("ACOG sight", 1.5 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::ThermalSight => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("thermal sight", 1.75 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::Laser1mw => {
|
||||||
|
effects.spawn(SimpleStatBonus::<CritRate>::new("1mw laser", 4), weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::Laser5mw => {
|
||||||
|
effects.spawn(SimpleStatBonus::<CritRate>::new("5mw laser", 6), weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::Laser30mw => {
|
||||||
|
effects.spawn(SimpleStatBonus::<CritRate>::new("30mw laser", 8), weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::Laser100mw => {
|
||||||
|
effects.spawn(SimpleStatBonus::<CritRate>::new("100mw laser", 10), weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::SmallSuppressor => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("small suppressor", -0.05),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::StandardSuppressor => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("standard suppressor", -0.05),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::LargeSuppressor => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("large suppressor", -0.05),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::ExtendedMags => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<ClipSize>::new("extended mags", 1.2),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::HighCapacityMags => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<ClipSize>::new("high capacity mags", 1.3),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::CustomGrip => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("custom grip", 0.75),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::SkeetChoke => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("skeet choke", 0.06),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::ImprovedChoke => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("improved choke", 0.08),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::FullChoke => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("full choke", 0.10),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::ExtraClip => {
|
||||||
|
effects.spawn(SimpleStatBonus::<Clips>::new("extra clip", 1), weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::ExtraClip2 => {
|
||||||
|
effects.spawn(SimpleStatBonus::<Clips>::new("extra clip x2", 2), weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::RecoilPad => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<AmmoControl>::new("recoil pad", 0.24),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::StandardBrake => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("standard brake", 1.00 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::HeavyDutyBreak => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("heavy duty brake", 1.25 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::TacticalBrake => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("tactical brake", 1.50 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
WeaponMod::Bipod => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("bipod", 1.75 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::Bipod))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::Tripod => {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("bipod", 2.00 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::Tripod))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::SmallLight => {
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::SmallLight))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::PrecisionLight => {
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Mod(TurnTriggeredMod::PrecisionLight))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::TacticalIlluminator => {
|
||||||
|
commands
|
||||||
|
.spawn(TurnTriggeredEffect::Mod(
|
||||||
|
TurnTriggeredMod::TacticalIlluminator,
|
||||||
|
))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::AdjustableTrigger => {
|
||||||
|
commands
|
||||||
|
.spawn(FirstTurnEffect::Mod(FirstTurnMod::AdjustableTrigger))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
WeaponMod::HairTrigger => {
|
||||||
|
commands
|
||||||
|
.spawn(FirstTurnEffect::Mod(FirstTurnMod::HairTrigger))
|
||||||
|
.set_parent(weapon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (merits, education, faction) = player_q.get(player.get()).unwrap();
|
||||||
|
|
||||||
|
let (mastery, edu_acc) = match cat {
|
||||||
|
WeaponCategory::HeavyArtillery => (
|
||||||
|
merits.heavy_artillery_mastery,
|
||||||
|
education.cbt2860.then_some("CBT2860"),
|
||||||
|
),
|
||||||
|
WeaponCategory::MachineGun => (
|
||||||
|
merits.machine_gun_mastery,
|
||||||
|
education.cbt2830.then_some("CTB2830"),
|
||||||
|
),
|
||||||
|
WeaponCategory::Rifle => (merits.rifle_mastery, education.cbt2850.then_some("CBT2850")),
|
||||||
|
WeaponCategory::Smg => (merits.smg_mastery, education.cbt2830.then_some("CBT2830")),
|
||||||
|
WeaponCategory::Shotgun => (
|
||||||
|
merits.shotgun_mastery,
|
||||||
|
education.cbt2125.then_some("CBT2125"),
|
||||||
|
),
|
||||||
|
WeaponCategory::Pistol => (
|
||||||
|
merits.pistol_mastery,
|
||||||
|
education.cbt2840.then_some("CBT2840"),
|
||||||
|
),
|
||||||
|
WeaponCategory::Club => (merits.club_mastery, None),
|
||||||
|
WeaponCategory::Piercing => (merits.piercing_mastery, None),
|
||||||
|
WeaponCategory::Slashing => (merits.slashing_mastery, None),
|
||||||
|
WeaponCategory::Mechanical => (merits.mechanical_mastery, None),
|
||||||
|
WeaponCategory::Temporary => {
|
||||||
|
if education.gen2119 {
|
||||||
|
effects.spawn(SimpleStatBonus::<DamageBonus>::new("GEN2119", 0.05), weapon);
|
||||||
|
}
|
||||||
|
(
|
||||||
|
merits.temporary_mastery,
|
||||||
|
education.gen2116.then_some("GEN2116"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
WeaponCategory::HandToHand => (0, None),
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(label) = edu_acc {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new(label, 1.0 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: Even though it says "weapons" in the description, this also applies to h2h
|
||||||
|
if education.bio2350 {
|
||||||
|
effects.spawn(SimpleStatBonus::<DamageBonus>::new("BIO2350", 0.01), weapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if education.def3770 && *slot == WeaponSlot::Fists {
|
||||||
|
effects.spawn(SimpleStatBonus::<DamageBonus>::new("DEF3770", 1.0), weapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if education.his2170 && *slot == WeaponSlot::Melee {
|
||||||
|
effects.spawn(SimpleStatBonus::<DamageBonus>::new("HIS2170", 0.02), weapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(*slot, WeaponSlot::Primary | WeaponSlot::Secondary) {
|
||||||
|
if education.mth2310 {
|
||||||
|
effects.spawn(SimpleStatBonus::<AmmoControl>::new("MTH2310", 0.05), weapon);
|
||||||
|
}
|
||||||
|
if education.mth3330 {
|
||||||
|
effects.spawn(SimpleStatBonus::<AmmoControl>::new("MTH3330", 0.20), weapon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if education.his2160 && japanese {
|
||||||
|
effects.spawn(SimpleStatBonus::<DamageBonus>::new("HIS2160", 0.10), weapon);
|
||||||
|
}
|
||||||
|
|
||||||
|
if mastery > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new("mastery", (mastery as f32) * 0.2 / 50.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("mastery", (mastery as f32) / 100.0),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if faction.dmg > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<DamageBonus>::new("faction", (faction.dmg as f32) * 0.01),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if faction.acc > 0 {
|
||||||
|
effects.spawn(
|
||||||
|
SimpleStatBonus::<WeaponAccuracy>::new(
|
||||||
|
"faction",
|
||||||
|
(faction.dmg as f32) * 0.2 / 50.0,
|
||||||
|
),
|
||||||
|
weapon,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unset_current(weapon_q: Query<Entity, (With<Current>, With<Weapon>)>, mut commands: Commands) {
|
||||||
|
for weapon in weapon_q.iter() {
|
||||||
|
commands.entity(weapon).remove::<Current>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Move all mitigation aspects of this into the player system
|
||||||
|
pub fn use_damaging_weapon(
|
||||||
|
mut rng: ResMut<Rng>,
|
||||||
|
weapon_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&DamageStat,
|
||||||
|
&SimpleStatEffective<WeaponAccuracy>,
|
||||||
|
&SimpleStatEffective<DamageBonus>,
|
||||||
|
&SimpleStatEffective<CritRate>,
|
||||||
|
Has<NonTargeted>,
|
||||||
|
),
|
||||||
|
(With<Weapon>, With<Current>, Without<NeedsReload>),
|
||||||
|
>,
|
||||||
|
player_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&EffectiveStat<Speed>,
|
||||||
|
&SimpleStatEffective<CritRate>,
|
||||||
|
&SimpleStatEffective<WeaponAccuracy>,
|
||||||
|
&SimpleStatEffective<DamageBonus>,
|
||||||
|
),
|
||||||
|
(With<Player>, With<Current>),
|
||||||
|
>,
|
||||||
|
target_q: Query<(Entity, &EffectiveStat<Dexterity>), With<CurrentTarget>>,
|
||||||
|
(mut ammo_q, mut temp_q): (
|
||||||
|
Query<(
|
||||||
|
&mut Ammo,
|
||||||
|
&SimpleStatEffective<Clips>,
|
||||||
|
&RateOfFire,
|
||||||
|
&SimpleStatEffective<AmmoControl>,
|
||||||
|
)>,
|
||||||
|
Query<&mut Uses>,
|
||||||
|
),
|
||||||
|
mut hit_events: EventWriter<InitiateHit>,
|
||||||
|
(mut logger, mut commands, metrics): (Logger, Commands, Res<Metrics>),
|
||||||
|
) {
|
||||||
|
let Ok((weapon, dmg, acc, dmg_bonus, crit, non_targeted)) = weapon_q.get_single() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let (player, player_spd, player_crit, acc_bonus, p_dmg_bonus) = player_q.single();
|
||||||
|
let (target, target_dex) = target_q.single();
|
||||||
|
|
||||||
|
if let Ok(mut uses) = temp_q.get_mut(weapon) {
|
||||||
|
uses.0 -= 1;
|
||||||
|
if uses.0 == 0 {
|
||||||
|
commands.entity(weapon).remove::<Usable>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let spd_dex_ratio = (player_spd.value / target_dex.value).clamp(1.0 / 64.0, 64.0);
|
||||||
|
let base_hit_chance = if spd_dex_ratio < 1.0 {
|
||||||
|
0.5 * (8.0 * spd_dex_ratio.sqrt() - 1.0) / 7.0
|
||||||
|
} else {
|
||||||
|
1.0 - 0.5 * (8.0 / spd_dex_ratio.sqrt() - 1.0) / 7.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut acc_eff = acc + acc_bonus;
|
||||||
|
|
||||||
|
let mut ammo = ammo_q
|
||||||
|
.get_mut(weapon)
|
||||||
|
.ok()
|
||||||
|
.map(|(ammo, clips, rof, ammo_ctrl)| {
|
||||||
|
let ammo_ctrl = 1.0 - (ammo_ctrl).value;
|
||||||
|
let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..((rof.0[1] as f32) * ammo_ctrl);
|
||||||
|
(ammo, clips, rof_eff)
|
||||||
|
});
|
||||||
|
|
||||||
|
enum MultiAttack {
|
||||||
|
Blindfire,
|
||||||
|
Rage(usize),
|
||||||
|
Fury(usize),
|
||||||
|
DoubleTap { fired_first: bool },
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut multi_attack_proc = None;
|
||||||
|
let crit = player_crit + crit;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| {
|
||||||
|
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0);
|
||||||
|
metrics.increment_counter(player, "rounds_fired", rounds.into());
|
||||||
|
metrics.increment_counter(weapon, "rounds_fired", rounds.into());
|
||||||
|
ammo.0 -= rounds;
|
||||||
|
if ammo.0 == 0 {
|
||||||
|
if clips.value == 0 {
|
||||||
|
commands.entity(weapon).remove::<Usable>();
|
||||||
|
} else {
|
||||||
|
commands.entity(weapon).insert(NeedsReload);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rounds
|
||||||
|
});
|
||||||
|
|
||||||
|
let hit_chance = if base_hit_chance < 0.5 {
|
||||||
|
base_hit_chance + acc_eff.value * base_hit_chance
|
||||||
|
} else {
|
||||||
|
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
|
||||||
|
};
|
||||||
|
|
||||||
|
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||||
|
logger.log(|| MissTarget {
|
||||||
|
weapon,
|
||||||
|
actor: player,
|
||||||
|
recipient: target,
|
||||||
|
rounds,
|
||||||
|
});
|
||||||
|
metrics.increment_counter(player, "miss", 1);
|
||||||
|
metrics.increment_counter(weapon, "miss", 1);
|
||||||
|
|
||||||
|
if multi_attack_proc.is_none() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
let body_part = if !non_targeted {
|
||||||
|
rng.sample(crit)
|
||||||
|
} else {
|
||||||
|
BodyPart::Stomach
|
||||||
|
};
|
||||||
|
|
||||||
|
hit_events.send(InitiateHit {
|
||||||
|
body_part,
|
||||||
|
weapon,
|
||||||
|
rounds,
|
||||||
|
dmg: dmg.0,
|
||||||
|
dmg_bonus_weapon: dmg_bonus.value,
|
||||||
|
dmg_bonus_player: p_dmg_bonus.value,
|
||||||
|
hit_chance,
|
||||||
|
crit_rate: crit.value,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
match multi_attack_proc {
|
||||||
|
Some(MultiAttack::Blindfire) => acc_eff.value -= 5.0 / 50.0,
|
||||||
|
Some(MultiAttack::Rage(turns @ 1..)) => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::Rage(turns - 1))
|
||||||
|
}
|
||||||
|
Some(MultiAttack::Fury(turns @ 1..)) => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::Fury(turns - 1))
|
||||||
|
}
|
||||||
|
Some(MultiAttack::DoubleTap { fired_first: false }) => {
|
||||||
|
multi_attack_proc = Some(MultiAttack::DoubleTap { fired_first: true })
|
||||||
|
}
|
||||||
|
_ => break,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
let hit_chance = if base_hit_chance < 0.5 {
|
||||||
|
base_hit_chance + acc_eff.value * base_hit_chance
|
||||||
|
} else {
|
||||||
|
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
|
||||||
|
};
|
||||||
|
|
||||||
|
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||||
|
logger.log(|| MissTarget {
|
||||||
|
weapon,
|
||||||
|
actor: player,
|
||||||
|
recipient: target,
|
||||||
|
rounds: rounds.map(|(rounds, _)| rounds),
|
||||||
|
});
|
||||||
|
metrics.increment_counter(player, "miss", 1);
|
||||||
|
metrics.increment_counter(weapon, "miss", 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let crit = player_crit + crit;
|
||||||
|
|
||||||
|
let body_part = if !non_targeted {
|
||||||
|
rng.sample(crit)
|
||||||
|
} else {
|
||||||
|
BodyPart::Stomach
|
||||||
|
};
|
||||||
|
|
||||||
|
let def_str_ratio = (target_def.value / player_str.value).clamp(1.0 / 32.0, 14.0);
|
||||||
|
let mitigation = if def_str_ratio < 1.0 {
|
||||||
|
0.5 * def_str_ratio.log(32.0) + 0.5
|
||||||
|
} else {
|
||||||
|
0.5 * def_str_ratio.log(14.0) + 0.5
|
||||||
|
};
|
||||||
|
|
||||||
|
let dmg_intrinsic = 7.0 * (player_str.value / 10.0).log10().powi(2)
|
||||||
|
+ 27.0 * (player_str.value / 10.0).log10()
|
||||||
|
+ 30.0;
|
||||||
|
|
||||||
|
init_hit.send(InitiateHit {
|
||||||
|
body_part,
|
||||||
|
weapon,
|
||||||
|
rounds: rounds.map(|(rounds, _)| rounds),
|
||||||
|
crit_rate: crit.value,
|
||||||
|
dmg: dmg.0,
|
||||||
|
dmg_bonus_weapon: (dmg_bonus + p_dmg_bonus).value,
|
||||||
|
dmg_intrinsic,
|
||||||
|
def_mitigation: mitigation,
|
||||||
|
hit_chance,
|
||||||
|
}); */
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reload_weapon(
|
||||||
|
mut weapon_q: Query<
|
||||||
|
(
|
||||||
|
Entity,
|
||||||
|
&Parent,
|
||||||
|
&mut Ammo,
|
||||||
|
&mut SimpleStatEffective<Clips>,
|
||||||
|
&SimpleStatEffective<ClipSize>,
|
||||||
|
),
|
||||||
|
(With<NeedsReload>, With<Current>),
|
||||||
|
>,
|
||||||
|
mut commands: Commands,
|
||||||
|
mut logger: Logger,
|
||||||
|
) {
|
||||||
|
for (weapon, player, mut ammo, mut clips, clip_size) in weapon_q.iter_mut() {
|
||||||
|
ammo.0 = clip_size.value;
|
||||||
|
clips.value -= 1;
|
||||||
|
|
||||||
|
logger.log(|| ReloadWeapon {
|
||||||
|
actor: player.get(),
|
||||||
|
weapon,
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.entity(weapon).remove::<NeedsReload>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_usability(weapon_q: Query<Entity, With<Weapon>>, mut commands: Commands) {
|
||||||
|
for weapon in weapon_q.iter() {
|
||||||
|
commands.entity(weapon).insert(Usable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_ammo(
|
||||||
|
mut ammo_q: Query<(Entity, &mut Ammo, &SimpleStatEffective<ClipSize>)>,
|
||||||
|
mut commands: Commands,
|
||||||
|
) {
|
||||||
|
for (weapon, mut ammo, clip_size) in ammo_q.iter_mut() {
|
||||||
|
ammo.0 = clip_size.value;
|
||||||
|
commands.entity(weapon).remove::<NeedsReload>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn apply_first_turn_effects(
|
||||||
|
effect_q: Query<(&Parent, &FirstTurnEffect)>,
|
||||||
|
player_q: Query<&Parent>,
|
||||||
|
mut effects: Effects,
|
||||||
|
) {
|
||||||
|
for (weapon, effect) in effect_q.iter() {
|
||||||
|
let player = player_q.get(weapon.get()).unwrap();
|
||||||
|
effect.spawn(&mut effects, weapon.get(), player.get());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.equip.add_systems(set_owner);
|
||||||
|
stages.pre_fight.add_systems((
|
||||||
|
apply_passives,
|
||||||
|
apply_first_turn_effects
|
||||||
|
.after(apply_passives)
|
||||||
|
.after(bonus::prepare_bonuses),
|
||||||
|
));
|
||||||
|
stages.turn.add_systems(reload_weapon);
|
||||||
|
stages.post_turn.add_systems(unset_current);
|
||||||
|
stages
|
||||||
|
.restore
|
||||||
|
.add_systems((restore_ammo, restore_usability, apply_first_turn_effects));
|
||||||
|
|
||||||
|
temp::configure(stages);
|
||||||
|
bonus::configure(stages);
|
||||||
|
}
|
||||||
249
src/weapon/temp.rs
Normal file
249
src/weapon/temp.rs
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
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 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let target = target_q.single();
|
||||||
|
|
||||||
|
match temp {
|
||||||
|
DebuffingTemp::TearGas => effects.spawn_and_insert(
|
||||||
|
TempDebuffEffect::<TearGas>::default(),
|
||||||
|
target,
|
||||||
|
AssociatedWeapon(weapon),
|
||||||
|
),
|
||||||
|
DebuffingTemp::SmokeGrenade => effects.spawn_and_insert(
|
||||||
|
TempDebuffEffect::<SmokeGrenade>::default(),
|
||||||
|
target,
|
||||||
|
AssociatedWeapon(weapon),
|
||||||
|
),
|
||||||
|
DebuffingTemp::PepperSpray => effects.spawn_and_insert(
|
||||||
|
TempDebuffEffect::<PepperSpray>::default(),
|
||||||
|
target,
|
||||||
|
AssociatedWeapon(weapon),
|
||||||
|
),
|
||||||
|
DebuffingTemp::ConcussionGrenade => effects.spawn_and_insert(
|
||||||
|
TempDebuffEffect::<ConcussionGrenade>::default(),
|
||||||
|
target,
|
||||||
|
AssociatedWeapon(weapon),
|
||||||
|
),
|
||||||
|
DebuffingTemp::FlashGrenade => effects.spawn_and_insert(
|
||||||
|
TempDebuffEffect::<FlashGrenade>::default(),
|
||||||
|
target,
|
||||||
|
AssociatedWeapon(weapon),
|
||||||
|
),
|
||||||
|
DebuffingTemp::Sand => effects.spawn_and_insert(
|
||||||
|
TempDebuffEffect::<Sand>::default(),
|
||||||
|
target,
|
||||||
|
AssociatedWeapon(weapon),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
uses.0 -= 1;
|
||||||
|
if uses.0 == 0 {
|
||||||
|
commands.entity(weapon).remove::<Usable>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn restore_uses(mut uses_q: Query<&mut Uses>) {
|
||||||
|
for mut uses in uses_q.iter_mut() {
|
||||||
|
uses.0 = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn configure(stages: &mut Stages) {
|
||||||
|
stages.turn.add_systems(use_debuffing_temp);
|
||||||
|
stages.restore.add_systems(restore_uses);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue