initial commit

This commit is contained in:
TotallyNot 2023-12-31 21:26:43 +01:00
commit 86f9333aec
21 changed files with 6449 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
/target
.DS_Store
Cargo.lock

23
Cargo.toml Normal file
View 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
View 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
View 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
View 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
View file

@ -0,0 +1,2 @@
[toolchain]
channel = "nightly"

284
src/armour/mod.rs Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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);
}