diff --git a/Cargo.toml b/Cargo.toml index 861b235..7d1531e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ json = ["dep:serde", "dep:serde_json"] [dependencies] bevy_ecs = { version = "0.17.2", features = [] } +bevy_utils = { version = "0.17.2", features = ["debug"] } +bevy_reflect = { version = "0.17.2", features = ["debug"] } rand = { version = "0.9.2", default-features = false, features = [ "std", "alloc", diff --git a/models/src/bundle/bonus.rs b/models/src/bundle/bonus.rs index c641158..e5f60a3 100644 --- a/models/src/bundle/bonus.rs +++ b/models/src/bundle/bonus.rs @@ -183,7 +183,7 @@ impl BonusPartDamageBonus { } } -#[derive(Clone, Copy, Debug, Display)] +#[derive(Clone, Copy, Debug, PartialEq, Eq, Component, Display)] #[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "json", serde(rename_all = "snake_case"))] pub enum ArmourBonusType { diff --git a/models/src/bundle/player.rs b/models/src/bundle/player.rs index 6a2fe6c..871d8b2 100644 --- a/models/src/bundle/player.rs +++ b/models/src/bundle/player.rs @@ -5,7 +5,7 @@ use crate::bundle::{ Name, bonus::BonusPartDamageBonus, passive::EducationPartDamageBonus, - stat::{CritRate, DamageBonus, SimpleStatBundle, WeaponAccuracy}, + stat::{CritRate, DamageBonus, MaxHealth, SimpleStatBundle, WeaponAccuracy}, weapon::WeaponSlot, }; @@ -37,13 +37,7 @@ impl Default for Level { } #[derive(Component, Debug)] -pub struct MaxHealth(pub u16); - -impl Default for MaxHealth { - fn default() -> Self { - Self(100) - } -} +pub struct Health(pub u16); #[derive(Component, Debug, Default)] pub struct CombatTurns(pub u16); @@ -158,6 +152,7 @@ pub struct PlayerBundle { pub name: Name, pub player: Player, pub level: Level, + pub max_health: SimpleStatBundle, pub crit_rate: SimpleStatBundle, // 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 @@ -174,6 +169,12 @@ impl PlayerBundle { name: Name(name.to_string()), player: Player, level: Level(level), + max_health: SimpleStatBundle::new(match level { + 1..=8 => 100 + (level - 1) * 25, + 9..=95 => 275 + (level - 8) * 50, + 96.. => 4625 + (level - 95) * 75, + 0 => unreachable!(), + }), crit_rate: SimpleStatBundle::new(24), acc_bonus: SimpleStatBundle::new(0.0), dmg_bonus: SimpleStatBundle::new(0.0), diff --git a/models/src/bundle/stat.rs b/models/src/bundle/stat.rs index 5c34c8a..7eb8ab5 100644 --- a/models/src/bundle/stat.rs +++ b/models/src/bundle/stat.rs @@ -226,9 +226,23 @@ impl SimpleStatMarker for Clips { } #[derive(Default)] -pub struct Health; +pub struct MaxHealth; -impl SimpleStatMarker for Health { +impl SimpleStatMarker for MaxHealth { + type ValueType = u16; + type BonusType = f32; + fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { + ((value as f32) * bonus) as u16 + } + fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { + ((value as f32) / bonus) as u16 + } +} + +#[derive(Default)] +pub struct ArmourBonusValue; + +impl SimpleStatMarker for ArmourBonusValue { type ValueType = u16; type BonusType = u16; fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { diff --git a/models/src/dto/armour.rs b/models/src/dto/armour.rs index cf052c8..305f491 100644 --- a/models/src/dto/armour.rs +++ b/models/src/dto/armour.rs @@ -8,6 +8,7 @@ use crate::{ Name, armour::{Armour, ArmourCoverage, ArmourValue, Immunities, Immunity}, bonus::ArmourBonusType, + stat::{ArmourBonusValue, SimpleStatBundle}, }, dto::draw_id, }; @@ -27,7 +28,7 @@ pub enum ArmourSlot { #[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))] pub struct ArmourBonusInfo { pub kind: ArmourBonusType, - pub value: Option, + pub value: Option, } #[derive(Debug, Clone)] @@ -60,7 +61,7 @@ pub struct ArmourDto { } impl ArmourDto { - pub fn new(name: &str, armour: Option, bonus: Option) -> Option { + pub fn new(name: &str, armour: Option, bonus: Option) -> Option { let base = match name { "Leather Vest" => Self::LEATHER_VEST, "Police Vest" => Self::POLICE_VEST, @@ -197,6 +198,13 @@ impl ArmourDto { Immunities(self.immunities.to_vec()), )); + if let Some(bonus) = self.bonus { + commands.insert(( + bonus.kind, + SimpleStatBundle::::new(bonus.value.unwrap_or_default()), + )); + } + commands.id() } diff --git a/models/src/dto/metrics.rs b/models/src/dto/metrics.rs index 1ced5a2..f2cf76d 100644 --- a/models/src/dto/metrics.rs +++ b/models/src/dto/metrics.rs @@ -12,6 +12,11 @@ pub enum EntityInfo { owner: usize, id: usize, }, + Armour { + name: String, + owner: usize, + id: usize, + }, Global, } diff --git a/src/armour/mod.rs b/src/armour/mod.rs index 5b8fd54..32f9ee3 100644 --- a/src/armour/mod.rs +++ b/src/armour/mod.rs @@ -1,27 +1,55 @@ +use std::collections::HashMap; + use bevy_ecs::prelude::*; -use proxisim_models::bundle::armour::{ - ArmourBodyPart, ArmourBodyPartSlot, ArmourBodyParts, ArmourCoverage, ArmourValue, ArmourVec, - BodyPartCoverage, Immunities, Immunity, PlayerArmour, +use proxisim_models::bundle::{ + armour::{ + ArmourBodyPart, ArmourBodyPartSlot, ArmourBodyParts, ArmourCoverage, ArmourValue, + ArmourVec, BodyPartCoverage, Immunities, Immunity, PlayerArmour, + }, + bonus::ArmourBonusType, + stat::{ + AdditiveBonus, ArmourBonusValue, Defence, Dexterity, MaxHealth, SimpleStatBonus, + SimpleStatEffective, + }, }; use strum::IntoEnumIterator; use crate::{ Stages, + effect::Effects, player::status_effect::{ ConcussionGrenade, FlashGrenade, PepperSpray, TearGas, TempDebuffImmunity, }, }; -fn generate_body_parts( +fn equip_armour( equip_q: Query<(Entity, &PlayerArmour)>, - armour_q: Query<(Entity, &ArmourCoverage, &ArmourValue, Option<&Immunities>)>, + armour_q: Query<( + Entity, + &ArmourCoverage, + &ArmourValue, + Option<&Immunities>, + Option<&ArmourBonusType>, + )>, mut commands: Commands, + mut effects: Effects, ) { for (player, equipped_armour) in equip_q.iter() { let mut parts = ArmourVec::::default(); - for (armour, coverage, armour_value, immunities) in armour_q.iter_many(equipped_armour) { - // commands.entity(player).add_child(armour); + let mut set = Some(None); + + for (armour, coverage, armour_value, immunities, bonus) in + armour_q.iter_many(equipped_armour) + { + if let Some(kind) = &mut set + && let Some(bonus) = bonus + && kind.is_none_or(|k| k == *bonus) + { + set = Some(Some(*bonus)); + } else { + set = None + } if let Some(immunities) = immunities { let mut player = commands.entity(player); @@ -62,6 +90,28 @@ fn generate_body_parts( } } + if let Some(Some(set)) = set { + for piece in equipped_armour { + effects.spawn( + SimpleStatBonus::::new( + "set bonus", + match set { + ArmourBonusType::Impregnable => 10, + ArmourBonusType::Impenetrable => 10, + ArmourBonusType::Insurmountable => 15, + ArmourBonusType::Invulnerable => 15, + ArmourBonusType::Imperviable => 1, + ArmourBonusType::Immmutable => 5, + ArmourBonusType::Irrepressible => 5, + ArmourBonusType::Impassable => 10, + ArmourBonusType::Kinetokinesis => 0, + }, + ), + piece, + ); + } + } + let parts = parts.0.map(|p| commands.spawn(p).id()); commands @@ -71,6 +121,45 @@ fn generate_body_parts( } } -pub(crate) fn configure(stages: &mut Stages) { - stages.equip.add_systems(generate_body_parts); +fn apply_passives( + bonus_q: Query<( + &ArmourBonusType, + &SimpleStatEffective, + &ChildOf, + )>, + mut effects: Effects, +) { + let mut max_health_increase = HashMap::::new(); + for (kind, value, relation) in bonus_q { + match kind { + ArmourBonusType::Imperviable => { + *max_health_increase.entry(relation.parent()).or_default() += value.value; + } + ArmourBonusType::Immmutable => { + effects.spawn( + AdditiveBonus::::new("immutable", value.value as f32 / 100.0), + relation.parent(), + ); + } + ArmourBonusType::Irrepressible => { + effects.spawn( + AdditiveBonus::::new("irrepressible", value.value as f32 / 100.0), + relation.parent(), + ); + } + _ => (), + } + } + + for (target, increase) in max_health_increase { + effects.spawn( + SimpleStatBonus::::new("marauder", (increase as f32) * 0.01 + 1.0), + target, + ); + } +} + +pub(crate) fn configure(stages: &mut Stages) { + stages.equip.add_systems(equip_armour); + stages.passives.add_systems(apply_passives); } diff --git a/src/attacker.json b/src/attacker.json index 62d2173..06c2dce 100644 --- a/src/attacker.json +++ b/src/attacker.json @@ -10,10 +10,9 @@ "strategy": { "type": "in_order", "order": [ - "temporary", - "primary" + "secondary" ], - "reload": true + "reload": false }, "education": { "bio2350": true, @@ -86,55 +85,47 @@ "side_effects": 10 }, "weapons": { - "primary": { - "id": 488, - "name": "MP 40", - "kind": "primary", - "cat": "smg", - "base_dmg": 37, - "base_acc": 41, - "dmg": 5, - "acc": 41, + "primary": null, + "secondary": { + "id": 490, + "name": "Blunderbuss", + "kind": "secondary", + "cat": "shotgun", + "base_dmg": 46, + "base_acc": 24, + "dmg": 46, + "acc": 24, "ammo": { - "clip_size": 32, + "clip_size": 1, "rate_of_fire": [ - 3, - 5 + 1, + 1 ] }, "mods": [ - "high_capacity_mags", + null, null ], "bonuses": [ { - "bonus": "specialist", - "value": 10 + "bonus": "bleed", + "value": 100 }, null ], "compatible_mods": [ - "reflex_sight", - "holographic_sight", - "acog_sight", - "thermal_sight", "laser1mw", "laser5mw", "laser30mw", "laser100mw", - "small_suppressor", - "standard_suppressor", - "large_suppressor", - "extended_mags", - "high_capacity_mags", "extra_clip", "extra_clip2", "adjustable_trigger", "hair_trigger", "custom_grip", - "standard_brake", - "heavy_duty_brake", - "tactical_brake", + "skeet_choke", + "improved_choke", + "full_choke", "small_light", "precision_light", "tactical_illuminator" @@ -142,36 +133,138 @@ "experience": 0, "japanese": false }, - "secondary": null, "melee": null, - "temporary": { - "id": 464, - "name": "Melatonin", - "kind": "temporary", - "cat": "temporary", - "base_dmg": 0, - "base_acc": 0, - "dmg": 0, - "acc": 0, - "ammo": null, - "mods": [ - null, - null - ], - "bonuses": [ - null, - null - ], - "compatible_mods": [], - "experience": 0, - "japanese": false - } + "temporary": null }, "armour": { - "helmet": null, - "body": null, - "pants": null, - "gloves": null, - "boots": null + "helmet": { + "slot": "head", + "id": 1355, + "name": "Vanguard Respirator", + "base_armour": 48, + "armour": 48, + "coverage": { + "body": 2.819999933242798, + "heart": 0, + "stomach": 0, + "chest": 0, + "arm": 0, + "groin": 0, + "leg": 0, + "throat": 0, + "hand": 0, + "foot": 0, + "head": 39.599998474121094 + }, + "immunities": [ + "pepper_spray", + "nerve_gas", + "tear_gas" + ], + "bonus": { + "kind": "irrepressible", + "value": null + } + }, + "body": { + "slot": "body", + "id": 1356, + "name": "Vanguard Body", + "base_armour": 48, + "armour": 48, + "coverage": { + "body": 44.279998779296875, + "heart": 100, + "stomach": 100, + "chest": 100, + "arm": 100, + "groin": 35.38999938964844, + "leg": 0.2800000011920929, + "throat": 83.52999877929688, + "hand": 0.12999999523162842, + "foot": 0, + "head": 0 + }, + "immunities": [], + "bonus": { + "kind": "irrepressible", + "value": null + } + }, + "pants": { + "slot": "legs", + "id": 1357, + "name": "Vanguard Pants", + "base_armour": 48, + "armour": 48, + "coverage": { + "body": 24.959999084472656, + "heart": 0, + "stomach": 0, + "chest": 0, + "arm": 0, + "groin": 99.06999969482422, + "leg": 100, + "throat": 0, + "hand": 0, + "foot": 25.200000762939453, + "head": 0 + }, + "immunities": [], + "bonus": { + "kind": "irrepressible", + "value": null + } + }, + "gloves": { + "slot": "hands", + "id": 1359, + "name": "Vanguard Gloves", + "base_armour": 48, + "armour": 48, + "coverage": { + "body": 14.399999618530273, + "heart": 0, + "stomach": 0, + "chest": 0, + "arm": 0.7300000190734863, + "groin": 0, + "leg": 0, + "throat": 0, + "hand": 100, + "foot": 0, + "head": 0 + }, + "immunities": [], + "bonus": { + "kind": "irrepressible", + "value": null + } + }, + "boots": { + "slot": "feet", + "id": 1358, + "name": "Vanguard Boots", + "base_armour": 48, + "armour": 48, + "coverage": { + "body": 15.130000114440918, + "heart": 0, + "stomach": 0, + "chest": 0, + "arm": 0, + "groin": 0, + "leg": 5.829999923706055, + "throat": 0, + "hand": 0, + "foot": 100, + "head": 0 + }, + "immunities": [], + "bonus": { + "kind": "irrepressible", + "value": null + } + } } } diff --git a/src/entity_registry.rs b/src/entity_registry.rs index 1604260..98b9ecf 100644 --- a/src/entity_registry.rs +++ b/src/entity_registry.rs @@ -4,6 +4,7 @@ use bevy_ecs::prelude::*; use proxisim_models::{ bundle::{ Id, Name, + armour::Armour, player::{Attacker, Player}, weapon::Weapon, }, @@ -18,6 +19,7 @@ pub struct EntityRegistry(pub HashMap); fn read_entities( player_q: Query<(Entity, &Name, &Id, Has), With>, weapon_q: Query<(Entity, &ChildOf, &Name, &Id), With>, + armour_q: Query<(Entity, &ChildOf, &Name, &Id), With>, mut registry: ResMut, ) { for (player, name, id, is_attacker) in player_q.iter() { @@ -42,6 +44,18 @@ fn read_entities( }, ); } + + for (weapon, player, name, id) in armour_q.iter() { + let (_, _, player_id, _) = player_q.get(player.parent()).unwrap(); + registry.0.insert( + weapon, + EntityInfo::Armour { + name: name.0.clone(), + owner: player_id.0, + id: id.0, + }, + ); + } } pub(crate) fn configure(stages: &mut Stages) { diff --git a/src/lib.rs b/src/lib.rs index 05b16e9..88aa180 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,7 @@ #![warn(clippy::perf, clippy::style, clippy::all)] #![allow(clippy::type_complexity)] use bevy_ecs::{message::MessageRegistry, prelude::*, schedule::ScheduleLabel}; -use effect::{register_effect, EffectBuilder}; +use effect::{EffectBuilder, register_effect}; use metrics::Metrics; use proxisim_models::{ bundle::player::{Attacker, Current, Defender}, @@ -53,6 +53,7 @@ enum FightStatus { enum Stage { Equip, Snapshot, + Passives, PreFight, PreTurn, Turn, @@ -64,6 +65,7 @@ enum Stage { struct Stages { equip: Schedule, snapshot: Schedule, + passives: Schedule, pre_fight: Schedule, pre_turn: Schedule, turn: Schedule, @@ -102,6 +104,7 @@ impl Simulation { let mut stages = Stages { equip: Schedule::new(Stage::Equip), snapshot: Schedule::new(Stage::Snapshot), + passives: Schedule::new(Stage::Passives), pre_fight: Schedule::new(Stage::PreFight), pre_turn: Schedule::new(Stage::PreTurn), turn: Schedule::new(Stage::Turn), @@ -134,8 +137,10 @@ impl Simulation { 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.passives.run(&mut stages.world); + effect::run_effects(&mut stages.world); + stages.pre_fight.run(&mut stages.world); stages.snapshot.run(&mut stages.world); Self(stages) diff --git a/src/log.rs b/src/log.rs index c6652a4..0026d02 100644 --- a/src/log.rs +++ b/src/log.rs @@ -3,9 +3,10 @@ use std::sync::Mutex; use bevy_ecs::{prelude::*, query::QuerySingleError, system::SystemParam}; use proxisim_models::bundle::{ stat::{ - AdditiveBonus, BaselineStat, ClipSize, Clips, CritRate, DamageBonus, Defence, Dexterity, - EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, - SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy, + AdditiveBonus, ArmourBonusValue, BaselineStat, ClipSize, Clips, CritRate, DamageBonus, + Defence, Dexterity, EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, + SimpleStatBonus, SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength, + WeaponAccuracy, }, weapon::WeaponVerb, }; @@ -124,7 +125,12 @@ where V: Into, { fn from(value: Vec<(&'static str, V)>) -> Self { - LogValue::Map(value.into_iter().map(|(k, v)| (k, v.into())).collect()) + LogValue::Array( + value + .into_iter() + .map(|(k, v)| LogValue::Array(vec![LogValue::String(k.to_string()), v.into()])) + .collect(), + ) } } @@ -256,182 +262,6 @@ impl std::fmt::Display for Log { } } -/* 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: MessageWriter<'w, LogEvent>, @@ -603,6 +433,7 @@ pub(crate) fn configure(stages: &mut Stages) { log_simple_stat_changes::, log_simple_stat_changes::, log_simple_stat_changes::, + log_simple_stat_changes::, ) .run_if(logging_enabled), ); diff --git a/src/passives.rs b/src/passives.rs index 8f7c228..e135e01 100644 --- a/src/passives.rs +++ b/src/passives.rs @@ -1,10 +1,12 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ passive::{DrugCooldown, Education, FactionUpgrades, Merits}, - stat::{AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength}, + stat::{ + AdditiveBonus, CritRate, Defence, Dexterity, MaxHealth, SimpleStatBonus, Speed, Strength, + }, }; -use crate::{effect::Effects, Stages}; +use crate::{Stages, effect::Effects}; fn spawn_permanent_effects( merit_q: Query<( @@ -50,6 +52,12 @@ fn spawn_permanent_effects( player, ); } + if merits.life > 0 { + effects.spawn( + SimpleStatBonus::::new("merits", (merits.life as f32) * 0.05 + 1.0), + player, + ); + } if faction.spd > 0 { effects.spawn( @@ -75,6 +83,12 @@ fn spawn_permanent_effects( player, ); } + if faction.life > 0 { + effects.spawn( + SimpleStatBonus::::new("faction", (faction.life as f32) * 0.01 + 1.0), + player, + ); + } #[allow(clippy::too_many_arguments)] fn spawn_drug_bonuses( diff --git a/src/player/mod.rs b/src/player/mod.rs index 4799998..480f726 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -1,14 +1,13 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ armour::{ArmourBodyPart, ArmourBodyParts}, - passive::{FactionUpgrades, Merits}, player::{ Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender, - FightEndType, Level, MaxHealth, PartDamageBonus, Player, PlayerStrategy, Weapons, + FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons, }, stat::{ - AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health, - SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy, + AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth, + SimpleStatEffective, Speed, Strength, WeaponAccuracy, }, weapon::{ Ammo, DamageStat, NeedsReload, NonTargeted, RateOfFire, Usable, Uses, Weapon, WeaponSlot, @@ -22,6 +21,7 @@ use crate::{ log, log::Logger, metrics::Metrics, + player::status_effect::{Bleed, DamageOverTime, DamageOverTimeType, DeferredDamage}, weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus}, }; @@ -52,28 +52,6 @@ fn select_weapon<'a>( } } -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::::new(max_health), - )); - } -} - fn designate_first( attacker_q: Query>, defender_q: Query>, @@ -183,7 +161,7 @@ pub fn use_damaging_weapon( ), (With, With, Without), >, - player_q: Query< + mut player_q: Query< ( Entity, &EffectiveStat, @@ -191,6 +169,8 @@ pub fn use_damaging_weapon( &SimpleStatEffective, &SimpleStatEffective, &SimpleStatEffective, + &Children, + &mut Health, Has, ), (With, With), @@ -201,11 +181,11 @@ pub fn use_damaging_weapon( &EffectiveStat, &EffectiveStat, &ArmourBodyParts, - &mut SimpleStatEffective, + &mut Health, ), - With, + (With, Without), >, - armour_q: Query<&ArmourBodyPart>, + (armour_q, damage_q): (Query<&ArmourBodyPart>, Query<(Entity, &DeferredDamage)>), (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), (mut ammo_q, mut temp_q): ( Query<( @@ -228,10 +208,48 @@ pub fn use_damaging_weapon( else { return; }; - let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) = - player_q.single().unwrap(); + let ( + player, + player_spd, + player_str, + player_crit, + acc_bonus, + p_dmg_bonus, + p_children, + mut p_health, + attacker, + ) = player_q.single_mut().unwrap(); let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut().unwrap(); + for (instance, damage) in damage_q.iter_many(p_children) { + let health_before = p_health.0; + p_health.0 = p_health.0.saturating_sub(damage.amount); + + log!(logger, "deferred_damage", { + health_before: health_before, + health_after: p_health.0, + amount: damage.amount, + label: damage.label, + target: player, + }); + + commands.entity(instance).despawn(); + + if p_health.0 == 0 { + log!(logger, "fight_end", { + actor: target, + recipient: player, + fight_end_type: %if attacker { + FightEndType::Loss + } else { + FightEndType::Victory + }, + }); + metrics.increment_counter(Some(target), "victory", 1); + return; + } + } + if let Ok(mut uses) = temp_q.get_mut(weapon) { uses.0 -= 1; if uses.0 == 0 { @@ -253,7 +271,7 @@ pub fn use_damaging_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); + let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..=((rof.0[1] as f32) * ammo_ctrl); (ammo, clips, rof_eff) }); @@ -414,13 +432,25 @@ pub fn use_damaging_weapon( bonus.spawn(target, &mut effects, &mut rng.0); } } + DamageProcEffect::DamageOverTimer { value, kind } => { + let chance = (value / 100.0) as f64; + if chance > 1.0 || rng.random_bool(chance) { + match kind { + DamageOverTimeType::Bleed => { + commands + .entity(target) + .insert(DamageOverTime::::new(dmg)); + } + } + } + } } } } - let health_before = health.value; + let health_before = health.0; - health.value = health.value.saturating_sub(dmg as u16); + health.0 = health.0.saturating_sub(dmg as u16); log!(logger, "hit_target", { actor: player, @@ -431,7 +461,7 @@ pub fn use_damaging_weapon( part_mult: mult, rounds, health_before: health_before, - health_after: health.value, + health_after: health.0, dmg, dmg_spread, dmg_intrinsic, @@ -443,7 +473,7 @@ pub fn use_damaging_weapon( crit_rate: crit.value, }); - if health.value == 0 && !defeated { + if health.0 == 0 && !defeated { defeated = true; commands.entity(target).insert(Defeated); @@ -539,12 +569,23 @@ pub fn restore_initial_state( } } -fn record_post_fight_stats( - player_q: Query<(Entity, &SimpleStatEffective)>, - metrics: Res, -) { +fn record_post_fight_stats(player_q: Query<(Entity, &Health)>, metrics: Res) { for (player, health) in player_q.iter() { - metrics.record_histogram(Some(player), "rem_health", health.value as u32); + metrics.record_histogram(Some(player), "rem_health", health.0 as u32); + } +} + +fn restore_health( + health_q: Query<(Entity, &SimpleStatEffective)>, + mut commands: Commands, + mut logger: Logger, +) { + for (player, max_health) in health_q { + log!(logger, "initial_health", { + target: player, + max_health: max_health.value, + }); + commands.entity(player).insert(Health(max_health.value)); } } @@ -554,7 +595,7 @@ pub(crate) fn configure(stages: &mut Stages) { stages.add_event::(); stages.equip.add_systems(designate_first); - stages.pre_fight.add_systems(derive_max_health); + stages.pre_fight.add_systems(restore_health); stages.pre_turn.add_systems(pick_action); stages.turn.add_systems(use_damaging_weapon); stages diff --git a/src/player/stats.rs b/src/player/stats.rs index 6041010..13ea05a 100644 --- a/src/player/stats.rs +++ b/src/player/stats.rs @@ -2,8 +2,8 @@ use std::marker::PhantomData; use bevy_ecs::prelude::*; use proxisim_models::bundle::stat::{ - AdditiveBonus, AdditiveBonuses, AmmoControl, BaselineStat, ClipSize, Clips, CritRate, - DamageBonus, Defence, Dexterity, EffectiveStat, Health, MultiplicativeBonus, + AdditiveBonus, AdditiveBonuses, AmmoControl, ArmourBonusValue, BaselineStat, ClipSize, Clips, + CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth, MultiplicativeBonus, MultiplicativeBonuses, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker, SimpleStatSnapshot, Speed, StatMarker, StatSnapshot, Strength, WeaponAccuracy, }; @@ -196,5 +196,6 @@ pub(crate) fn configure(stages: &mut Stages) { register_simple_stat_effects::(stages); register_simple_stat_effects::(stages); register_simple_stat_effects::(stages); - register_simple_stat_effects::(stages); + register_simple_stat_effects::(stages); + register_simple_stat_effects::(stages); } diff --git a/src/player/status_effect.rs b/src/player/status_effect.rs index e4e3c5f..9a04e7d 100644 --- a/src/player/status_effect.rs +++ b/src/player/status_effect.rs @@ -1,8 +1,9 @@ use std::{collections::VecDeque, marker::PhantomData}; use bevy_ecs::prelude::*; -use proxisim_models::bundle::stat::{ - AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength, +use proxisim_models::bundle::{ + player::Current, + stat::{AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength}, }; use rand::Rng as _; @@ -340,6 +341,46 @@ impl AdditiveStatusEffectMarker<2> for Frozen { } } +pub trait DamageOverTimeMarker: Send + Sync + 'static { + const INCREMENTS: &[f32]; + const LABEL: &str; +} + +#[derive(Component)] +pub struct DamageOverTime { + marker: PhantomData, + pub base_damage: u32, + pub turn: u16, +} + +impl DamageOverTime { + pub fn new(base_damage: u32) -> Self { + Self { + marker: PhantomData, + base_damage, + turn: 0, + } + } +} + +#[derive(Component)] +pub struct DeferredDamage { + pub label: &'static str, + pub amount: u16, +} + +#[derive(Clone, Copy)] +pub enum DamageOverTimeType { + Bleed, +} + +pub struct Bleed; + +impl DamageOverTimeMarker for Bleed { + const INCREMENTS: &[f32] = &[0.45, 0.40, 0.35, 0.30, 0.25, 0.20, 0.15, 0.10, 0.05]; + const LABEL: &str = "Bleeding"; +} + fn apply_additive_status_effect>( In(entities): In>, effect_q: Query<(Entity, &ChildOf, &AdditiveStatusEffect)>, @@ -510,6 +551,34 @@ fn remove_temp_debuff_effect( } } +fn process_dot( + dot_q: Query<(Entity, &mut DamageOverTime), With>, + mut commands: Commands, + mut logger: Logger, +) { + for (player, mut dot) in dot_q { + if dot.turn as usize == Dot::INCREMENTS.len() { + commands.entity(player).remove::>(); + } else { + if dot.turn == 0 { + log!(logger, "spawned_dot", { + target: player, + base_damage: dot.base_damage, + label: Dot::LABEL, + }); + } + let factor = Dot::INCREMENTS[dot.turn as usize]; + let amount = (factor * dot.base_damage as f32) as u16; + commands.entity(player).with_child(DeferredDamage { + amount, + label: Dot::LABEL, + }); + } + + dot.turn += 1; + } +} + fn register_debuff_temp(stages: &mut Stages) { stages .register_effect::>() @@ -544,4 +613,6 @@ pub(crate) fn configure(stages: &mut Stages) { register_status_effect::<4, Motivate>(stages); register_status_effect::<4, Demoralise>(stages); register_status_effect::<2, Frozen>(stages); + + stages.post_turn.add_systems(process_dot::); } diff --git a/src/weapon/bonus.rs b/src/weapon/bonus.rs index 424d2e0..5d3642f 100644 --- a/src/weapon/bonus.rs +++ b/src/weapon/bonus.rs @@ -12,7 +12,8 @@ use crate::{ Stages, effect::{Effects, TurnLimitedEffect}, player::status_effect::{ - AdditiveStatusEffect, Crippled, Demoralise, Frozen, Motivate, Slow, Weakened, Withered, + AdditiveStatusEffect, Crippled, DamageOverTimeType, Demoralise, Frozen, Motivate, Slow, + Weakened, Withered, }, }; @@ -427,6 +428,15 @@ pub(crate) fn prepare_bonuses( }); } + WeaponBonusType::Bleed => { + commands + .entity(weapon.parent()) + .with_child(DamageProcEffect::DamageOverTimer { + value: value.0, + kind: DamageOverTimeType::Bleed, + }); + } + val => unimplemented!("{val:?}"), } } @@ -434,6 +444,6 @@ pub(crate) fn prepare_bonuses( pub(crate) fn configure(stages: &mut Stages) { stages - .pre_fight + .passives .add_systems(prepare_bonuses.after(super::apply_passives)); } diff --git a/src/weapon/mod.rs b/src/weapon/mod.rs index cfc2869..66a85cf 100644 --- a/src/weapon/mod.rs +++ b/src/weapon/mod.rs @@ -1,7 +1,7 @@ use bevy_ecs::prelude::*; use proxisim_models::bundle::{ passive::{Education, EducationPartDamageBonus, FactionUpgrades, Merits}, - player::{Current, PartDamageBonus, Weapons}, + player::{Current, PartDamageBonus}, stat::{ AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity, SimpleStatBonus, SimpleStatEffective, WeaponAccuracy, @@ -17,6 +17,7 @@ use crate::{ effect::{Effects, TurnLimitedEffect}, log, log::Logger, + player::status_effect::DamageOverTimeType, }; use self::bonus::{ @@ -152,6 +153,10 @@ pub enum DamageProcEffect { value: f32, bonus: SelfStatusEffect, }, + DamageOverTimer { + value: f32, + kind: DamageOverTimeType, + }, } fn apply_passives( @@ -520,7 +525,7 @@ pub(crate) fn configure(stages: &mut Stages) { // running this in the snapshot layer ensures that the stat increases aren't restored at the // end of the run stages.snapshot.add_systems(apply_first_turn_effects); - stages.pre_fight.add_systems(apply_passives); + stages.equip.add_systems(apply_passives); stages.turn.add_systems(reload_weapon); stages.post_turn.add_systems(unset_current); stages