added log! macro
This commit is contained in:
parent
86f9333aec
commit
b45d04b872
5 changed files with 140 additions and 522 deletions
|
|
@ -180,8 +180,11 @@ impl Simulation {
|
|||
|
||||
#[cfg(feature = "json")]
|
||||
pub fn read_log(&self) -> serde_json::Value {
|
||||
use entity_registry::EntityRegistry;
|
||||
|
||||
let log = self.0.world.resource::<Log>();
|
||||
log.to_value()
|
||||
let registry = self.0.world.resource::<EntityRegistry>();
|
||||
log.to_value(registry)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
166
src/log.rs
166
src/log.rs
|
|
@ -1,20 +1,18 @@
|
|||
use std::{collections::HashMap, sync::Mutex};
|
||||
use std::sync::Mutex;
|
||||
|
||||
use bevy_ecs::{prelude::*, system::SystemParam};
|
||||
use macros::LogMessage;
|
||||
|
||||
use crate::{
|
||||
entity_registry::EntityRegistry,
|
||||
hierarchy::Children,
|
||||
player::{
|
||||
stats::{
|
||||
AdditiveBonus, BaselineStat, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat,
|
||||
MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective,
|
||||
SimpleStatMarker, Speed, StatMarker, StatType, Strength, WeaponAccuracy,
|
||||
},
|
||||
Player,
|
||||
player::stats::{
|
||||
AdditiveBonus, BaselineStat, CritRate, DamageBonus, Defence, Dexterity, EffectiveStat,
|
||||
MultiplicativeBonus, SimpleStatBaseline, SimpleStatBonus, SimpleStatEffective,
|
||||
SimpleStatMarker, Speed, StatMarker, StatType, Strength, WeaponAccuracy,
|
||||
},
|
||||
weapon::{Weapon, WeaponVerb},
|
||||
Name, Stages,
|
||||
weapon::WeaponVerb,
|
||||
Stages,
|
||||
};
|
||||
|
||||
#[derive(Resource)]
|
||||
|
|
@ -49,6 +47,7 @@ pub enum LogValue<'a> {
|
|||
Unsigned(u32),
|
||||
Bool(bool),
|
||||
String(String),
|
||||
Entity(Entity),
|
||||
OptionNone,
|
||||
Display(&'a (dyn std::fmt::Display + Send + Sync)),
|
||||
Debug(&'a (dyn std::fmt::Debug + Send + Sync)),
|
||||
|
|
@ -86,6 +85,12 @@ impl From<bool> for LogValue<'static> {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<Entity> for LogValue<'static> {
|
||||
fn from(value: Entity) -> Self {
|
||||
Self::Entity(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, T> From<Option<T>> for LogValue<'a>
|
||||
where
|
||||
T: Into<LogValue<'a>>,
|
||||
|
|
@ -100,11 +105,7 @@ where
|
|||
|
||||
#[cfg(feature = "json")]
|
||||
impl<'a> LogValue<'a> {
|
||||
fn to_value(
|
||||
&self,
|
||||
player_registry: &HashMap<Entity, PlayerInfo>,
|
||||
weapon_registry: &HashMap<Entity, WeaponInfo>,
|
||||
) -> serde_json::Value {
|
||||
fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value {
|
||||
match self {
|
||||
LogValue::OptionNone => serde_json::Value::Null,
|
||||
LogValue::Float(val) => {
|
||||
|
|
@ -115,27 +116,20 @@ impl<'a> LogValue<'a> {
|
|||
LogValue::Unsigned(val) => serde_json::Value::Number(serde_json::Number::from(*val)),
|
||||
LogValue::Debug(boxed) => serde_json::Value::String(format!("{boxed:?}")),
|
||||
LogValue::Display(boxed) => serde_json::Value::String(format!("{boxed}")),
|
||||
LogValue::Player(id) => serde_json::json!({
|
||||
"type": "player",
|
||||
"name": player_registry.get(id).unwrap().name,
|
||||
}),
|
||||
LogValue::Weapon(id) => serde_json::json!({
|
||||
"type": "weapon",
|
||||
"name": weapon_registry.get(id).unwrap().name,
|
||||
}),
|
||||
LogValue::Player(id) => {
|
||||
serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap()
|
||||
}
|
||||
LogValue::Weapon(id) => {
|
||||
serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap()
|
||||
}
|
||||
LogValue::Entity(id) => {
|
||||
serde_json::to_value(entity_registry.0.get(id).unwrap()).unwrap()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait LogMessage: Send + Sync + 'static {
|
||||
fn torn_style(
|
||||
&self,
|
||||
_player_registry: HashMap<Entity, PlayerInfo>,
|
||||
_weapon_registry: HashMap<Entity, WeaponInfo>,
|
||||
) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn tag(&self) -> &'static str;
|
||||
|
||||
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)>;
|
||||
|
|
@ -143,9 +137,6 @@ pub trait LogMessage: Send + Sync + 'static {
|
|||
|
||||
#[derive(Resource, Default)]
|
||||
pub struct Log {
|
||||
pub player_registry: HashMap<Entity, PlayerInfo>,
|
||||
pub weapon_registry: HashMap<Entity, WeaponInfo>,
|
||||
|
||||
pub entries: Vec<Box<dyn LogMessage>>,
|
||||
|
||||
pub expanded: bool,
|
||||
|
|
@ -153,7 +144,7 @@ pub struct Log {
|
|||
|
||||
impl Log {
|
||||
#[cfg(feature = "json")]
|
||||
pub fn to_value(&self) -> serde_json::Value {
|
||||
pub fn to_value(&self, entity_registry: &EntityRegistry) -> serde_json::Value {
|
||||
use serde_json::json;
|
||||
|
||||
serde_json::json!({
|
||||
|
|
@ -161,7 +152,7 @@ impl Log {
|
|||
json!({
|
||||
"type": e.tag(),
|
||||
"values": serde_json::Value::Object(
|
||||
e.entries().iter().map(|e| (e.0.to_owned(), e.1.to_value(&self.player_registry, &self.weapon_registry))).collect()
|
||||
e.entries().iter().map(|e| (e.0.to_owned(), e.1.to_value(entity_registry))).collect()
|
||||
)
|
||||
})
|
||||
).collect::<Vec<_>>()
|
||||
|
|
@ -190,11 +181,8 @@ impl std::fmt::Display for Log {
|
|||
LogValue::OptionNone => write!(f, "None")?,
|
||||
LogValue::Display(val) => write!(f, "\"{val}\"")?,
|
||||
LogValue::Debug(val) => write!(f, "\"{val:?}\"")?,
|
||||
LogValue::Player(id) => {
|
||||
write!(f, "\"{}\"", self.player_registry.get(&id).unwrap().name)?
|
||||
}
|
||||
LogValue::Weapon(id) => {
|
||||
write!(f, "\"{}\"", self.weapon_registry.get(&id).unwrap().name)?
|
||||
LogValue::Player(id) | LogValue::Weapon(id) | LogValue::Entity(id) => {
|
||||
write!(f, "{:?}", id)?
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -418,31 +406,6 @@ fn logging_enabled(logging: Res<Logging>) -> bool {
|
|||
logging.0
|
||||
}
|
||||
|
||||
fn register_entities(
|
||||
player_q: Query<(Entity, &Name), With<Player>>,
|
||||
weapon_q: Query<(Entity, &Name, &WeaponVerb), With<Weapon>>,
|
||||
mut log: ResMut<Log>,
|
||||
) {
|
||||
for (player, name) in player_q.iter() {
|
||||
log.player_registry.insert(
|
||||
player,
|
||||
PlayerInfo {
|
||||
name: name.0.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (weapon, name, verb) in weapon_q.iter() {
|
||||
log.weapon_registry.insert(
|
||||
weapon,
|
||||
WeaponInfo {
|
||||
name: name.0.clone(),
|
||||
verb: *verb,
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn append_log_messages(mut events: EventReader<LogEvent>, mut log: ResMut<Log>) {
|
||||
for event in events.read() {
|
||||
log.entries.push(event.0.lock().unwrap().take().unwrap());
|
||||
|
|
@ -541,13 +504,59 @@ fn log_simple_stat_changes<Stat: SimpleStatMarker>(
|
|||
}
|
||||
}
|
||||
|
||||
pub struct DynamicLogMessage {
|
||||
pub label: &'static str,
|
||||
pub entries: Vec<(&'static str, LogValue<'static>)>,
|
||||
}
|
||||
|
||||
impl LogMessage for DynamicLogMessage {
|
||||
fn tag(&self) -> &'static str {
|
||||
self.label
|
||||
}
|
||||
|
||||
fn entries(&self) -> Vec<(&'static str, LogValue<'_>)> {
|
||||
self.entries.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log {
|
||||
($logger:expr, $tag:literal, { $($fields:tt)* }) => {
|
||||
$logger.log(|| $crate::log_message!($tag, { $($fields)* }))
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_message {
|
||||
($tag:literal, { $($fields:tt)* }) => {
|
||||
$crate::log::DynamicLogMessage { label: $tag, entries: $crate::log_values!($($fields)*) }
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! log_values {
|
||||
(@ { $(,)* $($out:expr),* $(,)* } $(,)*) => {
|
||||
vec![$($out),*]
|
||||
};
|
||||
(@ { $(,)* $($out:expr),* } $label:ident: $val:expr, $($rest:tt)*) => {
|
||||
$crate::log_values!(@ { $($out),*, (stringify!($label),$val.into()) } $($rest)*)
|
||||
};
|
||||
(@ { $(,)* $($out:expr),* } $label:ident: ?$val:expr, $($rest:tt)*) => {
|
||||
$crate::log_values!(@ { $($out),*, (stringify!($label),$crate::log::LogValue::String(format!("{:?}",$val))) } $($rest)*)
|
||||
};
|
||||
(@ { $(,)* $($out:expr),* } $label:ident: %$val:expr, $($rest:tt)*) => {
|
||||
$crate::log_values!(@ { $($out),*, (stringify!($label),$crate::log::LogValue::String(format!("{}",$val))) } $($rest)*)
|
||||
};
|
||||
|
||||
($($args:tt)* ) => {
|
||||
$crate::log_values!(@ { } $($args)*,)
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) fn configure(stages: &mut Stages) {
|
||||
stages.world.insert_resource(Log::default());
|
||||
stages.world.insert_resource(Logging(false));
|
||||
stages.add_event::<LogEvent>();
|
||||
stages
|
||||
.equip
|
||||
.add_systems(register_entities.run_if(logging_enabled));
|
||||
|
||||
stages.post_turn.add_systems(append_log_messages);
|
||||
stages.turn.add_systems(
|
||||
|
|
@ -563,3 +572,22 @@ pub(crate) fn configure(stages: &mut Stages) {
|
|||
.run_if(logging_enabled),
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn log_keys() {
|
||||
let message = log_message!("test", { foo: 12u32, bar: ?17 });
|
||||
|
||||
assert_eq!(message.label, "test");
|
||||
let mut entries = message.entries.into_iter();
|
||||
|
||||
let next = entries.next();
|
||||
assert!(matches!(next, Some(("foo", LogValue::Unsigned(12)))));
|
||||
|
||||
let next = entries.next();
|
||||
assert!(matches!(next, Some(("bar", LogValue::String(val))) if val == "17"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
use macros::LogMessage;
|
||||
use rand::Rng as _;
|
||||
use strum::Display;
|
||||
|
||||
|
|
@ -7,6 +6,7 @@ use crate::{
|
|||
armour,
|
||||
effect::Effects,
|
||||
hierarchy::Children,
|
||||
log,
|
||||
log::Logger,
|
||||
metrics::Metrics,
|
||||
passives::{Education, FactionUpgrades, Merits},
|
||||
|
|
@ -165,18 +165,6 @@ impl std::fmt::Display for BodyPart {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Event)]
|
||||
pub struct InitiateHit {
|
||||
pub body_part: BodyPart,
|
||||
pub weapon: Entity,
|
||||
pub rounds: Option<u16>,
|
||||
pub dmg: f32,
|
||||
pub dmg_bonus_weapon: f32,
|
||||
pub dmg_bonus_player: f32,
|
||||
pub hit_chance: f32,
|
||||
pub crit_rate: u16,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Display)]
|
||||
pub enum FightEndType {
|
||||
Victory,
|
||||
|
|
@ -184,16 +172,6 @@ pub enum FightEndType {
|
|||
Loss,
|
||||
}
|
||||
|
||||
#[derive(LogMessage)]
|
||||
struct FightEnd {
|
||||
#[log(player)]
|
||||
actor: Entity,
|
||||
#[log(player)]
|
||||
recipient: Entity,
|
||||
#[log(display)]
|
||||
fight_end_type: FightEndType,
|
||||
}
|
||||
|
||||
#[derive(Bundle)]
|
||||
pub struct PlayerBundle {
|
||||
pub name: Name,
|
||||
|
|
@ -338,164 +316,6 @@ impl FromWorld for DamageSpread {
|
|||
}
|
||||
}
|
||||
|
||||
fn receive_hit(
|
||||
(mut rng, spread): (ResMut<crate::Rng>, Local<DamageSpread>),
|
||||
mut hit_init_events: EventReader<InitiateHit>,
|
||||
current_q: Query<
|
||||
(
|
||||
Entity,
|
||||
&Education,
|
||||
Option<&Attacker>,
|
||||
&EffectiveStat<Strength>,
|
||||
),
|
||||
(With<Current>, With<Player>),
|
||||
>,
|
||||
mut target_q: Query<
|
||||
(
|
||||
Entity,
|
||||
&mut SimpleStatEffective<Health>,
|
||||
&armour::ArmourBodyParts,
|
||||
&EffectiveStat<Defence>,
|
||||
),
|
||||
With<CurrentTarget>,
|
||||
>,
|
||||
armour_q: Query<&armour::ArmourBodyPart>,
|
||||
(mut commands, mut logger): (Commands, Logger),
|
||||
metrics: Res<Metrics>,
|
||||
) {
|
||||
#[derive(LogMessage)]
|
||||
struct HitTarget {
|
||||
#[log(player)]
|
||||
actor: Entity,
|
||||
#[log(player)]
|
||||
recipient: Entity,
|
||||
#[log(weapon)]
|
||||
weapon: Entity,
|
||||
|
||||
#[log(display)]
|
||||
part: BodyPart,
|
||||
part_mult: f32,
|
||||
rounds: Option<u16>,
|
||||
|
||||
dmg: u32,
|
||||
health_before: u16,
|
||||
health_after: u16,
|
||||
|
||||
dmg_intrinsic: f32,
|
||||
dmg_spread: f32,
|
||||
|
||||
armour_mitigation: f32,
|
||||
def_mitigation: f32,
|
||||
|
||||
weapon_dmg: f32,
|
||||
bonus_dmg: f32,
|
||||
hit_chance: f32,
|
||||
|
||||
crit_rate: u16,
|
||||
}
|
||||
|
||||
if hit_init_events.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let (target, mut health, body_parts, target_def) = target_q.single_mut();
|
||||
let (current, edu, attacker, current_str) = current_q.single();
|
||||
|
||||
let def_str_ratio = (target_def.value / current_str.value).clamp(1.0 / 32.0, 14.0);
|
||||
let def_mitigation = if def_str_ratio < 1.0 {
|
||||
0.5 * def_str_ratio.log(32.0) + 0.5
|
||||
} else {
|
||||
0.5 * def_str_ratio.log(14.0) + 0.5
|
||||
};
|
||||
|
||||
let dmg_intrinsic = 7.0 * (current_str.value / 10.0).log10().powi(2)
|
||||
+ 27.0 * (current_str.value / 10.0).log10()
|
||||
+ 30.0;
|
||||
|
||||
for event in hit_init_events.read() {
|
||||
let mult = match event.body_part {
|
||||
BodyPart::Head | BodyPart::Heart | BodyPart::Throat => {
|
||||
metrics.increment_counter(current, "crit", 1);
|
||||
metrics.increment_counter(event.weapon, "crit", 1);
|
||||
1.0
|
||||
}
|
||||
BodyPart::LeftHand | BodyPart::RightHand | BodyPart::LeftFoot | BodyPart::RightFoot => {
|
||||
0.2
|
||||
}
|
||||
BodyPart::LeftArm | BodyPart::RightArm | BodyPart::LeftLeg | BodyPart::RightLeg => {
|
||||
1.0 / 3.5
|
||||
}
|
||||
BodyPart::Groin | BodyPart::Stomach | BodyPart::Chest => 1.0 / 1.75,
|
||||
};
|
||||
|
||||
metrics.increment_counter(current, "hit", 1);
|
||||
metrics.increment_counter(event.weapon, "hit", 1);
|
||||
|
||||
let armour_parts = armour_q.get(body_parts.0[event.body_part.into()]).unwrap();
|
||||
let piece = rng.sample(armour_parts);
|
||||
let armour_mitigation = piece.map_or(0.0, |p| p.armour_value);
|
||||
|
||||
// NOTE: The beta distribution is defined on [0,1], so we rescale here
|
||||
let dmg_spread = rng.sample(spread.0) / 10.0 + 1.0;
|
||||
|
||||
let mut dmg_bonus = event.dmg_bonus_weapon + event.dmg_bonus_player;
|
||||
|
||||
if edu.bio2380 && event.body_part == BodyPart::Throat {
|
||||
dmg_bonus += 0.10;
|
||||
}
|
||||
|
||||
let dmg = dmg_intrinsic
|
||||
* event.dmg
|
||||
* dmg_bonus
|
||||
* (1.0 - armour_mitigation)
|
||||
* (1.0 - def_mitigation)
|
||||
* mult
|
||||
* dmg_spread;
|
||||
let dmg = dmg.round() as u32;
|
||||
|
||||
metrics.record_histogram(current, "dmg", dmg);
|
||||
metrics.record_histogram(event.weapon, "dmg", dmg);
|
||||
|
||||
let health_before = health.value;
|
||||
|
||||
health.value = health.value.saturating_sub(dmg as u16);
|
||||
|
||||
logger.log(|| HitTarget {
|
||||
actor: current,
|
||||
recipient: target,
|
||||
weapon: event.weapon,
|
||||
part: event.body_part,
|
||||
part_mult: mult,
|
||||
rounds: event.rounds,
|
||||
dmg,
|
||||
health_before,
|
||||
health_after: health.value,
|
||||
dmg_spread,
|
||||
dmg_intrinsic,
|
||||
armour_mitigation,
|
||||
def_mitigation,
|
||||
weapon_dmg: event.dmg,
|
||||
bonus_dmg: dmg_bonus,
|
||||
hit_chance: event.hit_chance,
|
||||
crit_rate: event.crit_rate,
|
||||
});
|
||||
|
||||
if health.value == 0 {
|
||||
commands.entity(target).insert(Defeated);
|
||||
logger.log(|| FightEnd {
|
||||
actor: current,
|
||||
recipient: target,
|
||||
fight_end_type: if attacker.is_some() {
|
||||
FightEndType::Victory
|
||||
} else {
|
||||
FightEndType::Loss
|
||||
},
|
||||
});
|
||||
metrics.increment_counter(current, "victory", 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: unfortunately this function can't really be split into smaller parts due to the existence
|
||||
// of multi turn bonuses
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
|
|
@ -554,48 +374,6 @@ pub fn use_damaging_weapon(
|
|||
Res<Metrics>,
|
||||
),
|
||||
) {
|
||||
#[derive(LogMessage)]
|
||||
pub struct MissTarget {
|
||||
#[log(player)]
|
||||
pub actor: Entity,
|
||||
#[log(player)]
|
||||
pub recipient: Entity,
|
||||
#[log(weapon)]
|
||||
pub weapon: Entity,
|
||||
pub rounds: Option<u16>,
|
||||
}
|
||||
|
||||
#[derive(LogMessage)]
|
||||
struct HitTarget {
|
||||
#[log(player)]
|
||||
actor: Entity,
|
||||
#[log(player)]
|
||||
recipient: Entity,
|
||||
#[log(weapon)]
|
||||
weapon: Entity,
|
||||
|
||||
#[log(display)]
|
||||
part: BodyPart,
|
||||
part_mult: f32,
|
||||
rounds: Option<u16>,
|
||||
|
||||
dmg: u32,
|
||||
health_before: u16,
|
||||
health_after: u16,
|
||||
|
||||
dmg_intrinsic: f32,
|
||||
dmg_spread: f32,
|
||||
|
||||
armour_mitigation: f32,
|
||||
def_mitigation: f32,
|
||||
|
||||
weapon_dmg: f32,
|
||||
bonus_dmg: f32,
|
||||
hit_chance: f32,
|
||||
|
||||
crit_rate: u16,
|
||||
}
|
||||
|
||||
let Ok((weapon, w_dmg, acc, dmg_bonus, crit, children, non_targeted)) = weapon_q.get_single()
|
||||
else {
|
||||
return;
|
||||
|
|
@ -674,11 +452,11 @@ pub fn use_damaging_weapon(
|
|||
};
|
||||
|
||||
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||
logger.log(|| MissTarget {
|
||||
weapon,
|
||||
log!(logger, "miss_target", {
|
||||
weapon: weapon,
|
||||
actor: player,
|
||||
recipient: target,
|
||||
rounds,
|
||||
rounds: rounds,
|
||||
});
|
||||
metrics.increment_counter(player, "miss", 1);
|
||||
metrics.increment_counter(weapon, "miss", 1);
|
||||
|
|
@ -745,7 +523,7 @@ pub fn use_damaging_weapon(
|
|||
DamageProcEffect::MultiTurn { value, bonus }
|
||||
if multi_attack_proc.is_none() =>
|
||||
{
|
||||
if rng.gen_bool(*value as f64) {
|
||||
if rng.gen_bool((*value / 100.0) as f64) {
|
||||
match bonus {
|
||||
MultiTurnBonus::Blindfire => {
|
||||
multi_attack_proc = Some(MultiAttack::Blindfire)
|
||||
|
|
@ -763,6 +541,8 @@ pub fn use_damaging_weapon(
|
|||
Some(MultiAttack::DoubleTap { first_shot: true })
|
||||
}
|
||||
};
|
||||
metrics.increment_counter(player, bonus.counter_label(), 1);
|
||||
metrics.increment_counter(weapon, bonus.counter_label(), 1);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
@ -774,23 +554,22 @@ pub fn use_damaging_weapon(
|
|||
|
||||
health.value = health.value.saturating_sub(dmg as u16);
|
||||
|
||||
logger.log(|| HitTarget {
|
||||
log!(logger, "hit_target", {
|
||||
actor: player,
|
||||
recipient: target,
|
||||
weapon,
|
||||
part: body_part,
|
||||
weapon: weapon,
|
||||
part: %body_part,
|
||||
part_mult: mult,
|
||||
rounds,
|
||||
dmg,
|
||||
health_before,
|
||||
dmg: dmg,
|
||||
health_before: health_before,
|
||||
health_after: health.value,
|
||||
dmg_spread,
|
||||
dmg_intrinsic,
|
||||
armour_mitigation,
|
||||
def_mitigation,
|
||||
dmg_spread: dmg_spread,
|
||||
dmg_intrinsic: dmg_intrinsic,
|
||||
armour_mitigation: armour_mitigation,
|
||||
def_mitigation: def_mitigation,
|
||||
weapon_dmg: w_dmg.0,
|
||||
bonus_dmg: dmg_bonus.value,
|
||||
hit_chance,
|
||||
hit_chance: hit_chance,
|
||||
crit_rate: crit.value,
|
||||
});
|
||||
|
||||
|
|
@ -798,14 +577,14 @@ pub fn use_damaging_weapon(
|
|||
defeated = true;
|
||||
|
||||
commands.entity(target).insert(Defeated);
|
||||
logger.log(|| FightEnd {
|
||||
log!(logger, "fight_end", {
|
||||
actor: player,
|
||||
recipient: target,
|
||||
fight_end_type: if attacker {
|
||||
fight_end_type: %if attacker {
|
||||
FightEndType::Victory
|
||||
} else {
|
||||
FightEndType::Loss
|
||||
},
|
||||
}
|
||||
});
|
||||
metrics.increment_counter(player, "victory", 1);
|
||||
}
|
||||
|
|
@ -846,12 +625,12 @@ pub fn check_stalemate(
|
|||
if *state == FightStatus::Ongoing && current_turns.0 >= 25 && attacker.is_some() {
|
||||
commands.entity(current).insert(Defeated);
|
||||
let target = target_q.single();
|
||||
logger.log(|| FightEnd {
|
||||
|
||||
log!(logger, "fight_end", {
|
||||
actor: current,
|
||||
recipient: target,
|
||||
fight_end_type: FightEndType::Stalemate,
|
||||
fight_end_type: %FightEndType::Stalemate
|
||||
});
|
||||
|
||||
metrics.increment_counter(current, "stalemate", 1);
|
||||
|
||||
if other_attackers_q.is_empty() {
|
||||
|
|
@ -898,7 +677,6 @@ pub(crate) fn configure(stages: &mut Stages) {
|
|||
status_effect::configure(stages);
|
||||
|
||||
stages.add_event::<ChooseWeapon>();
|
||||
stages.add_event::<InitiateHit>();
|
||||
stages.equip.add_systems(designate_first);
|
||||
stages.pre_fight.add_systems(derive_max_health);
|
||||
stages.pre_turn.add_systems(pick_action);
|
||||
|
|
|
|||
|
|
@ -143,6 +143,18 @@ pub enum MultiTurnBonus {
|
|||
Rage,
|
||||
}
|
||||
|
||||
impl MultiTurnBonus {
|
||||
#[inline(always)]
|
||||
pub fn counter_label(self) -> &'static str {
|
||||
match self {
|
||||
Self::Blindfire => "proc_blindfire",
|
||||
Self::Fury => "proc_fury",
|
||||
Self::DoubleTap => "proc_double_tap",
|
||||
Self::Rage => "proc_rage",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn prepare_bonuses(
|
||||
bonus_q: Query<(
|
||||
&Parent,
|
||||
|
|
|
|||
|
|
@ -1,28 +1,22 @@
|
|||
use bevy_ecs::prelude::*;
|
||||
use macros::LogMessage;
|
||||
use rand::Rng as _;
|
||||
|
||||
use crate::{
|
||||
effect::{Effects, TurnLimitedEffect},
|
||||
hierarchy::{HierarchyBuilder, Parent},
|
||||
log::Logger,
|
||||
metrics::Metrics,
|
||||
passives::{Education, FactionUpgrades, Merits},
|
||||
player::{
|
||||
stats::{
|
||||
AdditiveBonus, AmmoControl, ClipSize, Clips, CritRate, DamageBonus, Dexterity,
|
||||
EffectiveStat, SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, Speed,
|
||||
WeaponAccuracy,
|
||||
SimpleStatBonus, SimpleStatBundle, SimpleStatEffective, WeaponAccuracy,
|
||||
},
|
||||
BodyPart, Current, CurrentTarget, InitiateHit, Player, Weapons,
|
||||
Current, Weapons,
|
||||
},
|
||||
Id, Name, Rng, Stages,
|
||||
Id, Name, Stages,
|
||||
};
|
||||
|
||||
use self::{
|
||||
bonus::{FirstTurnBonus, MultiTurnBonus, TurnTriggeredBonus},
|
||||
temp::{NonTargeted, Uses},
|
||||
};
|
||||
use self::bonus::{FirstTurnBonus, MultiTurnBonus, TurnTriggeredBonus};
|
||||
|
||||
pub mod bonus;
|
||||
pub mod temp;
|
||||
|
|
@ -685,203 +679,6 @@ fn unset_current(weapon_q: Query<Entity, (With<Current>, With<Weapon>)>, mut com
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: Move all mitigation aspects of this into the player system
|
||||
pub fn use_damaging_weapon(
|
||||
mut rng: ResMut<Rng>,
|
||||
weapon_q: Query<
|
||||
(
|
||||
Entity,
|
||||
&DamageStat,
|
||||
&SimpleStatEffective<WeaponAccuracy>,
|
||||
&SimpleStatEffective<DamageBonus>,
|
||||
&SimpleStatEffective<CritRate>,
|
||||
Has<NonTargeted>,
|
||||
),
|
||||
(With<Weapon>, With<Current>, Without<NeedsReload>),
|
||||
>,
|
||||
player_q: Query<
|
||||
(
|
||||
Entity,
|
||||
&EffectiveStat<Speed>,
|
||||
&SimpleStatEffective<CritRate>,
|
||||
&SimpleStatEffective<WeaponAccuracy>,
|
||||
&SimpleStatEffective<DamageBonus>,
|
||||
),
|
||||
(With<Player>, With<Current>),
|
||||
>,
|
||||
target_q: Query<(Entity, &EffectiveStat<Dexterity>), With<CurrentTarget>>,
|
||||
(mut ammo_q, mut temp_q): (
|
||||
Query<(
|
||||
&mut Ammo,
|
||||
&SimpleStatEffective<Clips>,
|
||||
&RateOfFire,
|
||||
&SimpleStatEffective<AmmoControl>,
|
||||
)>,
|
||||
Query<&mut Uses>,
|
||||
),
|
||||
mut hit_events: EventWriter<InitiateHit>,
|
||||
(mut logger, mut commands, metrics): (Logger, Commands, Res<Metrics>),
|
||||
) {
|
||||
let Ok((weapon, dmg, acc, dmg_bonus, crit, non_targeted)) = weapon_q.get_single() else {
|
||||
return;
|
||||
};
|
||||
let (player, player_spd, player_crit, acc_bonus, p_dmg_bonus) = player_q.single();
|
||||
let (target, target_dex) = target_q.single();
|
||||
|
||||
if let Ok(mut uses) = temp_q.get_mut(weapon) {
|
||||
uses.0 -= 1;
|
||||
if uses.0 == 0 {
|
||||
commands.entity(weapon).remove::<Usable>();
|
||||
}
|
||||
}
|
||||
|
||||
let spd_dex_ratio = (player_spd.value / target_dex.value).clamp(1.0 / 64.0, 64.0);
|
||||
let base_hit_chance = if spd_dex_ratio < 1.0 {
|
||||
0.5 * (8.0 * spd_dex_ratio.sqrt() - 1.0) / 7.0
|
||||
} else {
|
||||
1.0 - 0.5 * (8.0 / spd_dex_ratio.sqrt() - 1.0) / 7.0
|
||||
};
|
||||
|
||||
let mut acc_eff = acc + acc_bonus;
|
||||
|
||||
let mut ammo = ammo_q
|
||||
.get_mut(weapon)
|
||||
.ok()
|
||||
.map(|(ammo, clips, rof, ammo_ctrl)| {
|
||||
let ammo_ctrl = 1.0 - (ammo_ctrl).value;
|
||||
let rof_eff = ((rof.0[0] as f32) * ammo_ctrl)..((rof.0[1] as f32) * ammo_ctrl);
|
||||
(ammo, clips, rof_eff)
|
||||
});
|
||||
|
||||
enum MultiAttack {
|
||||
Blindfire,
|
||||
Rage(usize),
|
||||
Fury(usize),
|
||||
DoubleTap { fired_first: bool },
|
||||
}
|
||||
|
||||
let mut multi_attack_proc = None;
|
||||
let crit = player_crit + crit;
|
||||
|
||||
loop {
|
||||
let rounds = ammo.as_mut().map(|(ref mut ammo, clips, rof)| {
|
||||
let rounds = (rng.gen_range(rof.clone()).round() as u16).clamp(1, ammo.0);
|
||||
metrics.increment_counter(player, "rounds_fired", rounds.into());
|
||||
metrics.increment_counter(weapon, "rounds_fired", rounds.into());
|
||||
ammo.0 -= rounds;
|
||||
if ammo.0 == 0 {
|
||||
if clips.value == 0 {
|
||||
commands.entity(weapon).remove::<Usable>();
|
||||
} else {
|
||||
commands.entity(weapon).insert(NeedsReload);
|
||||
}
|
||||
}
|
||||
rounds
|
||||
});
|
||||
|
||||
let hit_chance = if base_hit_chance < 0.5 {
|
||||
base_hit_chance + acc_eff.value * base_hit_chance
|
||||
} else {
|
||||
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
|
||||
};
|
||||
|
||||
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||
logger.log(|| MissTarget {
|
||||
weapon,
|
||||
actor: player,
|
||||
recipient: target,
|
||||
rounds,
|
||||
});
|
||||
metrics.increment_counter(player, "miss", 1);
|
||||
metrics.increment_counter(weapon, "miss", 1);
|
||||
|
||||
if multi_attack_proc.is_none() {
|
||||
return;
|
||||
};
|
||||
} else {
|
||||
let body_part = if !non_targeted {
|
||||
rng.sample(crit)
|
||||
} else {
|
||||
BodyPart::Stomach
|
||||
};
|
||||
|
||||
hit_events.send(InitiateHit {
|
||||
body_part,
|
||||
weapon,
|
||||
rounds,
|
||||
dmg: dmg.0,
|
||||
dmg_bonus_weapon: dmg_bonus.value,
|
||||
dmg_bonus_player: p_dmg_bonus.value,
|
||||
hit_chance,
|
||||
crit_rate: crit.value,
|
||||
});
|
||||
}
|
||||
|
||||
match multi_attack_proc {
|
||||
Some(MultiAttack::Blindfire) => acc_eff.value -= 5.0 / 50.0,
|
||||
Some(MultiAttack::Rage(turns @ 1..)) => {
|
||||
multi_attack_proc = Some(MultiAttack::Rage(turns - 1))
|
||||
}
|
||||
Some(MultiAttack::Fury(turns @ 1..)) => {
|
||||
multi_attack_proc = Some(MultiAttack::Fury(turns - 1))
|
||||
}
|
||||
Some(MultiAttack::DoubleTap { fired_first: false }) => {
|
||||
multi_attack_proc = Some(MultiAttack::DoubleTap { fired_first: true })
|
||||
}
|
||||
_ => break,
|
||||
}
|
||||
}
|
||||
/*
|
||||
let hit_chance = if base_hit_chance < 0.5 {
|
||||
base_hit_chance + acc_eff.value * base_hit_chance
|
||||
} else {
|
||||
base_hit_chance + acc_eff.value * (1.0 - base_hit_chance)
|
||||
};
|
||||
|
||||
if hit_chance <= 1.0 && !rng.gen_bool(hit_chance as f64) {
|
||||
logger.log(|| MissTarget {
|
||||
weapon,
|
||||
actor: player,
|
||||
recipient: target,
|
||||
rounds: rounds.map(|(rounds, _)| rounds),
|
||||
});
|
||||
metrics.increment_counter(player, "miss", 1);
|
||||
metrics.increment_counter(weapon, "miss", 1);
|
||||
return;
|
||||
}
|
||||
|
||||
let crit = player_crit + crit;
|
||||
|
||||
let body_part = if !non_targeted {
|
||||
rng.sample(crit)
|
||||
} else {
|
||||
BodyPart::Stomach
|
||||
};
|
||||
|
||||
let def_str_ratio = (target_def.value / player_str.value).clamp(1.0 / 32.0, 14.0);
|
||||
let mitigation = if def_str_ratio < 1.0 {
|
||||
0.5 * def_str_ratio.log(32.0) + 0.5
|
||||
} else {
|
||||
0.5 * def_str_ratio.log(14.0) + 0.5
|
||||
};
|
||||
|
||||
let dmg_intrinsic = 7.0 * (player_str.value / 10.0).log10().powi(2)
|
||||
+ 27.0 * (player_str.value / 10.0).log10()
|
||||
+ 30.0;
|
||||
|
||||
init_hit.send(InitiateHit {
|
||||
body_part,
|
||||
weapon,
|
||||
rounds: rounds.map(|(rounds, _)| rounds),
|
||||
crit_rate: crit.value,
|
||||
dmg: dmg.0,
|
||||
dmg_bonus_weapon: (dmg_bonus + p_dmg_bonus).value,
|
||||
dmg_intrinsic,
|
||||
def_mitigation: mitigation,
|
||||
hit_chance,
|
||||
}); */
|
||||
}
|
||||
|
||||
fn reload_weapon(
|
||||
mut weapon_q: Query<
|
||||
(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue