diff --git a/src/armour/mod.rs b/src/armour/mod.rs index b139710..74d7987 100644 --- a/src/armour/mod.rs +++ b/src/armour/mod.rs @@ -238,7 +238,7 @@ fn generate_body_parts( // 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 + // unknown Immunity::NerveGas => (), Immunity::TearGas => { player.insert(TempDebuffImmunity::::default()); diff --git a/src/effect.rs b/src/effect.rs index acea7db..1f5b7e1 100644 --- a/src/effect.rs +++ b/src/effect.rs @@ -68,8 +68,6 @@ pub struct Effects<'w, 's> { 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( &mut self, @@ -203,6 +201,8 @@ pub(crate) fn run_effects(world: &mut World) { }; for entity in entities { + let parent = world.entity(entity).get::().unwrap().get(); + world.entity_mut(parent).remove_child(entity); world.despawn(entity); } } @@ -233,11 +233,11 @@ fn update_round_limited_effects( continue; } + effect.turns -= 1; if effect.turns == 0 { effects.remove(entity); continue; } - effect.turns -= 1; } } @@ -291,9 +291,9 @@ pub(crate) fn configure(stages: &mut Stages) { stages.world.init_resource::(); stages.snapshot.add_systems(mark_permanent_effects); + stages.pre_turn.add_systems(advance_clock); stages - .pre_turn - .add_systems((advance_clock, update_round_limited_effects)); - stages.post_turn.add_systems(update_time_limited_effects); + .post_turn + .add_systems((update_time_limited_effects, update_round_limited_effects)); stages.restore.add_systems(remove_transient_effects); } diff --git a/src/lib.rs b/src/lib.rs index 04bf5fe..a721411 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -149,7 +149,7 @@ impl Simulation { pub fn truncate_log(&mut self) { let mut log = self.0.world.resource_mut::(); - log.entries.clear(); + log.clear(); } pub fn set_metrics(&mut self, recording: bool) { @@ -158,6 +158,16 @@ impl Simulation { } pub fn consume_metrics(&mut self) -> (Vec, Vec) { + let entities = self.0.world.entities().len(); + let components = self.0.world.components().len(); + self.0 + .world + .resource::() + .increment_counter(Entity::from_raw(0), "entities", entities.into()); + self.0 + .world + .resource::() + .increment_counter(Entity::from_raw(0), "components", components as u64); metrics::consume_metrics(&self.0.world) } @@ -352,7 +362,7 @@ mod tests { clip_size: 25, rate_of_fire: [3, 5], }, - mods: Vec::default(), + mods: vec![WeaponMod::HairTrigger], bonuses: vec![WeaponBonusInfo { bonus: WeaponBonus::Expose, value: 9.0, @@ -431,17 +441,15 @@ mod tests { #[test] fn init_simulator() { - let mut sim = Simulation::new(attacker(), defender()); - sim.run_once(); + Simulation::new(attacker(), defender()); } #[test] fn metrics() { let mut sim = Simulation::new(attacker(), defender()); sim.set_metrics(true); - for _ in 0..20 { - sim.run_once(); - } + sim.run_once(); sim.consume_metrics(); + panic!(); } } diff --git a/src/log.rs b/src/log.rs index 51d01d9..cb8ec48 100644 --- a/src/log.rs +++ b/src/log.rs @@ -1,7 +1,6 @@ use std::sync::Mutex; use bevy_ecs::{prelude::*, system::SystemParam}; -use macros::LogMessage; use crate::{ entity_registry::EntityRegistry, @@ -9,7 +8,7 @@ use crate::{ player::stats::{ AdditiveBonus, BaselineStat, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat, MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective, - SimpleStatMarker, Speed, StatMarker, StatType, Strength, WeaponAccuracy, + SimpleStatMarker, Speed, StatMarker, Strength, WeaponAccuracy, }, weapon::WeaponVerb, Stages, @@ -19,14 +18,11 @@ use crate::{ pub struct Logging(pub bool); #[derive(Event)] -struct LogEvent(Mutex>>); +struct LogEvent(Mutex>); -impl From for LogEvent -where - T: LogMessage, -{ - fn from(value: T) -> Self { - Self(Mutex::new(Some(Box::new(value)))) +impl From for LogEvent { + fn from(value: DynamicLogMessage) -> Self { + LogEvent(Mutex::new(Some(value))) } } @@ -53,6 +49,8 @@ pub enum LogValue<'a> { Debug(&'a (dyn std::fmt::Debug + Send + Sync)), Player(Entity), Weapon(Entity), + Array(Vec>), + Map(Vec<(&'static str, LogValue<'a>)>), } impl<'a> From for LogValue<'a> { @@ -61,6 +59,12 @@ impl<'a> From for LogValue<'a> { } } +impl<'a> From<&'a str> for LogValue<'static> { + fn from(value: &'a str) -> Self { + Self::String(value.to_owned()) + } +} + impl<'a> From for LogValue<'a> { fn from(value: f32) -> Self { Self::Float(value) @@ -103,6 +107,24 @@ where } } +impl<'a, V> From> for LogValue<'a> +where + V: Into>, +{ + fn from(value: Vec) -> Self { + LogValue::Array(value.into_iter().map(Into::into).collect()) + } +} + +impl<'a, V> From> for LogValue<'a> +where + V: Into>, +{ + fn from(value: Vec<(&'static str, V)>) -> Self { + LogValue::Map(value.into_iter().map(|(k, v)| (k, v.into())).collect()) + } +} + #[cfg(feature = "json")] impl<'a> LogValue<'a> { fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value { @@ -125,11 +147,19 @@ impl<'a> LogValue<'a> { LogValue::Entity(id) => { serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap() } + LogValue::Array(items) => serde_json::Value::Array( + items.iter().map(|v| v.to_value(entity_registry)).collect(), + ), + LogValue::Map(map) => serde_json::Value::Object( + map.iter() + .map(|(k, v)| ((*k).to_owned(), v.to_value(entity_registry))) + .collect(), + ), } } } -pub trait LogMessage: Send + Sync + 'static { +trait LogMessage: Send + Sync + 'static { fn tag(&self) -> &'static str; fn entries(&self) -> Vec<(&'static str, LogValue<'_>)>; @@ -137,7 +167,7 @@ pub trait LogMessage: Send + Sync + 'static { #[derive(Resource, Default)] pub struct Log { - pub entries: Vec>, + entries: Vec, pub expanded: bool, } @@ -158,6 +188,10 @@ impl Log { ).collect::>() }) } + + pub fn clear(&mut self) { + self.entries.clear(); + } } impl std::fmt::Display for Log { @@ -184,6 +218,7 @@ impl std::fmt::Display for Log { LogValue::Player(id) | LogValue::Weapon(id) | LogValue::Entity(id) => { write!(f, "{:?}", id)? } + LogValue::Array(_) | LogValue::Map(_) => (), }; if fields.peek().is_some() { @@ -391,10 +426,9 @@ pub struct Logger<'w> { } impl<'w> Logger<'w> { - pub fn log(&mut self, body: B) + pub fn log(&mut self, body: B) where - B: FnOnce() -> M, - M: LogMessage, + B: FnOnce() -> DynamicLogMessage, { if self.logging.0 { self.event_writer.send(body().into()); @@ -408,99 +442,9 @@ fn logging_enabled(logging: Res) -> bool { fn append_log_messages(mut events: EventReader, mut log: ResMut) { 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_q: Query< - (Entity, &BaselineStat, &EffectiveStat, &Children), - Changed>, - >, - add_q: Query<&AdditiveBonus>, - mult_q: Query<&MultiplicativeBonus>, - 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_q: Query< - ( - Entity, - &SimpleStatBaseline, - &SimpleStatEffective, - &Children, - ), - Changed>, - >, - bonus_q: Query<&SimpleStatBonus>, - mut logger: Logger, -) where - Stat::ValueType: Into>, - 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::(), - baseline: baseline.value.into(), - effective: effective.value.into(), - bonuses, - }); + if let Some(entry) = event.0.lock().unwrap().take() { + log.entries.push(entry); + } } } @@ -547,18 +491,94 @@ macro_rules! log_values { (@ { $(,)* $($out:expr),* } $label:ident: %$val:expr, $($rest:tt)*) => { $crate::log_values!(@ { $($out),*, (stringify!($label),$crate::log::LogValue::String(format!("{}",$val))) } $($rest)*) }; + (@ { $(,)* $($out:expr),* } $value:ident, $($rest:tt)*) => { + $crate::log_values!(@ { $($out),*, (stringify!($value),$value.into()) } $($rest)*) + }; + (@ { $(,)* $($out:expr),* } ?$value:ident, $($rest:tt)*) => { + $crate::log_values!(@ { $($out),*, (stringify!($value),$crate::log::LogValue::String(format!("{:?}",$val))) } $($rest)*) + }; + (@ { $(,)* $($out:expr),* } %$value:ident, $($rest:tt)*) => { + $crate::log_values!(@ { $($out),*, (stringify!($value),$crate::log::LogValue::String(format!("{}",$val))) } $($rest)*) + }; ($($args:tt)* ) => { $crate::log_values!(@ { } $($args)*,) }; } +fn log_stat_changes( + stat_q: Query< + (Entity, &BaselineStat, &EffectiveStat, &Children), + Changed>, + >, + add_q: Query<&AdditiveBonus>, + mult_q: Query<&MultiplicativeBonus>, + mut logger: Logger, +) { + for (player, baseline, effective, children) in stat_q.iter() { + let effects_add: Vec<_> = add_q + .iter_many(children.get()) + .map(|bonus| (bonus.label, 100.0 * bonus.value)) + .collect(); + let effects_mult: Vec<_> = mult_q + .iter_many(children.get()) + .map(|bonus| (bonus.label, 100.0 * bonus.value)) + .collect(); + + log!(logger, "stat_change", { + target: player, + stat: ?Stat::stat_type(), + effects_add, + effects_mult, + baseline: baseline.value, + effective: effective.value, + }); + } +} + +fn log_simple_stat_changes( + stat_q: Query< + ( + Entity, + &SimpleStatBaseline, + &SimpleStatEffective, + &Children, + ), + Changed>, + >, + bonus_q: Query<&SimpleStatBonus>, + mut logger: Logger, +) where + Stat::ValueType: Into>, + Stat::BonusType: Into>, +{ + for (target, baseline, effective, children) in stat_q.iter() { + let bonuses: Vec<_> = bonus_q + .iter_many(children.get()) + .map(|bonus| (bonus.label, Stat::denormalise_bonus(bonus.value))) + .collect(); + + log!(logger, "applied_bonus", { + target, + stat: std::any::type_name::(), + baseline: Stat::denormalise_value(baseline.value), + effective: Stat::denormalise_value(effective.value), + bonuses + }); + } +} + pub(crate) fn configure(stages: &mut Stages) { stages.world.insert_resource(Log::default()); stages.world.insert_resource(Logging(false)); stages.add_event::(); - stages.post_turn.add_systems(append_log_messages); + stages + .post_turn + .add_systems(append_log_messages.run_if(logging_enabled)); + stages + .post_fight + .add_systems(append_log_messages.run_if(logging_enabled)); stages.turn.add_systems( ( log_stat_changes::, diff --git a/src/passives.rs b/src/passives.rs index 17ca8c7..77e5a17 100644 --- a/src/passives.rs +++ b/src/passives.rs @@ -2,8 +2,9 @@ use bevy_ecs::prelude::*; use crate::{ effect::Effects, - player::stats::{ - AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength, + player::{ + stats::{AdditiveBonus, CritRate, Defence, Dexterity, SimpleStatBonus, Speed, Strength}, + BodyPart, }, Stages, }; @@ -191,6 +192,20 @@ pub enum DrugCooldown { Vicodin, } +#[derive(Clone, Copy)] +pub enum EducationPartDamageBonus { + Bio2380, +} + +impl EducationPartDamageBonus { + pub fn dmg_bonus(self, part: BodyPart) -> Option { + match part { + BodyPart::Throat => Some(0.10), + _ => None, + } + } +} + #[derive(Bundle, Default)] pub(crate) struct PassiveBundle { pub merits: Merits, diff --git a/src/player/mod.rs b/src/player/mod.rs index 05022e9..a2e8b77 100644 --- a/src/player/mod.rs +++ b/src/player/mod.rs @@ -9,9 +9,9 @@ use crate::{ log, log::Logger, metrics::Metrics, - passives::{Education, FactionUpgrades, Merits}, + passives::{EducationPartDamageBonus, FactionUpgrades, Merits}, weapon::{ - bonus::MultiTurnBonus, + bonus::{BonusPartDamageBonus, MultiTurnBonus}, temp::{NonTargeted, Uses}, Ammo, DamageProcEffect, DamageStat, NeedsReload, RateOfFire, TurnTriggeredEffect, Usable, Weapon, WeaponSlot, @@ -172,6 +172,24 @@ pub enum FightEndType { Loss, } +#[derive(Component)] +pub enum PartDamageBonus { + Education(EducationPartDamageBonus), + WeaponBonus { + value: f32, + bonus: BonusPartDamageBonus, + }, +} + +impl PartDamageBonus { + pub fn dmg_bonus(&self, part: BodyPart) -> Option { + match self { + Self::Education(edu) => edu.dmg_bonus(part), + Self::WeaponBonus { value, bonus } => bonus.dmg_bonus(part, *value), + } + } +} + #[derive(Bundle)] pub struct PlayerBundle { pub name: Name, @@ -341,7 +359,6 @@ pub fn use_damaging_weapon( &SimpleStatEffective, &SimpleStatEffective, &SimpleStatEffective, - &Education, Has, ), (With, With), @@ -357,7 +374,7 @@ pub fn use_damaging_weapon( With, >, armour_q: Query<&armour::ArmourBodyPart>, - damage_proc_q: Query<&DamageProcEffect>, + (damage_proc_q, part_bonus_q): (Query<&DamageProcEffect>, Query<&PartDamageBonus>), (mut ammo_q, mut temp_q): ( Query<( &mut Ammo, @@ -367,18 +384,19 @@ pub fn use_damaging_weapon( )>, Query<&mut Uses>, ), - (mut logger, mut commands, dmg_spread, metrics): ( + (mut logger, mut commands, dmg_spread, metrics, mut effects): ( Logger, Commands, Local, Res, + Effects, ), ) { 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) = + let (player, player_spd, player_str, player_crit, acc_bonus, p_dmg_bonus, attacker) = player_q.single(); let (target, target_dex, target_def, armour_parts, mut health) = target_q.single_mut(); @@ -457,6 +475,7 @@ pub fn use_damaging_weapon( actor: player, recipient: target, rounds: rounds, + hit_chance: hit_chance, }); metrics.increment_counter(player, "miss", 1); metrics.increment_counter(weapon, "miss", 1); @@ -494,13 +513,17 @@ pub fn use_damaging_weapon( 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 + // NOTE: Proxima's simulator seems to have the damage spread be between 95% and 105%, + // but from my brief tests it seems that 100% to 110% lines up better, at least for h2h. + // It might be better to revivisit this detail later down the line and run more tests. 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; + for part_bonus in part_bonus_q.iter_many(children.get()) { + if let Some(bonus) = part_bonus.dmg_bonus(body_part) { + dmg_bonus.value += bonus; + } } // TODO: special ammo @@ -519,11 +542,13 @@ pub fn use_damaging_weapon( 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 / 100.0) as f64) { + match *effect { + DamageProcEffect::MultiTurn { value, bonus } => { + if multi_attack_proc.is_some() { + continue; + } + let chance = (value / 100.0) as f64; + if chance > 1.0 || rng.gen_bool(chance) { match bonus { MultiTurnBonus::Blindfire => { multi_attack_proc = Some(MultiAttack::Blindfire) @@ -545,7 +570,18 @@ pub fn use_damaging_weapon( metrics.increment_counter(weapon, bonus.counter_label(), 1); } } - _ => (), + DamageProcEffect::SelfEffect { value, bonus } => { + let chance = (value / 100.0) as f64; + if chance > 1.0 || rng.gen_bool(chance) { + bonus.spawn(player, &mut effects); + } + } + DamageProcEffect::OpponentEffect { value, bonus } => { + let chance = (value / 100.0) as f64; + if chance > 1.0 || rng.gen_bool(chance) { + bonus.spawn(target, &mut effects, &mut rng.0); + } + } } } } @@ -561,6 +597,7 @@ pub fn use_damaging_weapon( part: %body_part, part_mult: mult, dmg: dmg, + rounds: rounds, health_before: health_before, health_after: health.value, dmg_spread: dmg_spread, @@ -584,7 +621,7 @@ pub fn use_damaging_weapon( FightEndType::Victory } else { FightEndType::Loss - } + }, }); metrics.increment_counter(player, "victory", 1); } @@ -613,7 +650,7 @@ pub fn use_damaging_weapon( } pub fn check_stalemate( - current_q: Query<(Entity, &CombatTurns, Option<&Attacker>), (With, With)>, + current_q: Query<(Entity, &CombatTurns, Has), (With, With)>, target_q: Query>, other_attackers_q: Query<(), (With, Without)>, mut state: ResMut, @@ -622,14 +659,14 @@ pub fn check_stalemate( metrics: Res, ) { let (current, current_turns, attacker) = current_q.single(); - if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker.is_some() { + if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker { commands.entity(current).insert(Defeated); let target = target_q.single(); log!(logger, "fight_end", { actor: current, recipient: target, - fight_end_type: %FightEndType::Stalemate + fight_end_type: %FightEndType::Stalemate, }); metrics.increment_counter(current, "stalemate", 1); diff --git a/src/player/stats.rs b/src/player/stats.rs index 7ba0bf4..ca49dcd 100644 --- a/src/player/stats.rs +++ b/src/player/stats.rs @@ -10,6 +10,12 @@ pub trait SimpleStatMarker: Send + Sync + 'static { fn apply_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType; fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType; + fn denormalise_value(value: Self::ValueType) -> Self::ValueType { + value + } + fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType { + value + } } #[derive(Component)] @@ -143,6 +149,12 @@ impl SimpleStatMarker for AmmoControl { fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { value - bonus } + fn denormalise_value(value: Self::ValueType) -> Self::ValueType { + value * 100.0 + } + fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType { + value * 100.0 + } } #[derive(Default)] @@ -157,6 +169,12 @@ impl SimpleStatMarker for DamageBonus { fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { value - bonus } + fn denormalise_value(value: Self::ValueType) -> Self::ValueType { + value * 100.0 + } + fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType { + value * 100.0 + } } #[derive(Default)] @@ -171,6 +189,12 @@ impl SimpleStatMarker for WeaponAccuracy { fn revert_bonus(value: Self::ValueType, bonus: Self::BonusType) -> Self::ValueType { value - bonus } + fn denormalise_value(value: Self::ValueType) -> Self::ValueType { + value * 50.0 + 50.0 + } + fn denormalise_bonus(value: Self::BonusType) -> Self::BonusType { + value * 50.0 + } } #[derive(Default)] @@ -490,7 +514,7 @@ fn apply_simple_stat_bonus( effect_q: Query<(&SimpleStatBonus, &Parent)>, mut stat_q: Query<&mut SimpleStatEffective>, ) { - for (bonus, target) in effect_q.iter_many(entities) { + 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); } @@ -568,5 +592,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); } diff --git a/src/player/status_effect.rs b/src/player/status_effect.rs index 0df2132..ceb6b7c 100644 --- a/src/player/status_effect.rs +++ b/src/player/status_effect.rs @@ -1,12 +1,12 @@ 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, log::Logger, weapon::temp::AssociatedWeapon, Rng, Stages, @@ -117,7 +117,7 @@ impl DebuffingTempMarker for Sand { #[derive(Component)] struct LinkedComponents([Entity; N]); -trait Stats { +pub trait Stats { fn spawn_additive_effects( effects: &mut Effects, target: Entity, @@ -160,15 +160,18 @@ impl_n_stats!(2, A, B); impl_n_stats!(3, A, B, C); impl_n_stats!(4, A, B, C, D); -trait AdditiveStatusEffectMarker: Send + Sync + 'static { +// TODO: the const generic arguably isn't worth the trouble and it might be better to remove it +pub trait AdditiveStatusEffectMarker: Send + Sync + 'static { type AffectedStats: Stats; fn max_stack() -> usize; fn factor() -> f32; fn duration() -> f32; } +// TODO: instead of tracking it in the status effect itself, add generic +// `StatusEffectEffectiveness` and `StatusEffectExtraDuration` components #[derive(Component)] -struct AdditiveStatusEffect +pub struct AdditiveStatusEffect where M: AdditiveStatusEffectMarker, { @@ -187,16 +190,6 @@ impl> Default for AdditiveStatu } } -impl> AdditiveStatusEffect { - 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 { @@ -368,8 +361,14 @@ fn apply_additive_status_effect mut parent_q: Query>>, mut commands: Commands, mut effects: Effects, + mut logger: Logger, ) { for (entity, player, effect) in effect_q.iter_many(entities) { + log!(logger, "apply_status_effect", { + recipient: player.get(), + effect: std::any::type_name::(), + }); + let stack = parent_q.get_mut(player.get()).unwrap(); let new_effects = >::spawn_additive_effects( @@ -434,24 +433,13 @@ fn apply_temp_debuff_effect( (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 { + log!(logger, "used_debuff_temp", { actor: user.get(), recipient: player.get(), weapon: weapon.0, @@ -488,7 +476,7 @@ fn apply_temp_debuff_effect( }); } - logger.log(|| UsedDebuffTemp { + log!(logger, "used_debuff_temp", { actor: user.get(), recipient: player.get(), weapon: weapon.0, @@ -505,14 +493,6 @@ fn remove_temp_debuff_effect( _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 { diff --git a/src/weapon/bonus.rs b/src/weapon/bonus.rs index 86f0896..7d6a34b 100644 --- a/src/weapon/bonus.rs +++ b/src/weapon/bonus.rs @@ -3,9 +3,15 @@ use bevy_ecs::prelude::*; use crate::{ effect::{Effects, TurnLimitedEffect}, hierarchy::{HierarchyBuilder, Parent}, - player::stats::{ - AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, - SimpleStatEffective, Speed, Strength, WeaponAccuracy, + player::{ + stats::{ + AdditiveBonus, AmmoControl, Clips, CritRate, DamageBonus, SimpleStatBonus, + SimpleStatEffective, Speed, Strength, WeaponAccuracy, + }, + status_effect::{ + AdditiveStatusEffect, Crippled, Demoralise, Frozen, Motivate, Slow, Weakened, Withered, + }, + BodyPart, PartDamageBonus, }, Stages, }; @@ -155,6 +161,117 @@ impl MultiTurnBonus { } } +#[derive(Clone, Copy)] +pub enum OpponentStatusEffect { + Cripple, + // TODO: implement for group fights + Demoralise, + Freeze, + Slow, + Toxin, + Weaken, + Wither, +} + +impl OpponentStatusEffect { + pub fn spawn(self, target: Entity, effects: &mut Effects, rng: &mut impl rand::Rng) { + match self { + Self::Cripple => { + effects.spawn(AdditiveStatusEffect::<1, Crippled>::default(), target); + } + Self::Demoralise => { + effects.spawn(AdditiveStatusEffect::<4, Demoralise>::default(), target); + } + Self::Freeze => { + effects.spawn(AdditiveStatusEffect::<2, Frozen>::default(), target); + } + Self::Slow => { + effects.spawn(AdditiveStatusEffect::<1, Slow>::default(), target); + } + Self::Toxin => match rng.gen_range(0..4) { + 0 => OpponentStatusEffect::Cripple.spawn(target, effects, rng), + 1 => OpponentStatusEffect::Slow.spawn(target, effects, rng), + 2 => OpponentStatusEffect::Weaken.spawn(target, effects, rng), + _ => OpponentStatusEffect::Wither.spawn(target, effects, rng), + }, + Self::Weaken => { + effects.spawn(AdditiveStatusEffect::<1, Weakened>::default(), target); + } + Self::Wither => { + effects.spawn(AdditiveStatusEffect::<1, Withered>::default(), target); + } + } + } +} + +#[derive(Clone, Copy)] +pub enum SelfStatusEffect { + Motivate, +} + +impl SelfStatusEffect { + pub fn spawn(self, current: Entity, effects: &mut Effects) { + match self { + Self::Motivate => { + effects.spawn(AdditiveStatusEffect::<4, Motivate>::default(), current); + } + } + } +} + +#[derive(Clone, Copy)] +pub enum BonusPartDamageBonus { + Achilles, + Crusher, + Cupid, + Deadeye, + Roshambo, + Throttle, +} + +impl BonusPartDamageBonus { + pub fn dmg_bonus(self, part: BodyPart, value: f32) -> Option { + match self { + Self::Achilles => match part { + BodyPart::LeftFoot | BodyPart::RightFoot => Some(value), + _ => None, + }, + Self::Crusher => { + if part == BodyPart::Head { + Some(value) + } else { + None + } + } + Self::Cupid => { + if part == BodyPart::Heart { + Some(value) + } else { + None + } + } + Self::Deadeye => match part { + BodyPart::Head | BodyPart::Heart | BodyPart::Throat => Some(value), + _ => None, + }, + Self::Roshambo => { + if part == BodyPart::Groin { + Some(value) + } else { + None + } + } + Self::Throttle => { + if part == BodyPart::Throat { + Some(value) + } else { + None + } + } + } + } +} + pub(crate) fn prepare_bonuses( bonus_q: Query<( &Parent, @@ -278,6 +395,121 @@ pub(crate) fn prepare_bonuses( .set_parent(weapon.get()); } + WeaponBonus::Achilles => { + commands + .spawn(PartDamageBonus::WeaponBonus { + value: value.0 / 100.0, + bonus: BonusPartDamageBonus::Achilles, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Cupid => { + commands + .spawn(PartDamageBonus::WeaponBonus { + value: value.0 / 100.0, + bonus: BonusPartDamageBonus::Cupid, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Crusher => { + commands + .spawn(PartDamageBonus::WeaponBonus { + value: value.0 / 100.0, + bonus: BonusPartDamageBonus::Crusher, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Deadeye => { + commands + .spawn(PartDamageBonus::WeaponBonus { + value: value.0 / 100.0, + bonus: BonusPartDamageBonus::Deadeye, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Throttle => { + commands + .spawn(PartDamageBonus::WeaponBonus { + value: value.0 / 100.0, + bonus: BonusPartDamageBonus::Throttle, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Roshambo => { + commands + .spawn(PartDamageBonus::WeaponBonus { + value: value.0 / 100.0, + bonus: BonusPartDamageBonus::Roshambo, + }) + .set_parent(weapon.get()); + } + + WeaponBonus::Cripple => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Cripple, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Demoralise => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Demoralise, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Freeze => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Freeze, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Slow => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Slow, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Toxin => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Toxin, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Weaken => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Weaken, + }) + .set_parent(weapon.get()); + } + WeaponBonus::Wither => { + commands + .spawn(DamageProcEffect::OpponentEffect { + value: value.0, + bonus: OpponentStatusEffect::Wither, + }) + .set_parent(weapon.get()); + } + + WeaponBonus::Motivate => { + commands + .spawn(DamageProcEffect::SelfEffect { + value: value.0, + bonus: SelfStatusEffect::Motivate, + }) + .set_parent(weapon.get()); + } + val => unimplemented!("{val:?}"), } } diff --git a/src/weapon/mod.rs b/src/weapon/mod.rs index 261e49e..44e6ae1 100644 --- a/src/weapon/mod.rs +++ b/src/weapon/mod.rs @@ -1,22 +1,24 @@ use bevy_ecs::prelude::*; -use macros::LogMessage; use crate::{ effect::{Effects, TurnLimitedEffect}, hierarchy::{HierarchyBuilder, Parent}, + log, log::Logger, - passives::{Education, FactionUpgrades, Merits}, + passives::{Education, EducationPartDamageBonus, FactionUpgrades, Merits}, player::{ stats::{ AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity, SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, WeaponAccuracy, }, - Current, Weapons, + Current, PartDamageBonus, Weapons, }, Id, Name, Stages, }; -use self::bonus::{FirstTurnBonus, MultiTurnBonus, TurnTriggeredBonus}; +use self::bonus::{ + FirstTurnBonus, MultiTurnBonus, OpponentStatusEffect, SelfStatusEffect, TurnTriggeredBonus, +}; pub mod bonus; pub mod temp; @@ -58,7 +60,7 @@ pub enum WeaponCategory { Smg, Shotgun, Pistol, - Club, + Clubbing, Piercing, Slashing, Mechanical, @@ -110,7 +112,7 @@ pub enum WeaponMod { FullChoke, RecoilPad, StandardBrake, - HeavyDutyBreak, + HeavyDutyBrake, TacticalBrake, SmallLight, PrecisionLight, @@ -220,11 +222,6 @@ pub enum FirstTurnEffect { 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 { @@ -234,31 +231,28 @@ impl FirstTurnEffect { } } +#[derive(Component)] +pub enum DamageProcEffect { + MultiTurn { + value: f32, + bonus: MultiTurnBonus, + }, + OpponentEffect { + value: f32, + bonus: OpponentStatusEffect, + }, + SelfEffect { + value: f32, + bonus: SelfStatusEffect, + }, +} + #[derive(Component)] pub struct EquippedMods(pub Vec); #[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, -} - #[derive(Bundle)] pub struct WeaponBundle { pub usable: Usable, @@ -514,7 +508,7 @@ fn apply_passives( weapon, ); } - WeaponMod::HeavyDutyBreak => { + WeaponMod::HeavyDutyBrake => { effects.spawn( SimpleStatBonus::::new("heavy duty brake", 1.25 / 50.0), weapon, @@ -595,7 +589,7 @@ fn apply_passives( merits.pistol_mastery, education.cbt2840.then_some("CBT2840"), ), - WeaponCategory::Club => (merits.club_mastery, None), + WeaponCategory::Clubbing => (merits.club_mastery, None), WeaponCategory::Piercing => (merits.piercing_mastery, None), WeaponCategory::Slashing => (merits.slashing_mastery, None), WeaponCategory::Mechanical => (merits.mechanical_mastery, None), @@ -644,6 +638,14 @@ fn apply_passives( effects.spawn(SimpleStatBonus::::new("HIS2160", 0.10), weapon); } + if education.bio2380 { + commands + .spawn(PartDamageBonus::Education( + EducationPartDamageBonus::Bio2380, + )) + .set_parent(weapon); + } + if mastery > 0 { effects.spawn( SimpleStatBonus::::new("mastery", (mastery as f32) * 0.2 / 50.0), @@ -697,9 +699,9 @@ fn reload_weapon( ammo.0 = clip_size.value; clips.value -= 1; - logger.log(|| ReloadWeapon { + log!(logger, "reload_weapon", { actor: player.get(), - weapon, + weapon }); commands.entity(weapon).remove::(); @@ -735,12 +737,10 @@ fn apply_first_turn_effects( 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), - )); + // 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.turn.add_systems(reload_weapon); stages.post_turn.add_systems(unset_current); stages