feat: added armour passives

This commit is contained in:
TotallyNot 2025-11-04 13:04:56 +01:00
parent cfe2631578
commit b92ad37a5b
Signed by: pyrite
GPG key ID: 7F1BA9170CD35D15
17 changed files with 520 additions and 316 deletions

View file

@ -14,6 +14,8 @@ json = ["dep:serde", "dep:serde_json"]
[dependencies] [dependencies]
bevy_ecs = { version = "0.17.2", features = [] } 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 = [ rand = { version = "0.9.2", default-features = false, features = [
"std", "std",
"alloc", "alloc",

View file

@ -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", derive(serde::Deserialize, serde::Serialize))]
#[cfg_attr(feature = "json", serde(rename_all = "snake_case"))] #[cfg_attr(feature = "json", serde(rename_all = "snake_case"))]
pub enum ArmourBonusType { pub enum ArmourBonusType {

View file

@ -5,7 +5,7 @@ use crate::bundle::{
Name, Name,
bonus::BonusPartDamageBonus, bonus::BonusPartDamageBonus,
passive::EducationPartDamageBonus, passive::EducationPartDamageBonus,
stat::{CritRate, DamageBonus, SimpleStatBundle, WeaponAccuracy}, stat::{CritRate, DamageBonus, MaxHealth, SimpleStatBundle, WeaponAccuracy},
weapon::WeaponSlot, weapon::WeaponSlot,
}; };
@ -37,13 +37,7 @@ impl Default for Level {
} }
#[derive(Component, Debug)] #[derive(Component, Debug)]
pub struct MaxHealth(pub u16); pub struct Health(pub u16);
impl Default for MaxHealth {
fn default() -> Self {
Self(100)
}
}
#[derive(Component, Debug, Default)] #[derive(Component, Debug, Default)]
pub struct CombatTurns(pub u16); pub struct CombatTurns(pub u16);
@ -158,6 +152,7 @@ pub struct PlayerBundle {
pub name: Name, pub name: Name,
pub player: Player, pub player: Player,
pub level: Level, pub level: Level,
pub max_health: SimpleStatBundle<MaxHealth>,
pub crit_rate: SimpleStatBundle<CritRate>, pub crit_rate: SimpleStatBundle<CritRate>,
// TODO: since these two need to be tracked here anyways it might be preferable to shift all // 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 // player specific passives here instead of tracking them on the weapons
@ -174,6 +169,12 @@ impl PlayerBundle {
name: Name(name.to_string()), name: Name(name.to_string()),
player: Player, player: Player,
level: Level(level), 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), crit_rate: SimpleStatBundle::new(24),
acc_bonus: SimpleStatBundle::new(0.0), acc_bonus: SimpleStatBundle::new(0.0),
dmg_bonus: SimpleStatBundle::new(0.0), dmg_bonus: SimpleStatBundle::new(0.0),

View file

@ -226,9 +226,23 @@ impl SimpleStatMarker for Clips {
} }
#[derive(Default)] #[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 ValueType = u16;
type BonusType = u16; type BonusType = u16;
fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType {

View file

@ -8,6 +8,7 @@ use crate::{
Name, Name,
armour::{Armour, ArmourCoverage, ArmourValue, Immunities, Immunity}, armour::{Armour, ArmourCoverage, ArmourValue, Immunities, Immunity},
bonus::ArmourBonusType, bonus::ArmourBonusType,
stat::{ArmourBonusValue, SimpleStatBundle},
}, },
dto::draw_id, dto::draw_id,
}; };
@ -27,7 +28,7 @@ pub enum ArmourSlot {
#[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))] #[cfg_attr(feature = "json", derive(serde::Deserialize, serde::Serialize))]
pub struct ArmourBonusInfo { pub struct ArmourBonusInfo {
pub kind: ArmourBonusType, pub kind: ArmourBonusType,
pub value: Option<i16>, pub value: Option<u16>,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -60,7 +61,7 @@ pub struct ArmourDto {
} }
impl ArmourDto { impl ArmourDto {
pub fn new(name: &str, armour: Option<f32>, bonus: Option<i16>) -> Option<Self> { pub fn new(name: &str, armour: Option<f32>, bonus: Option<u16>) -> Option<Self> {
let base = match name { let base = match name {
"Leather Vest" => Self::LEATHER_VEST, "Leather Vest" => Self::LEATHER_VEST,
"Police Vest" => Self::POLICE_VEST, "Police Vest" => Self::POLICE_VEST,
@ -197,6 +198,13 @@ impl ArmourDto {
Immunities(self.immunities.to_vec()), Immunities(self.immunities.to_vec()),
)); ));
if let Some(bonus) = self.bonus {
commands.insert((
bonus.kind,
SimpleStatBundle::<ArmourBonusValue>::new(bonus.value.unwrap_or_default()),
));
}
commands.id() commands.id()
} }

View file

@ -12,6 +12,11 @@ pub enum EntityInfo {
owner: usize, owner: usize,
id: usize, id: usize,
}, },
Armour {
name: String,
owner: usize,
id: usize,
},
Global, Global,
} }

View file

@ -1,27 +1,55 @@
use std::collections::HashMap;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::armour::{ use proxisim_models::bundle::{
ArmourBodyPart, ArmourBodyPartSlot, ArmourBodyParts, ArmourCoverage, ArmourValue, ArmourVec, armour::{
BodyPartCoverage, Immunities, Immunity, PlayerArmour, ArmourBodyPart, ArmourBodyPartSlot, ArmourBodyParts, ArmourCoverage, ArmourValue,
ArmourVec, BodyPartCoverage, Immunities, Immunity, PlayerArmour,
},
bonus::ArmourBonusType,
stat::{
AdditiveBonus, ArmourBonusValue, Defence, Dexterity, MaxHealth, SimpleStatBonus,
SimpleStatEffective,
},
}; };
use strum::IntoEnumIterator; use strum::IntoEnumIterator;
use crate::{ use crate::{
Stages, Stages,
effect::Effects,
player::status_effect::{ player::status_effect::{
ConcussionGrenade, FlashGrenade, PepperSpray, TearGas, TempDebuffImmunity, ConcussionGrenade, FlashGrenade, PepperSpray, TearGas, TempDebuffImmunity,
}, },
}; };
fn generate_body_parts( fn equip_armour(
equip_q: Query<(Entity, &PlayerArmour)>, 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 commands: Commands,
mut effects: Effects,
) { ) {
for (player, equipped_armour) in equip_q.iter() { for (player, equipped_armour) in equip_q.iter() {
let mut parts = ArmourVec::<ArmourBodyPart>::default(); let mut parts = ArmourVec::<ArmourBodyPart>::default();
for (armour, coverage, armour_value, immunities) in armour_q.iter_many(equipped_armour) { let mut set = Some(None);
// commands.entity(player).add_child(armour);
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 { if let Some(immunities) = immunities {
let mut player = commands.entity(player); 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::<ArmourBonusValue>::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()); let parts = parts.0.map(|p| commands.spawn(p).id());
commands commands
@ -71,6 +121,45 @@ fn generate_body_parts(
} }
} }
pub(crate) fn configure(stages: &mut Stages) { fn apply_passives(
stages.equip.add_systems(generate_body_parts); bonus_q: Query<(
&ArmourBonusType,
&SimpleStatEffective<ArmourBonusValue>,
&ChildOf,
)>,
mut effects: Effects,
) {
let mut max_health_increase = HashMap::<Entity, u16>::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::<Defence>::new("immutable", value.value as f32 / 100.0),
relation.parent(),
);
}
ArmourBonusType::Irrepressible => {
effects.spawn(
AdditiveBonus::<Dexterity>::new("irrepressible", value.value as f32 / 100.0),
relation.parent(),
);
}
_ => (),
}
}
for (target, increase) in max_health_increase {
effects.spawn(
SimpleStatBonus::<MaxHealth>::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);
} }

View file

@ -10,10 +10,9 @@
"strategy": { "strategy": {
"type": "in_order", "type": "in_order",
"order": [ "order": [
"temporary", "secondary"
"primary"
], ],
"reload": true "reload": false
}, },
"education": { "education": {
"bio2350": true, "bio2350": true,
@ -86,55 +85,47 @@
"side_effects": 10 "side_effects": 10
}, },
"weapons": { "weapons": {
"primary": { "primary": null,
"id": 488, "secondary": {
"name": "MP 40", "id": 490,
"kind": "primary", "name": "Blunderbuss",
"cat": "smg", "kind": "secondary",
"base_dmg": 37, "cat": "shotgun",
"base_acc": 41, "base_dmg": 46,
"dmg": 5, "base_acc": 24,
"acc": 41, "dmg": 46,
"acc": 24,
"ammo": { "ammo": {
"clip_size": 32, "clip_size": 1,
"rate_of_fire": [ "rate_of_fire": [
3, 1,
5 1
] ]
}, },
"mods": [ "mods": [
"high_capacity_mags", null,
null null
], ],
"bonuses": [ "bonuses": [
{ {
"bonus": "specialist", "bonus": "bleed",
"value": 10 "value": 100
}, },
null null
], ],
"compatible_mods": [ "compatible_mods": [
"reflex_sight",
"holographic_sight",
"acog_sight",
"thermal_sight",
"laser1mw", "laser1mw",
"laser5mw", "laser5mw",
"laser30mw", "laser30mw",
"laser100mw", "laser100mw",
"small_suppressor",
"standard_suppressor",
"large_suppressor",
"extended_mags",
"high_capacity_mags",
"extra_clip", "extra_clip",
"extra_clip2", "extra_clip2",
"adjustable_trigger", "adjustable_trigger",
"hair_trigger", "hair_trigger",
"custom_grip", "custom_grip",
"standard_brake", "skeet_choke",
"heavy_duty_brake", "improved_choke",
"tactical_brake", "full_choke",
"small_light", "small_light",
"precision_light", "precision_light",
"tactical_illuminator" "tactical_illuminator"
@ -142,36 +133,138 @@
"experience": 0, "experience": 0,
"japanese": false "japanese": false
}, },
"secondary": null,
"melee": null, "melee": null,
"temporary": { "temporary": null
"id": 464,
"name": "Melatonin",
"kind": "temporary",
"cat": "temporary",
"base_dmg": 0,
"base_acc": 0,
"dmg": 0,
"acc": 0,
"ammo": null,
"mods": [
null,
null
],
"bonuses": [
null,
null
],
"compatible_mods": [],
"experience": 0,
"japanese": false
}
}, },
"armour": { "armour": {
"helmet": null, "helmet": {
"body": null, "slot": "head",
"pants": null, "id": 1355,
"gloves": null, "name": "Vanguard Respirator",
"boots": null "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
}
}
} }
} }

View file

@ -4,6 +4,7 @@ use bevy_ecs::prelude::*;
use proxisim_models::{ use proxisim_models::{
bundle::{ bundle::{
Id, Name, Id, Name,
armour::Armour,
player::{Attacker, Player}, player::{Attacker, Player},
weapon::Weapon, weapon::Weapon,
}, },
@ -18,6 +19,7 @@ pub struct EntityRegistry(pub HashMap<Entity, EntityInfo>);
fn read_entities( fn read_entities(
player_q: Query<(Entity, &Name, &Id, Has<Attacker>), With<Player>>, player_q: Query<(Entity, &Name, &Id, Has<Attacker>), With<Player>>,
weapon_q: Query<(Entity, &ChildOf, &Name, &Id), With<Weapon>>, weapon_q: Query<(Entity, &ChildOf, &Name, &Id), With<Weapon>>,
armour_q: Query<(Entity, &ChildOf, &Name, &Id), With<Armour>>,
mut registry: ResMut<EntityRegistry>, mut registry: ResMut<EntityRegistry>,
) { ) {
for (player, name, id, is_attacker) in player_q.iter() { 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) { pub(crate) fn configure(stages: &mut Stages) {

View file

@ -1,7 +1,7 @@
#![warn(clippy::perf, clippy::style, clippy::all)] #![warn(clippy::perf, clippy::style, clippy::all)]
#![allow(clippy::type_complexity)] #![allow(clippy::type_complexity)]
use bevy_ecs::{message::MessageRegistry, prelude::*, schedule::ScheduleLabel}; use bevy_ecs::{message::MessageRegistry, prelude::*, schedule::ScheduleLabel};
use effect::{register_effect, EffectBuilder}; use effect::{EffectBuilder, register_effect};
use metrics::Metrics; use metrics::Metrics;
use proxisim_models::{ use proxisim_models::{
bundle::player::{Attacker, Current, Defender}, bundle::player::{Attacker, Current, Defender},
@ -53,6 +53,7 @@ enum FightStatus {
enum Stage { enum Stage {
Equip, Equip,
Snapshot, Snapshot,
Passives,
PreFight, PreFight,
PreTurn, PreTurn,
Turn, Turn,
@ -64,6 +65,7 @@ enum Stage {
struct Stages { struct Stages {
equip: Schedule, equip: Schedule,
snapshot: Schedule, snapshot: Schedule,
passives: Schedule,
pre_fight: Schedule, pre_fight: Schedule,
pre_turn: Schedule, pre_turn: Schedule,
turn: Schedule, turn: Schedule,
@ -102,6 +104,7 @@ impl Simulation {
let mut stages = Stages { let mut stages = Stages {
equip: Schedule::new(Stage::Equip), equip: Schedule::new(Stage::Equip),
snapshot: Schedule::new(Stage::Snapshot), snapshot: Schedule::new(Stage::Snapshot),
passives: Schedule::new(Stage::Passives),
pre_fight: Schedule::new(Stage::PreFight), pre_fight: Schedule::new(Stage::PreFight),
pre_turn: Schedule::new(Stage::PreTurn), pre_turn: Schedule::new(Stage::PreTurn),
turn: Schedule::new(Stage::Turn), turn: Schedule::new(Stage::Turn),
@ -134,8 +137,10 @@ impl Simulation {
defender.spawn(&mut stages.world).insert(Defender); defender.spawn(&mut stages.world).insert(Defender);
stages.equip.run(&mut stages.world); stages.equip.run(&mut stages.world);
stages.pre_fight.run(&mut stages.world);
effect::run_effects(&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); stages.snapshot.run(&mut stages.world);
Self(stages) Self(stages)

View file

@ -3,9 +3,10 @@ use std::sync::Mutex;
use bevy_ecs::{prelude::*, query::QuerySingleError, system::SystemParam}; use bevy_ecs::{prelude::*, query::QuerySingleError, system::SystemParam};
use proxisim_models::bundle::{ use proxisim_models::bundle::{
stat::{ stat::{
AdditiveBonus, BaselineStat, ClipSize, Clips, CritRate, DamageBonus, Defence, Dexterity, AdditiveBonus, ArmourBonusValue, BaselineStat, ClipSize, Clips, CritRate, DamageBonus,
EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, Defence, Dexterity, EffectiveStat, MultiplicativeBonus, SimpleStatBaseline,
SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker, Speed, StatMarker, Strength,
WeaponAccuracy,
}, },
weapon::WeaponVerb, weapon::WeaponVerb,
}; };
@ -124,7 +125,12 @@ where
V: Into<LogValue>, V: Into<LogValue>,
{ {
fn from(value: Vec<(&'static str, V)>) -> Self { fn from(value: Vec<(&'static str, V)>) -> Self {
LogValue::Map(value.into_iter().map(|(k, v)| (k, v.into())).collect()) LogValue::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)] #[derive(SystemParam)]
pub struct Logger<'w> { pub struct Logger<'w> {
event_writer: MessageWriter<'w, LogEvent>, event_writer: MessageWriter<'w, LogEvent>,
@ -603,6 +433,7 @@ pub(crate) fn configure(stages: &mut Stages) {
log_simple_stat_changes::<DamageBonus>, log_simple_stat_changes::<DamageBonus>,
log_simple_stat_changes::<Clips>, log_simple_stat_changes::<Clips>,
log_simple_stat_changes::<ClipSize>, log_simple_stat_changes::<ClipSize>,
log_simple_stat_changes::<ArmourBonusValue>,
) )
.run_if(logging_enabled), .run_if(logging_enabled),
); );

View file

@ -1,10 +1,12 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
passive::{DrugCooldown, Education, FactionUpgrades, Merits}, 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( fn spawn_permanent_effects(
merit_q: Query<( merit_q: Query<(
@ -50,6 +52,12 @@ fn spawn_permanent_effects(
player, player,
); );
} }
if merits.life > 0 {
effects.spawn(
SimpleStatBonus::<MaxHealth>::new("merits", (merits.life as f32) * 0.05 + 1.0),
player,
);
}
if faction.spd > 0 { if faction.spd > 0 {
effects.spawn( effects.spawn(
@ -75,6 +83,12 @@ fn spawn_permanent_effects(
player, player,
); );
} }
if faction.life > 0 {
effects.spawn(
SimpleStatBonus::<MaxHealth>::new("faction", (faction.life as f32) * 0.01 + 1.0),
player,
);
}
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn spawn_drug_bonuses( fn spawn_drug_bonuses(

View file

@ -1,14 +1,13 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
armour::{ArmourBodyPart, ArmourBodyParts}, armour::{ArmourBodyPart, ArmourBodyParts},
passive::{FactionUpgrades, Merits},
player::{ player::{
Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender, Attacker, BodyPart, ChooseWeapon, CombatTurns, Current, CurrentTarget, Defeated, Defender,
FightEndType, Level, MaxHealth, PartDamageBonus, Player, PlayerStrategy, Weapons, FightEndType, Health, PartDamageBonus, Player, PlayerStrategy, Weapons,
}, },
stat::{ stat::{
AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, Health, AmmoControl, Clips, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth,
SimpleStatBundle, SimpleStatEffective, Speed, Strength, WeaponAccuracy, SimpleStatEffective, Speed, Strength, WeaponAccuracy,
}, },
weapon::{ weapon::{
Ammo, DamageStat, NeedsReload, NonTargeted, RateOfFire, Usable, Uses, Weapon, WeaponSlot, Ammo, DamageStat, NeedsReload, NonTargeted, RateOfFire, Usable, Uses, Weapon, WeaponSlot,
@ -22,6 +21,7 @@ use crate::{
log, log,
log::Logger, log::Logger,
metrics::Metrics, metrics::Metrics,
player::status_effect::{Bleed, DamageOverTime, DamageOverTimeType, DeferredDamage},
weapon::{DamageProcEffect, TurnTriggeredEffect, bonus::MultiTurnBonus}, 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::<Health>::new(max_health),
));
}
}
fn designate_first( fn designate_first(
attacker_q: Query<Entity, With<Attacker>>, attacker_q: Query<Entity, With<Attacker>>,
defender_q: Query<Entity, With<Defender>>, defender_q: Query<Entity, With<Defender>>,
@ -183,7 +161,7 @@ pub fn use_damaging_weapon(
), ),
(With<Weapon>, With<Current>, Without<NeedsReload>), (With<Weapon>, With<Current>, Without<NeedsReload>),
>, >,
player_q: Query< mut player_q: Query<
( (
Entity, Entity,
&EffectiveStat<Speed>, &EffectiveStat<Speed>,
@ -191,6 +169,8 @@ pub fn use_damaging_weapon(
&SimpleStatEffective<CritRate>, &SimpleStatEffective<CritRate>,
&SimpleStatEffective<WeaponAccuracy>, &SimpleStatEffective<WeaponAccuracy>,
&SimpleStatEffective<DamageBonus>, &SimpleStatEffective<DamageBonus>,
&Children,
&mut Health,
Has<Attacker>, Has<Attacker>,
), ),
(With<Player>, With<Current>), (With<Player>, With<Current>),
@ -201,11 +181,11 @@ pub fn use_damaging_weapon(
&EffectiveStat<Dexterity>, &EffectiveStat<Dexterity>,
&EffectiveStat<Defence>, &EffectiveStat<Defence>,
&ArmourBodyParts, &ArmourBodyParts,
&mut SimpleStatEffective<Health>, &mut Health,
), ),
With<CurrentTarget>, (With<CurrentTarget>, Without<Current>),
>, >,
armour_q: Query<&ArmourBodyPart>, (armour_q, damage_q): (Query<&ArmourBodyPart>, Query<(Entity, &DeferredDamage)>),
(damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>),
(mut ammo_q, mut temp_q): ( (mut ammo_q, mut temp_q): (
Query<( Query<(
@ -228,10 +208,48 @@ pub fn use_damaging_weapon(
else { else {
return; return;
}; };
let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) = let (
player_q.single().unwrap(); 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(); 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) { if let Ok(mut uses) = temp_q.get_mut(weapon) {
uses.0 -= 1; uses.0 -= 1;
if uses.0 == 0 { if uses.0 == 0 {
@ -253,7 +271,7 @@ pub fn use_damaging_weapon(
.ok() .ok()
.map(|(ammo, clips, rof, ammo_ctrl)| { .map(|(ammo, clips, rof, ammo_ctrl)| {
let ammo_ctrl = 1.0 - (ammo_ctrl).value; 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) (ammo, clips, rof_eff)
}); });
@ -414,13 +432,25 @@ pub fn use_damaging_weapon(
bonus.spawn(target, &mut effects, &mut rng.0); 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::<Bleed>::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", { log!(logger, "hit_target", {
actor: player, actor: player,
@ -431,7 +461,7 @@ pub fn use_damaging_weapon(
part_mult: mult, part_mult: mult,
rounds, rounds,
health_before: health_before, health_before: health_before,
health_after: health.value, health_after: health.0,
dmg, dmg,
dmg_spread, dmg_spread,
dmg_intrinsic, dmg_intrinsic,
@ -443,7 +473,7 @@ pub fn use_damaging_weapon(
crit_rate: crit.value, crit_rate: crit.value,
}); });
if health.value == 0 && !defeated { if health.0 == 0 && !defeated {
defeated = true; defeated = true;
commands.entity(target).insert(Defeated); commands.entity(target).insert(Defeated);
@ -539,12 +569,23 @@ pub fn restore_initial_state(
} }
} }
fn record_post_fight_stats( fn record_post_fight_stats(player_q: Query<(Entity, &Health)>, metrics: Res<Metrics>) {
player_q: Query<(Entity, &SimpleStatEffective<Health>)>,
metrics: Res<Metrics>,
) {
for (player, health) in player_q.iter() { 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<MaxHealth>)>,
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::<ChooseWeapon>(); stages.add_event::<ChooseWeapon>();
stages.equip.add_systems(designate_first); 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.pre_turn.add_systems(pick_action);
stages.turn.add_systems(use_damaging_weapon); stages.turn.add_systems(use_damaging_weapon);
stages stages

View file

@ -2,8 +2,8 @@ use std::marker::PhantomData;
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::stat::{ use proxisim_models::bundle::stat::{
AdditiveBonus, AdditiveBonuses, AmmoControl, BaselineStat, ClipSize, Clips, CritRate, AdditiveBonus, AdditiveBonuses, AmmoControl, ArmourBonusValue, BaselineStat, ClipSize, Clips,
DamageBonus, Defence, Dexterity, EffectiveStat, Health, MultiplicativeBonus, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MaxHealth, MultiplicativeBonus,
MultiplicativeBonuses, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker, MultiplicativeBonuses, SimpleStatBonus, SimpleStatEffective, SimpleStatMarker,
SimpleStatSnapshot, Speed, StatMarker, StatSnapshot, Strength, WeaponAccuracy, SimpleStatSnapshot, Speed, StatMarker, StatSnapshot, Strength, WeaponAccuracy,
}; };
@ -196,5 +196,6 @@ pub(crate) fn configure(stages: &mut Stages) {
register_simple_stat_effects::<WeaponAccuracy>(stages); register_simple_stat_effects::<WeaponAccuracy>(stages);
register_simple_stat_effects::<ClipSize>(stages); register_simple_stat_effects::<ClipSize>(stages);
register_simple_stat_effects::<Clips>(stages); register_simple_stat_effects::<Clips>(stages);
register_simple_stat_effects::<Health>(stages); register_simple_stat_effects::<MaxHealth>(stages);
register_simple_stat_effects::<ArmourBonusValue>(stages);
} }

View file

@ -1,8 +1,9 @@
use std::{collections::VecDeque, marker::PhantomData}; use std::{collections::VecDeque, marker::PhantomData};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::stat::{ use proxisim_models::bundle::{
AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength, player::Current,
stat::{AdditiveBonus, Defence, Dexterity, MultiplicativeBonus, Speed, StatMarker, Strength},
}; };
use rand::Rng as _; 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<Dot: DamageOverTimeMarker> {
marker: PhantomData<Dot>,
pub base_damage: u32,
pub turn: u16,
}
impl<Dot: DamageOverTimeMarker> DamageOverTime<Dot> {
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<const N: usize, M: AdditiveStatusEffectMarker<N>>( fn apply_additive_status_effect<const N: usize, M: AdditiveStatusEffectMarker<N>>(
In(entities): In<Vec<Entity>>, In(entities): In<Vec<Entity>>,
effect_q: Query<(Entity, &ChildOf, &AdditiveStatusEffect<N, M>)>, effect_q: Query<(Entity, &ChildOf, &AdditiveStatusEffect<N, M>)>,
@ -510,6 +551,34 @@ fn remove_temp_debuff_effect<Temp: DebuffingTempMarker>(
} }
} }
fn process_dot<Dot: DamageOverTimeMarker>(
dot_q: Query<(Entity, &mut DamageOverTime<Dot>), With<Current>>,
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::<DamageOverTime<Dot>>();
} 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<Temp: DebuffingTempMarker>(stages: &mut Stages) { fn register_debuff_temp<Temp: DebuffingTempMarker>(stages: &mut Stages) {
stages stages
.register_effect::<TempDebuffEffect<Temp>>() .register_effect::<TempDebuffEffect<Temp>>()
@ -544,4 +613,6 @@ pub(crate) fn configure(stages: &mut Stages) {
register_status_effect::<4, Motivate>(stages); register_status_effect::<4, Motivate>(stages);
register_status_effect::<4, Demoralise>(stages); register_status_effect::<4, Demoralise>(stages);
register_status_effect::<2, Frozen>(stages); register_status_effect::<2, Frozen>(stages);
stages.post_turn.add_systems(process_dot::<Bleed>);
} }

View file

@ -12,7 +12,8 @@ use crate::{
Stages, Stages,
effect::{Effects, TurnLimitedEffect}, effect::{Effects, TurnLimitedEffect},
player::status_effect::{ 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:?}"), val => unimplemented!("{val:?}"),
} }
} }
@ -434,6 +444,6 @@ pub(crate) fn prepare_bonuses(
pub(crate) fn configure(stages: &mut Stages) { pub(crate) fn configure(stages: &mut Stages) {
stages stages
.pre_fight .passives
.add_systems(prepare_bonuses.after(super::apply_passives)); .add_systems(prepare_bonuses.after(super::apply_passives));
} }

View file

@ -1,7 +1,7 @@
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use proxisim_models::bundle::{ use proxisim_models::bundle::{
passive::{Education, EducationPartDamageBonus, FactionUpgrades, Merits}, passive::{Education, EducationPartDamageBonus, FactionUpgrades, Merits},
player::{Current, PartDamageBonus, Weapons}, player::{Current, PartDamageBonus},
stat::{ stat::{
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity, AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
SimpleStatBonus, SimpleStatEffective, WeaponAccuracy, SimpleStatBonus, SimpleStatEffective, WeaponAccuracy,
@ -17,6 +17,7 @@ use crate::{
effect::{Effects, TurnLimitedEffect}, effect::{Effects, TurnLimitedEffect},
log, log,
log::Logger, log::Logger,
player::status_effect::DamageOverTimeType,
}; };
use self::bonus::{ use self::bonus::{
@ -152,6 +153,10 @@ pub enum DamageProcEffect {
value: f32, value: f32,
bonus: SelfStatusEffect, bonus: SelfStatusEffect,
}, },
DamageOverTimer {
value: f32,
kind: DamageOverTimeType,
},
} }
fn apply_passives( 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 // running this in the snapshot layer ensures that the stat increases aren't restored at the
// end of the run // end of the run
stages.snapshot.add_systems(apply_first_turn_effects); 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.turn.add_systems(reload_weapon);
stages.post_turn.add_systems(unset_current); stages.post_turn.add_systems(unset_current);
stages stages