diff --git a/src/combat/breakbar.rs b/src/combat/breakbar.rs index 5cc5e21..c6c7bfc 100644 --- a/src/combat/breakbar.rs +++ b/src/combat/breakbar.rs @@ -1,5 +1,4 @@ use super::{agent::Target, skill::Skill}; -use arcdps::Agent; /// Information about a defiance damage hit. #[derive(Debug, Clone)] @@ -21,12 +20,12 @@ pub struct BreakbarHit { impl BreakbarHit { /// Creates a new breakbar hit. - pub fn new(time: i32, skill: Skill, damage: i32, target: &Agent) -> Self { + pub fn new(time: i32, skill: Skill, damage: i32, target: Target) -> Self { Self { time, skill, damage, - target: target.into(), + target, } } } diff --git a/src/combat/buff.rs b/src/combat/buff.rs index 66b69bc..23fa822 100644 --- a/src/combat/buff.rs +++ b/src/combat/buff.rs @@ -1,5 +1,5 @@ use super::agent::Target; -use arcdps::{evtc::AgentKind, Agent}; +use arcdps::evtc::AgentKind; pub use crate::data::Buff; @@ -21,12 +21,12 @@ pub struct BuffApply { impl BuffApply { /// Creates a new buff apply. - pub fn new(time: i32, buff: Buff, duration: i32, target: &Agent) -> Self { + pub fn new(time: i32, buff: Buff, duration: i32, target: Target) -> Self { Self { buff, time, duration, - target: target.into(), + target, } } diff --git a/src/combat/mod.rs b/src/combat/mod.rs index 7979603..4e5a015 100644 --- a/src/combat/mod.rs +++ b/src/combat/mod.rs @@ -3,16 +3,19 @@ pub mod breakbar; pub mod buff; pub mod cast; pub mod skill; +pub mod transfer; -use breakbar::BreakbarHit; -use buff::BuffApply; -use cast::Cast; +use self::breakbar::BreakbarHit; +use self::buff::BuffApply; +use self::cast::Cast; +use self::transfer::TransferTracker; #[derive(Debug, Clone)] pub struct CombatData { pub casts: Vec, pub buffs: Vec, pub breakbar: Vec, + pub transfers: TransferTracker, } impl CombatData { @@ -21,6 +24,7 @@ impl CombatData { casts: Vec::new(), buffs: Vec::new(), breakbar: Vec::new(), + transfers: TransferTracker::new(), } } } diff --git a/src/combat/transfer.rs b/src/combat/transfer.rs new file mode 100644 index 0000000..54fc299 --- /dev/null +++ b/src/combat/transfer.rs @@ -0,0 +1,140 @@ +use super::agent::Target; +use log::debug; + +pub use crate::data::Condition; + +/// Transfer tracking. +#[derive(Debug, Clone)] +pub struct TransferTracker { + /// Detected transfers. + pub transfers: Vec, + + /// Condition removes. + remove: Vec, + + /// Condition applies as transfer candidates. + apply: Vec, +} + +impl TransferTracker { + /// Time to retain candidates. + pub const RETAIN_TIME: i32 = 100; + + pub const fn new() -> Self { + Self { + transfers: Vec::new(), + remove: Vec::new(), + apply: Vec::new(), + } + } + + /// Adds a condition remove. + pub fn add_remove(&mut self, remove: Remove) { + self.purge(remove.time); + if let Some(apply) = Self::find_remove(&mut self.apply, |apply| apply.matches(&remove)) { + debug!("transfer: {remove:?} matches {apply:?}"); + self.transfers.push(apply) + } else { + self.remove.push(remove) + } + } + + /// Adds a condition apply as transfer candidate. + pub fn add_apply(&mut self, apply: Transfer) { + self.purge(apply.time); + if let Some(remove) = Self::find_remove(&mut self.remove, |remove| apply.matches(remove)) { + debug!("transfer: {apply:?} matches {remove:?}"); + self.transfers.push(apply) + } else { + self.apply.push(apply) + } + } + + fn find_remove(vec: &mut Vec, pred: impl FnMut(&T) -> bool) -> Option { + if let Some(index) = vec.iter().position(pred) { + Some(vec.swap_remove(index)) + } else { + None + } + } + + /// Purges old information. + fn purge(&mut self, now: i32) { + self.remove.retain(|el| Self::check_time(el.time, now)); + self.apply.retain(|el| Self::check_time(el.time, now)); + } + + /// Checks if the time should be kept. + fn check_time(time: i32, now: i32) -> bool { + time + Self::RETAIN_TIME >= now + } +} + +impl Default for TransferTracker { + fn default() -> Self { + Self::new() + } +} + +/// Information about a condition remove. +#[derive(Debug, Clone)] +pub struct Remove { + /// Time of the remove. + pub time: i32, + + /// Condition removed. + pub condi: Condition, + + /// Amount of stacks removed. + pub stacks: u32, +} + +impl Remove { + /// Creates a new condition transfer. + pub fn new(time: i32, condi: Condition, stacks: u32) -> Self { + Self { + time, + condi, + stacks, + } + } +} + +/// Information about a condition transfer. +#[derive(Debug, Clone)] +pub struct Transfer { + /// Time of the transfer. + pub time: i32, + + /// Condition transferred. + pub condi: Condition, + + /// Amount of stacks transferred. + pub stacks: u32, + + /// Target the condition was transferred to. + pub target: Target, +} + +impl Transfer { + /// Error margin for transfer times. + pub const TIME_EPSILON: i32 = 10; + + /// Creates a new condition transfer. + pub fn new(time: i32, condi: Condition, stacks: u32, target: Target) -> Self { + Self { + time, + condi, + stacks, + + target, + } + } + + /// Check whether the transfer candidate matches a remove. + pub fn matches(&self, remove: &Remove) -> bool { + self.condi == remove.condi + && self.stacks == remove.stacks + && (self.time - remove.time) <= Self::TIME_EPSILON + } +} diff --git a/src/data/buffs.rs b/src/data/buff.rs similarity index 100% rename from src/data/buffs.rs rename to src/data/buff.rs diff --git a/src/data/condi.rs b/src/data/condi.rs new file mode 100644 index 0000000..2e4c39b --- /dev/null +++ b/src/data/condi.rs @@ -0,0 +1,34 @@ +use num_enum::{IntoPrimitive, TryFromPrimitive}; +use strum::AsRefStr; + +/// Condition. +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Hash, + AsRefStr, + IntoPrimitive, + TryFromPrimitive, +)] +#[repr(u32)] +pub enum Condition { + Blind = 720, + Crippled = 721, + Chilled = 722, + Poison = 723, + Immobile = 727, + Bleeding = 736, + Burning = 737, + Vulnerability = 738, + Weakness = 742, + Fear = 791, + Confusion = 861, + Torment = 19426, + Slow = 26766, + Taunt = 27705, +} diff --git a/src/data/mod.rs b/src/data/mod.rs index cc80e2a..77786f4 100644 --- a/src/data/mod.rs +++ b/src/data/mod.rs @@ -1,7 +1,9 @@ -mod buffs; +mod buff; +mod condi; mod structs; -pub use self::buffs::*; +pub use self::buff::*; +pub use self::condi::*; pub use self::structs::*; use std::{ diff --git a/src/plugin/event.rs b/src/plugin/event.rs index 55b3185..019f355 100644 --- a/src/plugin/event.rs +++ b/src/plugin/event.rs @@ -1,9 +1,10 @@ use super::Plugin; use crate::combat::{ breakbar::BreakbarHit, - buff::BuffApply, + buff::{Buff, BuffApply}, cast::{Cast, CastState}, skill::Skill, + transfer::{Condition, Remove, Transfer}, }; use arcdps::{evtc::EventKind, Activation, Agent, CombatEvent, StateChange, Strike}; use log::debug; @@ -20,7 +21,7 @@ impl Plugin { ) { if let Some(src) = src { if let Some(event) = event { - let is_self = src.is_self != 0; + let src_self = src.is_self != 0; match event.kind() { EventKind::StateChange => match event.is_statechange { StateChange::LogStart => Self::lock().start_fight(event, dst), @@ -29,7 +30,7 @@ impl Plugin { _ => {} }, - EventKind::Activation if is_self => { + EventKind::Activation if src_self => { let mut plugin = Self::lock(); if let Some(time) = plugin.combat_time(&event) { if plugin.data.contains(event.skill_id) { @@ -51,13 +52,32 @@ impl Plugin { } EventKind::BuffApply => { - let mut plugin = Self::lock(); - if is_self || plugin.is_own_minion(&event) { - if let (Some(dst), Some(time)) = (dst, plugin.combat_time(&event)) { - // ignore applies to self or same agent + if let Some(dst) = dst { + let buff = event.skill_id; + if let Ok(buff) = buff.try_into() { + // ignore buff applies to self or same agent if dst.is_self == 0 && dst.id != src.id { - // TODO: "effective" duration excluding overstack? - plugin.apply_buff(event.skill_id, &dst, event.value, time); + Self::lock().apply_buff(&event, buff, &src, &dst) + } + } else if let Ok(condi) = buff.try_into() { + // only care about condis sourced from self + if src_self { + Self::lock().apply_condi(&event, condi, &dst) + } + } + } + } + + EventKind::BuffRemove => { + if let Some(dst) = dst { + // only care about self removes + // TODO: verify src dst for transfers + if src_self && dst.is_self != 0 { + if let Ok(condi) = event.skill_id.try_into() { + let mut plugin = Self::lock(); + if let Some(time) = plugin.combat_time(&event) { + plugin.remove_buff(condi, 1, time); + } } } } @@ -66,7 +86,7 @@ impl Plugin { EventKind::DirectDamage => { let mut plugin = Self::lock(); let is_minion = plugin.is_own_minion(&event); - if is_self || is_minion { + if src_self || is_minion { if let (Some(dst), Some(time)) = (dst, plugin.combat_time(&event)) { plugin.strike(&event, is_minion, skill_name, &dst, time) } @@ -101,24 +121,24 @@ impl Plugin { .map(|start| (event.time - start) as i32) } - fn start_fight(&mut self, event: CombatEvent, dst: Option) { + fn start_fight(&mut self, event: CombatEvent, target: Option) { let species = event.src_agent as u32; - debug!("log start for {species}, {dst:?}"); + debug!("log start for {species}, {target:?}"); self.start = Some(event.time); self.history - .add_fight_with_target(event.time, species, dst.as_ref()); + .add_fight_with_target(event.time, species, target.as_ref()); } - fn fight_target(&mut self, event: CombatEvent, dst: Option) { + fn fight_target(&mut self, event: CombatEvent, target: Option) { let species = event.src_agent as u32; - debug!("log target change to {species}, {dst:?}"); + debug!("log target change to {species}, {target:?}"); self.history - .update_fight_target(event.time, species, dst.as_ref()); + .update_fight_target(event.time, species, target.as_ref()); } - fn end_fight(&mut self, event: CombatEvent, dst: Option) { + fn end_fight(&mut self, event: CombatEvent, target: Option) { let species = event.src_agent; - debug!("log end for {species}, {dst:?}"); + debug!("log end for {species}, {target:?}"); self.start = None; self.history.end_latest_fight(event.time); } @@ -168,12 +188,32 @@ impl Plugin { } } - fn apply_buff(&mut self, buff: u32, target: &Agent, duration: i32, time: i32) { - if let (Some(fight), Ok(buff)) = (self.history.latest_fight_mut(), buff.try_into()) { - fight - .data - .buffs - .push(BuffApply::new(time, buff, duration, target)) + fn apply_buff(&mut self, event: &CombatEvent, buff: Buff, src: &Agent, dst: &Agent) { + if src.is_self != 0 || self.is_own_minion(&event) { + if let (Some(time), Some(fight)) = + (self.combat_time(&event), self.history.latest_fight_mut()) + { + // TODO: "effective" duration excluding overstack? + let duration = event.value; + let apply = BuffApply::new(time, buff, duration, dst.into()); + fight.data.buffs.push(apply) + } + } + } + + fn apply_condi(&mut self, event: &CombatEvent, condi: Condition, target: &Agent) { + if let (Some(time), Some(fight)) = + (self.combat_time(&event), self.history.latest_fight_mut()) + { + let apply = Transfer::new(time, condi, 1, target.into()); + fight.data.transfers.add_apply(apply); + } + } + + fn remove_buff(&mut self, condi: Condition, stacks: u32, time: i32) { + if let Some(fight) = self.history.latest_fight_mut() { + let remove = Remove::new(time, condi, stacks); + fight.data.transfers.add_remove(remove) } } @@ -222,7 +262,7 @@ impl Plugin { // TODO: display minion indicator? if let Some(fight) = self.history.latest_fight_mut() { debug!("breakbar {damage} {skill:?} {target:?}"); - let hit = BreakbarHit::new(time, skill, damage, target); + let hit = BreakbarHit::new(time, skill, damage, target.into()); fight.data.breakbar.push(hit); } }