aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/gamedata.rs149
-rw-r--r--src/lib.rs5
-rw-r--r--src/statistics/boon.rs361
-rw-r--r--src/statistics/damage.rs95
-rw-r--r--src/statistics/gamedata.rs294
-rw-r--r--src/statistics/math.rs240
-rw-r--r--src/statistics/mechanics.rs62
-rw-r--r--src/statistics/mod.rs145
-rw-r--r--src/statistics/trackers.rs443
9 files changed, 151 insertions, 1643 deletions
diff --git a/src/gamedata.rs b/src/gamedata.rs
new file mode 100644
index 0000000..c56af13
--- /dev/null
+++ b/src/gamedata.rs
@@ -0,0 +1,149 @@
+//! This module contains some low-level game data, such as different boss IDs.
+use std::{fmt, str::FromStr};
+use num_derive::FromPrimitive;
+
+/// Enum containing all bosses with their IDs.
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)]
+pub enum Boss {
+ // Wing 1
+ ValeGuardian = 0x3C4E,
+ Gorseval = 0x3C45,
+ Sabetha = 0x3C0F,
+
+ // Wing 2
+ Slothasor = 0x3EFB,
+ Matthias = 0x3EF3,
+
+ // Wing 3
+ KeepConstruct = 0x3F6B,
+ /// Xera ID for phase 1.
+ ///
+ /// This is only half of Xera's ID, as there will be a second agent for the
+ /// second phase. This agent will have another ID, see
+ /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html).
+ Xera = 0x3F76,
+
+ // Wing 4
+ Cairn = 0x432A,
+ MursaatOverseer = 0x4314,
+ Samarog = 0x4324,
+ Deimos = 0x4302,
+
+ // Wing 5
+ SoullessHorror = 0x4D37,
+ Dhuum = 0x4BFA,
+
+ // Wing 6
+ ConjuredAmalgamate = 0xABC6,
+ LargosTwins = 0x5271,
+ Qadim = 0x51C6,
+
+ // Wing 7
+ CardinalAdina = 0x55F6,
+ CardinalSabir = 0x55CC,
+ QadimThePeerless = 0x55F0,
+
+ // 100 CM
+ Skorvald = 0x44E0,
+ Artsariiv = 0x461D,
+ Arkk = 0x455F,
+
+ // 99 CM
+ MAMA = 0x427D,
+ Siax = 0x4284,
+ Ensolyss = 0x4234,
+
+ // Strike missions
+ IcebroodConstruct = 0x568A,
+ VoiceOfTheFallen = 0x5747,
+ FraenirOfJormag = 0x57DC,
+ Boneskinner = 0x57F9,
+ WhisperOfJormag = 0x58B7,
+}
+
+
+/// Error for when converting a string to the boss fails.
+#[derive(Debug, Clone, Hash, PartialEq, Eq)]
+pub struct ParseBossError(String);
+
+
+impl fmt::Display for ParseBossError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ write!(f, "Invalid boss identifier: {}", self.0)
+ }
+}
+
+
+impl FromStr for Boss {
+ type Err = ParseBossError;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ let lower = s.to_lowercase();
+ match &lower as &str {
+ "vg" | "vale guardian" => Ok(Boss::ValeGuardian),
+ "gorse" | "gorseval" => Ok(Boss::Gorseval),
+ "sab" | "sabetha" => Ok(Boss::Sabetha),
+
+ "sloth" | "slothasor" => Ok(Boss::Slothasor),
+ "matthias" => Ok(Boss::Matthias),
+
+ "kc" | "keep construct" => Ok(Boss::KeepConstruct),
+ "xera" => Ok(Boss::Xera),
+
+ "cairn" => Ok(Boss::Cairn),
+ "mo" | "mursaat overseer" => Ok(Boss::MursaatOverseer),
+ "sam" | "sama" | "samarog" => Ok(Boss::Samarog),
+ "deimos" => Ok(Boss::Deimos),
+
+ "desmina" | "sh" => Ok(Boss::SoullessHorror),
+ "dhuum" => Ok(Boss::Dhuum),
+
+ "ca" | "conjured almagamate" => Ok(Boss::ConjuredAmalgamate),
+ "largos" | "twins" => Ok(Boss::LargosTwins),
+ "qadim" => Ok(Boss::Qadim),
+
+ "adina" | "cardinal adina" => Ok(Boss::CardinalAdina),
+ "sabir" | "cardinal sabir" => Ok(Boss::CardinalSabir),
+ "qadimp" | "peerless qadim" | "qadim the peerless" => Ok(Boss::QadimThePeerless),
+
+ "skorvald" => Ok(Boss::Skorvald),
+ "artsariiv" => Ok(Boss::Artsariiv),
+ "arkk" => Ok(Boss::Arkk),
+
+ "mama" => Ok(Boss::MAMA),
+ "siax" => Ok(Boss::Siax),
+ "ensolyss" => Ok(Boss::Ensolyss),
+
+ "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct),
+ "super kodan brothers" => Ok(Boss::VoiceOfTheFallen),
+ "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag),
+ "boneskinner" => Ok(Boss::Boneskinner),
+ "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag),
+
+ _ => Err(ParseBossError(s.to_owned()))
+ }
+ }
+}
+
+
+/// ID for Xera in the second phase.
+///
+/// The original Xera will despawn, and after the tower phase, a separate spawn
+/// will take over. This new Xera will have this ID. Care needs to be taken when
+/// calculating boss damage on this encounter, as both Xeras have to be taken
+/// into account.
+pub const XERA_PHASE2_ID: u16 = 0x3F9E;
+
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ pub fn test_parsing() {
+ assert_eq!("vg".parse(), Ok(Boss::ValeGuardian));
+ assert_eq!("VG".parse(), Ok(Boss::ValeGuardian));
+
+ assert!("vga".parse::<Boss>().is_err());
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index c95b6f8..c7d4651 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -26,9 +26,8 @@ pub mod raw;
mod event;
pub use event::{Event, EventKind};
-pub mod statistics;
-
-use statistics::gamedata::{self, Boss};
+pub mod gamedata;
+pub use gamedata::Boss;
use std::fmt;
diff --git a/src/statistics/boon.rs b/src/statistics/boon.rs
deleted file mode 100644
index 196bd1d..0000000
--- a/src/statistics/boon.rs
+++ /dev/null
@@ -1,361 +0,0 @@
-//! Module providing functions and structs to deal with boon related statistics.
-use std::cmp;
-use std::fmt;
-use std::ops::Mul;
-
-use super::math::{Monoid, RecordFunc, Semigroup};
-
-use fnv::FnvHashMap;
-
-/// The type of a boon.
-#[derive(Copy, Clone, Debug, PartialEq, Eq)]
-pub enum BoonType {
- /// Boon stacks duration, e.g. Regeneration.
- Duration,
- /// Boon stacks intensity, e.g. Might.
- Intensity,
-}
-
-/// A struct that helps with simulating boon changes over time.
-///
-/// This basically simulates a single boon-queue (for a single boon).
-///
-/// # A quick word about how boon queues work
-///
-/// For each boon, you have an internal *boon queue*, limited to a specific
-/// capacity. When the current stack expires, the next one is taken from the
-/// queue.
-///
-/// The queue is sorted by boon strength. This means that "weak" boons are
-/// always at the end (and as such, are the first ones to be deleted when the
-/// queue is full). This prevents "bad" boons (e.g. the Quickness from Lightning
-/// Hammer #2) to override the "good" boons (e.g. the Quickness from your
-/// friendly neighborhood Chrono with 100% boon duration).
-///
-/// This also means that boons can be "lost". If the queue is full, the boon
-/// might not get applied, or it might replace another boon, thus wasting some
-/// of the boon duration.
-///
-/// Intensity-stacked boons (such as Might) work a bit differently: as time
-/// passes, all stacks are decreased simultaneously! As soon as a stack reaches
-/// 0, it is dropped.
-///
-/// You can find more information and the size of some of the queues on the wiki:
-/// https://wiki.guildwars2.com/wiki/Effect_stacking
-#[derive(Clone, Debug)]
-pub struct BoonQueue {
- capacity: u32,
- queue: Vec<u64>,
- boon_type: BoonType,
- next_update: u64,
- backlog: u64,
-}
-
-impl BoonQueue {
- /// Create a new boon queue.
- ///
- /// * `capacity` - The capacity of the queue.
- /// * `boon_type` - How the boons stack.
- pub fn new(capacity: u32, boon_type: BoonType) -> BoonQueue {
- BoonQueue {
- capacity,
- queue: Vec::new(),
- boon_type,
- next_update: 0,
- backlog: 0,
- }
- }
-
- fn fix_queue(&mut self) {
- // Sort reversed, so that the longest stack is at the front.
- self.queue.sort_unstable_by(|a, b| b.cmp(a));
- // Truncate queue by cutting of the shortest stacks
- if self.queue.len() > self.capacity as usize {
- self.queue.drain(self.capacity as usize..);
- }
- }
-
- /// Get the type of this boon.
- pub fn boon_type(&self) -> BoonType {
- self.boon_type
- }
-
- /// Add a boon stack to this queue.
- ///
- /// * `duration` - Duration (in milliseconds) of the added stack.
- pub fn add_stack(&mut self, duration: u64) {
- let backlog = self.backlog;
- self.do_simulate(backlog);
- self.queue.push(duration);
- self.fix_queue();
- self.next_update = self.next_update();
- }
-
- /// Return the amount of current stacks.
- ///
- /// If the boon type is a duration boon, this will always return 0 or 1.
- ///
- /// If the boon type is an intensity boon, it will return the number of
- /// stacks.
- pub fn current_stacks(&self) -> u32 {
- let result = match self.boon_type {
- BoonType::Intensity => self.queue.len(),
- BoonType::Duration => cmp::min(1, self.queue.len()),
- };
- result as u32
- }
-
- /// Simulate time passing.
- ///
- /// This will decrease the remaining duration of the stacks accordingly.
- ///
- /// * `duration` - The amount of time (in milliseconds) to simulate.
- pub fn simulate(&mut self, duration: u64) {
- if duration == 0 {
- return;
- }
- if duration < self.next_update {
- self.next_update -= duration;
- self.backlog += duration;
- } else {
- let total = self.backlog + duration;
- self.do_simulate(total);
- }
- }
-
- /// Simulate the thing without using the backlog.
- fn do_simulate(&mut self, duration: u64) {
- let mut remaining = duration;
- match self.boon_type {
- BoonType::Duration => {
- while remaining > 0 && !self.queue.is_empty() {
- let next = self.queue.remove(0);
- if next > remaining {
- self.queue.push(next - remaining);
- break;
- } else {
- remaining -= next;
- }
- }
- self.fix_queue();
- }
-
- BoonType::Intensity => {
- self.queue = self
- .queue
- .iter()
- .cloned()
- .filter(|v| *v > duration)
- .map(|v| v - duration)
- .collect();
- }
- }
- self.next_update = self.next_update();
- self.backlog = 0;
- }
-
- /// Remove all stacks.
- pub fn clear(&mut self) {
- self.queue.clear();
- self.next_update = 0;
- self.backlog = 0;
- }
-
- /// Cleanse a single stack
- pub fn drop_single(&mut self) {
- if self.is_empty() {
- return;
- }
- self.queue.pop();
- }
-
- /// Checks if any stacks are left.
- pub fn is_empty(&self) -> bool {
- self.queue.is_empty()
- }
-
- /// Calculate when the stacks will have the next visible change.
- ///
- /// This assumes that the stacks will not be modified during this time.
- ///
- /// The return value is the duration in milliseconds. If the boon queue is
- /// currently empty, 0 is returned.
- pub fn next_change(&self) -> u64 {
- match self.boon_type {
- BoonType::Duration => self.queue.iter().sum(),
- BoonType::Intensity => self.queue.last().cloned().unwrap_or(0),
- }
- }
-
- /// Calculate when the boon queue should be updated next.
- ///
- /// The next update always means that a stack runs out, even if it has no
- /// visible effect.
- ///
- /// For each queue: `next_update() <= next_change()`.
- ///
- /// A return value of 0 means that there's no update awaiting.
- pub fn next_update(&self) -> u64 {
- self.queue.last().cloned().unwrap_or(0)
- }
-}
-
-/// Amount of stacks of a boon.
-// Since this is also used to represent changes in stacks, we need access to
-// negative numbers too, as stacks can drop.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub struct Stacks(i32);
-
-impl Semigroup for Stacks {
- #[inline]
- fn combine(&self, other: &Self) -> Self {
- Stacks(self.0 + other.0)
- }
-}
-
-impl Monoid for Stacks {
- #[inline]
- fn mempty() -> Self {
- Stacks(0)
- }
-}
-
-// This shouldn't be negative, as total stacks are always greater than 0, thus
-// the area below the curve will always be positive.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-#[doc(hidden)]
-pub struct BoonArea(u64);
-
-impl Semigroup for BoonArea {
- #[inline]
- fn combine(&self, other: &Self) -> Self {
- BoonArea(self.0 + other.0)
- }
-}
-
-impl Monoid for BoonArea {
- #[inline]
- fn mempty() -> Self {
- BoonArea(0)
- }
-}
-
-impl Mul<u64> for Stacks {
- type Output = BoonArea;
-
- #[inline]
- fn mul(self, rhs: u64) -> BoonArea {
- BoonArea(self.0 as u64 * rhs)
- }
-}
-
-/// A boon log for a specific player.
-///
-/// This logs the amount of stacks of each boon a player had at any given time.
-#[derive(Clone, Default)]
-pub struct BoonLog {
- // Keep a separate RecordFunc for each boon.
- inner: FnvHashMap<u32, RecordFunc<u64, (), Stacks>>,
-}
-
-impl BoonLog {
- /// Create a new, empty boon log.
- pub fn new() -> Self {
- Default::default()
- }
-
- /// Add an event to the boon log.
- pub fn log(&mut self, time: u64, boon_id: u32, stacks: u32) {
- let func = self.inner.entry(boon_id).or_insert_with(Default::default);
- let current = func.tally();
- if current.0 == stacks as i32 {
- return;
- }
- let diff = stacks as i32 - current.0;
- func.insert(time, (), Stacks(diff));
- }
-
- /// Get the average amount of stacks between the two given time points.
- ///
- /// * `a` - Start time point.
- /// * `b` - End time point.
- /// * `boon_id` - ID of the boon that you want to get the average for.
- pub fn average_stacks(&self, a: u64, b: u64, boon_id: u32) -> f32 {
- assert!(b >= a, "timespan is negative?!");
- let func = if let Some(f) = self.inner.get(&boon_id) {
- f
- } else {
- return 0.;
- };
- let area = func.integral(&a, &b);
- area.0 as f32 / (b - a) as f32
- }
-
- /// Get the amount of stacks at the given time point.
- ///
- /// * `x` - Time point.
- /// * `boon_id` - ID of the boon that you want to get.
- pub fn stacks_at(&self, x: u64, boon_id: u32) -> u32 {
- self.inner
- .get(&boon_id)
- .map(|f| f.get(&x))
- .unwrap_or(Stacks(0))
- .0 as u32
- }
-}
-
-impl fmt::Debug for BoonLog {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "BoonLog {{ .. }}")
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[test]
- fn test_queue_capacity() {
- let mut queue = BoonQueue::new(5, BoonType::Intensity);
- assert_eq!(queue.current_stacks(), 0);
- for _ in 0..10 {
- queue.add_stack(10);
- }
- assert_eq!(queue.current_stacks(), 5);
- }
-
- #[test]
- fn test_simulate_duration() {
- let mut queue = BoonQueue::new(10, BoonType::Duration);
- queue.add_stack(10);
- queue.add_stack(20);
- assert_eq!(queue.current_stacks(), 1);
- queue.simulate(30);
- assert_eq!(queue.current_stacks(), 0);
-
- queue.add_stack(50);
- queue.simulate(30);
- assert_eq!(queue.current_stacks(), 1);
- queue.simulate(10);
- assert_eq!(queue.current_stacks(), 1);
- queue.simulate(15);
- assert_eq!(queue.current_stacks(), 0);
- }
-
- #[test]
- fn test_simulate_intensity() {
- let mut queue = BoonQueue::new(5, BoonType::Intensity);
-
- queue.add_stack(10);
- queue.add_stack(20);
- assert_eq!(queue.current_stacks(), 2);
-
- queue.simulate(5);
- assert_eq!(queue.current_stacks(), 2);
-
- queue.simulate(5);
- assert_eq!(queue.current_stacks(), 1);
- queue.simulate(15);
- assert_eq!(queue.current_stacks(), 0);
- }
-}
diff --git a/src/statistics/damage.rs b/src/statistics/damage.rs
deleted file mode 100644
index 0c26a9b..0000000
--- a/src/statistics/damage.rs
+++ /dev/null
@@ -1,95 +0,0 @@
-use super::math::{Monoid, RecordFunc, Semigroup};
-use std::fmt;
-
-/// Type of the damage.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub enum DamageType {
- Physical,
- Condition,
-}
-
-/// Meta information about a damage log entry.
-#[derive(Clone, Copy, Debug, PartialEq, Eq)]
-pub struct Meta {
- pub source: u64,
- pub target: u64,
- pub kind: DamageType,
- pub skill: u32,
-}
-
-/// A small wrapper that wraps a damage number.
-#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Damage(pub u64);
-
-impl Semigroup for Damage {
- #[inline]
- fn combine(&self, other: &Self) -> Self {
- Damage(self.0 + other.0)
- }
-}
-
-impl Monoid for Damage {
- #[inline]
- fn mempty() -> Self {
- Damage(0)
- }
-}
-
-/// Provides access to the damage log.
-#[derive(Clone, Default)]
-pub struct DamageLog {
- inner: RecordFunc<u64, Meta, Damage>,
-}
-
-impl DamageLog {
- pub fn new() -> Self {
- DamageLog {
- inner: RecordFunc::new(),
- }
- }
-
- pub fn log(
- &mut self,
- time: u64,
- source: u64,
- target: u64,
- kind: DamageType,
- skill: u32,
- value: u64,
- ) {
- self.inner.insert(
- time,
- Meta {
- source,
- target,
- kind,
- skill,
- },
- Damage(value),
- )
- }
-
- pub fn damage_between<F: FnMut(&Meta) -> bool>(
- &self,
- start: u64,
- stop: u64,
- filter: F,
- ) -> Damage {
- self.inner.between_only(&start, &stop, filter)
- }
-
- pub fn damage<F: FnMut(&Meta) -> bool>(&self, filter: F) -> Damage {
- self.inner.tally_only(filter)
- }
-}
-
-impl fmt::Debug for DamageLog {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(
- f,
- "DamageLog {{ {} events logged, {:?} total damage }}",
- self.inner.len(),
- self.inner.tally()
- )
- }
-}
diff --git a/src/statistics/gamedata.rs b/src/statistics/gamedata.rs
deleted file mode 100644
index d942939..0000000
--- a/src/statistics/gamedata.rs
+++ /dev/null
@@ -1,294 +0,0 @@
-//! This module contains some game data that is necessary to correctly calculate
-//! some statistics.
-use std::{fmt, str::FromStr};
-use super::boon::{BoonQueue, BoonType};
-use num_derive::FromPrimitive;
-
-/// Enum containing all bosses with their IDs.
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)]
-pub enum Boss {
- // Wing 1
- ValeGuardian = 0x3C4E,
- Gorseval = 0x3C45,
- Sabetha = 0x3C0F,
-
- // Wing 2
- Slothasor = 0x3EFB,
- Matthias = 0x3EF3,
-
- // Wing 3
- KeepConstruct = 0x3F6B,
- /// Xera ID for phase 1.
- ///
- /// This is only half of Xera's ID, as there will be a second agent for the
- /// second phase. This agent will have another ID, see
- /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html).
- Xera = 0x3F76,
-
- // Wing 4
- Cairn = 0x432A,
- MursaatOverseer = 0x4314,
- Samarog = 0x4324,
- Deimos = 0x4302,
-
- // Wing 5
- SoullessHorror = 0x4D37,
- Dhuum = 0x4BFA,
-
- // Wing 6
- ConjuredAmalgamate = 0xABC6,
- LargosTwins = 0x5271,
- Qadim = 0x51C6,
-
- // Wing 7
- CardinalAdina = 0x55F6,
- CardinalSabir = 0x55CC,
- QadimThePeerless = 0x55F0,
-
- // 100 CM
- Skorvald = 0x44E0,
- Artsariiv = 0x461D,
- Arkk = 0x455F,
-
- // 99 CM
- MAMA = 0x427D,
- Siax = 0x4284,
- Ensolyss = 0x4234,
-
- // Strike missions
- IcebroodConstruct = 0x568A,
- VoiceOfTheFallen = 0x5747,
- FraenirOfJormag = 0x57DC,
- Boneskinner = 0x57F9,
- WhisperOfJormag = 0x58B7,
-}
-
-
-/// Error for when converting a string to the boss fails.
-#[derive(Debug, Clone, Hash, PartialEq, Eq)]
-pub struct ParseBossError(String);
-
-
-impl fmt::Display for ParseBossError {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "Invalid boss identifier: {}", self.0)
- }
-}
-
-
-impl FromStr for Boss {
- type Err = ParseBossError;
-
- fn from_str(s: &str) -> Result<Self, Self::Err> {
- let lower = s.to_lowercase();
- match &lower as &str {
- "vg" | "vale guardian" => Ok(Boss::ValeGuardian),
- "gorse" | "gorseval" => Ok(Boss::Gorseval),
- "sab" | "sabetha" => Ok(Boss::Sabetha),
-
- "sloth" | "slothasor" => Ok(Boss::Slothasor),
- "matthias" => Ok(Boss::Matthias),
-
- "kc" | "keep construct" => Ok(Boss::KeepConstruct),
- "xera" => Ok(Boss::Xera),
-
- "cairn" => Ok(Boss::Cairn),
- "mo" | "mursaat overseer" => Ok(Boss::MursaatOverseer),
- "sam" | "sama" | "samarog" => Ok(Boss::Samarog),
- "deimos" => Ok(Boss::Deimos),
-
- "desmina" | "sh" => Ok(Boss::SoullessHorror),
- "dhuum" => Ok(Boss::Dhuum),
-
- "ca" | "conjured almagamate" => Ok(Boss::ConjuredAmalgamate),
- "largos" | "twins" => Ok(Boss::LargosTwins),
- "qadim" => Ok(Boss::Qadim),
-
- "adina" | "cardinal adina" => Ok(Boss::CardinalAdina),
- "sabir" | "cardinal sabir" => Ok(Boss::CardinalSabir),
- "qadimp" | "peerless qadim" | "qadim the peerless" => Ok(Boss::QadimThePeerless),
-
- "skorvald" => Ok(Boss::Skorvald),
- "artsariiv" => Ok(Boss::Artsariiv),
- "arkk" => Ok(Boss::Arkk),
-
- "mama" => Ok(Boss::MAMA),
- "siax" => Ok(Boss::Siax),
- "ensolyss" => Ok(Boss::Ensolyss),
-
- "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct),
- "super kodan brothers" => Ok(Boss::VoiceOfTheFallen),
- "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag),
- "boneskinner" => Ok(Boss::Boneskinner),
- "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag),
-
- _ => Err(ParseBossError(s.to_owned()))
- }
- }
-}
-
-
-/// ID for Xera in the second phase.
-///
-/// The original Xera will despawn, and after the tower phase, a separate spawn
-/// will take over. This new Xera will have this ID. Care needs to be taken when
-/// calculating boss damage on this encounter, as both Xeras have to be taken
-/// into account.
-pub const XERA_PHASE2_ID: u16 = 0x3F9E;
-
-/// Contains a boon.
-///
-/// Fields:
-/// * boon id
-/// * name (english) (just for easier debugging)
-/// * maximum number of stacks
-/// * boon type (intensity or duration)
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub struct Boon(pub u32, pub &'static str, pub u32, pub BoonType);
-
-impl Boon {
- pub fn create_queue(&self) -> BoonQueue {
- BoonQueue::new(self.2, self.3)
- }
-}
-
-/// A list of all boons (and conditions)
-pub static BOONS: &[Boon] = &[
- // Standard boons.
- // Boon queue sizes taken from the wiki:
- // https://wiki.guildwars2.com/wiki/Effect_stacking
- // IDs from wiki and skilldef.log:
- // https://www.deltaconnected.com/arcdps/evtc/
-
- // Duration based
- Boon(743, "Aegis", 5, BoonType::Duration),
- Boon(30328, "Alacrity", 9, BoonType::Duration),
- Boon(725, "Fury", 9, BoonType::Duration),
- Boon(717, "Protection", 5, BoonType::Duration),
- Boon(718, "Regeneration", 5, BoonType::Duration),
- Boon(26980, "Resistance", 5, BoonType::Duration),
- Boon(873, "Retaliation", 5, BoonType::Duration),
- Boon(719, "Swiftness", 9, BoonType::Duration),
- Boon(1187, "Quickness", 5, BoonType::Duration),
- Boon(726, "Vigor", 5, BoonType::Duration),
- // Intensity based
- Boon(740, "Might", 25, BoonType::Intensity),
- Boon(1122, "Stability", 25, BoonType::Intensity),
- // Standard conditions.
- // Duration based
- Boon(720, "Blinded", 5, BoonType::Duration),
- Boon(722, "Chilled", 5, BoonType::Duration),
- Boon(721, "Crippled", 5, BoonType::Duration),
- Boon(791, "Fear", 5, BoonType::Duration),
- Boon(727, "Immobile", 3, BoonType::Duration),
- Boon(26766, "Slow", 3, BoonType::Duration),
- Boon(742, "Weakness", 3, BoonType::Duration),
- // Intensity based
- Boon(736, "Bleeding", 1500, BoonType::Intensity),
- Boon(737, "Burning", 1500, BoonType::Intensity),
- Boon(861, "Confusion", 1500, BoonType::Intensity),
- Boon(723, "Poison", 1500, BoonType::Intensity),
- Boon(19426, "Torment", 1500, BoonType::Intensity),
- Boon(738, "Vulnerability", 25, BoonType::Intensity),
-];
-
-pub fn get_boon(boon_id: u32) -> Option<&'static Boon> {
- BOONS.iter().find(|b| b.0 == boon_id)
-}
-
-/// Contains pre-defined triggers for boss mechanics.
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub enum Trigger {
- /// Triggers when the given boon is applied to the player.
- BoonPlayer(u32),
- /// Triggers when the given boon is applied to the boss.
- BoonBoss(u32),
- /// Triggers when the given skill is used by a player.
- SkillByPlayer(u32),
- /// Triggers when the given skill is used on a player.
- SkillOnPlayer(u32),
- /// Triggers when the given boon is stripped from an enemy.
- BoonStripped(u32),
- /// Triggers when the given entity spawned.
- Spawn(u16),
- /// Triggers when the boss finishes channeling the given skill.
- ChannelComplete(u32),
-}
-
-/// Struct describing a boss mechanic.
-///
-/// Fields:
-/// * Boss id that this mechanic belongs to.
-/// * How the mechanic is triggered.
-/// * Technical term for the mechanic (for debugging purposes).
-#[derive(Debug, Clone, PartialEq, Eq, Hash)]
-pub struct Mechanic(pub u16, pub Trigger, pub &'static str);
-
-impl Mechanic {
- #[inline]
- pub fn boss_id(&self) -> u16 {
- self.0
- }
-
- #[inline]
- pub fn trigger(&self) -> &Trigger {
- &self.1
- }
-
- #[inline]
- pub fn name(&self) -> &'static str {
- self.2
- }
-}
-
-macro_rules! mechanics {
- ( $( $boss_id:expr => [ $($name:expr => $trigger:expr,)* ], )* ) => {
- &[
- $( $(Mechanic($boss_id as u16, $trigger, $name)),* ),*
- ]
- }
-}
-
-/// A slice of all mechanics that we know about.
-pub static MECHANICS: &[Mechanic] = mechanics! {
- // Wing 1
- Boss::ValeGuardian => [
- // Teleport:
- "Unstable Magic Spike" => Trigger::SkillOnPlayer(31392),
- ],
- Boss::Gorseval => [
- // Slam
- "Spectral Impact" => Trigger::SkillOnPlayer(31875),
- // Egg
- "Ghastly Prison" => Trigger::BoonPlayer(31623),
- ],
- Boss::Sabetha => [
- // Took the launch pad
- "Shell-Shocked" => Trigger::BoonPlayer(34108),
- ],
-
- // Wing 4
- Boss::Samarog => [
- "Prisoner Sweep" => Trigger::SkillOnPlayer(38168),
- "Shockwave" => Trigger::SkillOnPlayer(37996),
- ],
-};
-
-/// Get all mechanics that belong to the given boss.
-pub fn get_mechanics(boss_id: u16) -> Vec<&'static Mechanic> {
- MECHANICS.iter().filter(|m| m.0 == boss_id).collect()
-}
-
-
-#[cfg(test)]
-mod tests {
- use super::*;
-
- #[test]
- pub fn test_parsing() {
- assert_eq!("vg".parse(), Ok(Boss::ValeGuardian));
- assert_eq!("VG".parse(), Ok(Boss::ValeGuardian));
-
- assert!("vga".parse::<Boss>().is_err());
- }
-}
diff --git a/src/statistics/math.rs b/src/statistics/math.rs
deleted file mode 100644
index a0849a3..0000000
--- a/src/statistics/math.rs
+++ /dev/null
@@ -1,240 +0,0 @@
-//! This module provides some basic mathematical structures.
-
-use std::iter;
-use std::ops::{Mul, Sub};
-
-/// A semigroup.
-///
-/// This trait lets you combine elements by a binary operation.
-pub trait Semigroup {
- fn combine(&self, other: &Self) -> Self;
-}
-
-/// A monoid.
-///
-/// Extends the semigroup with a "neutral" element.
-///
-/// # Laws
-///
-/// ```raw
-/// mempty.combine(x) == x
-/// x.combine(mempty) == x
-/// ```
-pub trait Monoid: Semigroup {
- fn mempty() -> Self;
-}
-
-#[derive(Debug, Clone)]
-#[doc(hidden)]
-pub struct Record<X, T, D> {
- x: X,
- tag: T,
- data: D,
-}
-
-/// A function that records tagged data points.
-///
-/// This represents a "function" as a list of increases at soem discrete points.
-/// Think about it as a generalized damage log. Increases can be tagged by some
-/// arbitrary data, for example which the agent ID, the skill ID, the target,
-/// ...
-///
-/// This offers methods to get the value at a specific point (by "summing up"
-/// all increments before that point), between two points and in total. It also
-/// offers variants that allow you to filter the increments by their tag.
-///
-/// Type parameters:
-///
-/// * `X` domain of the function. Must have a defined `Ord`ering.
-/// * `T` tag for each data point. Can be arbitrary.
-/// * `D` actual data. Must be [`Monoid`](trait.Monoid.html), so that it can be
-/// summed up.
-#[derive(Clone)]
-pub struct RecordFunc<X, T, D> {
- data: Vec<Record<X, T, D>>,
-}
-
-impl<X, T, D> RecordFunc<X, T, D>
-where
- X: Ord,
- D: Monoid,
-{
- /// Create a new `RecordFunc`.
- pub fn new() -> Self {
- RecordFunc { data: Vec::new() }
- }
-
- #[doc(hidden)]
- pub fn data(&self) -> &[Record<X, T, D>] {
- &self.data
- }
-
- /// Insert a data point into the record func.
- ///
- /// Note that you should supply the *increment*, not the *absolute value*!
- pub fn insert(&mut self, x: X, tag: T, data: D) {
- // Usually, the list will be built up in order, which means we can
- // always append to the end. Check for this special case to make it
- // faster.
- if self.data.last().map(|r| r.x < x).unwrap_or(true) {
- self.data.push(Record { x, tag, data });
- } else {
- let index = match self.data.binary_search_by(|r| r.x.cmp(&x)) {
- Ok(i) => i,
- Err(i) => i,
- };
- self.data.insert(index, Record { x, tag, data });
- }
- //self.data.sort_by(|a, b| a.x.cmp(&b.x));
- }
-
- /// Get the amount of data points saved.
- pub fn len(&self) -> usize {
- self.data.len()
- }
-
- /// Check whether there are no records.
- pub fn is_emtpy(&self) -> bool {
- self.data.is_empty()
- }
-
- /// Get the absolute value at the specific point.
- #[inline]
- pub fn get(&self, x: &X) -> D {
- self.get_only(x, |_| true)
- }
-
- /// Get the absolute value at the specific point by only considering
- /// increments where the predicate holds.
- pub fn get_only<F: FnMut(&T) -> bool>(&self, x: &X, mut predicate: F) -> D {
- self.data
- .iter()
- .take_while(|record| record.x <= *x)
- .filter(|record| predicate(&record.tag))
- .fold(D::mempty(), |a, b| a.combine(&b.data))
- }
-
- /// Get the increments between the two given points.
- #[inline]
- pub fn between(&self, a: &X, b: &X) -> D {
- self.between_only(a, b, |_| true)
- }
-
- /// Get the increments between the two given points by only considering
- /// increments where the predicate holds.
- pub fn between_only<F: FnMut(&T) -> bool>(&self, a: &X, b: &X, mut predicate: F) -> D {
- self.data
- .iter()
- .skip_while(|record| record.x < *a)
- .take_while(|record| record.x <= *b)
- .filter(|record| predicate(&record.tag))
- .fold(D::mempty(), |a, b| a.combine(&b.data))
- }
-
- /// Get the sum of all increments.
- #[inline]
- pub fn tally(&self) -> D {
- self.tally_only(|_| true)
- }
-
- /// Get the sum of all increments by only considering increments where the
- /// predicate holds.
- pub fn tally_only<F: FnMut(&T) -> bool>(&self, mut predicate: F) -> D {
- self.data
- .iter()
- .filter(|record| predicate(&record.tag))
- .fold(D::mempty(), |a, b| a.combine(&b.data))
- }
-}
-
-impl<X, T, D, A> RecordFunc<X, T, D>
-where
- X: Ord + Sub<X, Output = X> + Copy,
- D: Monoid + Mul<X, Output = A>,
- A: Monoid,
-{
- #[inline]
- pub fn integral(&self, a: &X, b: &X) -> A {
- self.integral_only(a, b, |_| true)
- }
-
- pub fn integral_only<F: FnMut(&T) -> bool>(&self, a: &X, b: &X, mut predicate: F) -> A {
- let points = self
- .data
- .iter()
- .skip_while(|record| record.x < *a)
- .take_while(|record| record.x <= *b)
- .filter(|record| predicate(&record.tag))
- .map(|record| record.x)
- .chain(iter::once(*b))
- .collect::<Vec<_>>();
- let mut area = A::mempty();
- let mut last = *a;
- for point in points {
- let diff = point - last;
- let value = self.get_only(&last, &mut predicate);
- area = area.combine(&(value * diff));
- last = point;
- }
- area
- }
-}
-
-impl<X: Ord, T, D: Monoid> Default for RecordFunc<X, T, D> {
- fn default() -> Self {
- Self::new()
- }
-}
-
-#[cfg(test)]
-mod test {
- use super::*;
-
- #[derive(Debug, PartialEq, Eq)]
- struct Integer(u32);
-
- impl Semigroup for Integer {
- fn combine(&self, other: &Self) -> Self {
- Integer(self.0 + other.0)
- }
- }
-
- impl Monoid for Integer {
- fn mempty() -> Self {
- Integer(0)
- }
- }
-
- fn create() -> RecordFunc<u32, u8, Integer> {
- let mut result = RecordFunc::new();
-
- result.insert(6, 1, Integer(6));
- result.insert(4, 0, Integer(5));
- result.insert(0, 1, Integer(3));
- result.insert(2, 0, Integer(4));
-
- result
- }
-
- #[test]
- fn recordfunc_get() {
- let rf = create();
-
- assert_eq!(rf.get(&3), Integer(7));
- assert_eq!(rf.get(&4), Integer(12));
- }
-
- #[test]
- fn recordfunc_get_only() {
- let rf = create();
-
- assert_eq!(rf.get_only(&3, |t| *t == 0), Integer(4));
- }
-
- #[test]
- fn recordfunc_between() {
- let rf = create();
-
- assert_eq!(rf.between(&1, &5), Integer(9));
- }
-}
diff --git a/src/statistics/mechanics.rs b/src/statistics/mechanics.rs
deleted file mode 100644
index 0cf6f24..0000000
--- a/src/statistics/mechanics.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-use super::gamedata::Mechanic;
-use super::math::{Monoid, RecordFunc, Semigroup};
-
-use std::fmt;
-
-/// A simple wrapper for integers.
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
-struct Counter(u32);
-
-impl Semigroup for Counter {
- #[inline]
- fn combine(&self, other: &Counter) -> Counter {
- Counter(self.0 + other.0)
- }
-}
-
-impl Monoid for Counter {
- #[inline]
- fn mempty() -> Counter {
- Counter(0)
- }
-}
-
-/// Provides access to the mechanic log.
-#[derive(Clone, Default)]
-pub struct MechanicLog {
- inner: RecordFunc<u64, (&'static Mechanic, u64), Counter>,
-}
-
-impl MechanicLog {
- /// Increase the mechanic counter for the given mechanic and agent by one.
- pub fn increase(&mut self, time: u64, mechanic: &'static Mechanic, agent: u64) {
- self.inner.insert(time, (mechanic, agent), Counter(1));
- }
-
- /// Return the count of mechanics.
- ///
- /// A function can be provided to filter entries by mechanic type and agent.
- pub fn count<F: FnMut(&'static Mechanic, u64) -> bool>(&self, mut filter: F) -> u32 {
- self.inner.tally_only(|(a, b)| filter(a, *b)).0
- }
-
- /// Return the count of mechanics between the two given times.
- ///
- /// A function can be provided to filter entries by mechanic type and agent.
- pub fn count_between<F: FnMut(&'static Mechanic, u64) -> bool>(
- &self,
- start: u64,
- stop: u64,
- mut filter: F,
- ) -> u32 {
- self.inner
- .between_only(&start, &stop, |(a, b)| filter(a, *b))
- .0
- }
-}
-
-impl fmt::Debug for MechanicLog {
- fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
- write!(f, "MechanicLog {{ ... }}")
- }
-}
diff --git a/src/statistics/mod.rs b/src/statistics/mod.rs
deleted file mode 100644
index 3e42d9c..0000000
--- a/src/statistics/mod.rs
+++ /dev/null
@@ -1,145 +0,0 @@
-//! This module aids in the creation of actual boss statistics.
-use super::*;
-use std::collections::HashMap;
-use thiserror::Error;
-
-pub mod boon;
-pub mod damage;
-pub mod gamedata;
-pub mod math;
-pub mod mechanics;
-pub mod trackers;
-
-use self::boon::BoonLog;
-use self::damage::DamageLog;
-use self::mechanics::MechanicLog;
-use self::trackers::{RunnableTracker, Tracker};
-
-pub type StatResult<T> = Result<T, StatError>;
-
-#[derive(Error, Debug)]
-pub enum StatError {
- #[error("tracker returned an error: {0}")]
- TrackerError(#[from] Box<dyn std::error::Error>),
-}
-
-macro_rules! try_tracker {
- ($expr:expr) => {
- #[allow(unreachable_code)]
- match $expr {
- Ok(e) => e,
- Err(e) => return Err(StatError::TrackerError(e)),
- }
- };
-}
-
-/// A struct containing the calculated statistics for the log.
-#[derive(Clone, Debug)]
-pub struct Statistics {
- /// The complete damage log.
- pub damage_log: DamageLog,
- /// The complete mechanics log.
- pub mechanic_log: MechanicLog,
- /// A map mapping agent addresses to their stats.
- pub agent_stats: HashMap<u64, AgentStats>,
-}
-
-/// A struct describing the agent statistics.
-#[derive(Clone, Debug, Default)]
-pub struct AgentStats {
- /// Average stacks of boons.
- ///
- /// This also includes conditions.
- ///
- /// For duration-based boons, the average amount of stacks is the same as
- /// the uptime.
- pub boon_log: BoonLog,
- /// Time when the agent has entered combat (millseconds since log start).
- pub enter_combat: u64,
- /// Time when the agent has left combat (millseconds since log start).
- pub exit_combat: u64,
-}
-
-impl AgentStats {
- /// Returns the combat time of this agent in milliseconds.
- pub fn combat_time(&self) -> u64 {
- self.exit_combat - self.enter_combat
- }
-}
-
-/// Takes a bunch of trackers and runs them on the given log.
-///
-/// This method returns "nothing", as the statistics are saved in the trackers.
-/// It's the job of the caller to extract them appropriately.
-pub fn run_trackers(log: &Log, trackers: &mut [&mut dyn RunnableTracker]) -> StatResult<()> {
- for event in log.events() {
- for tracker in trackers.iter_mut() {
- try_tracker!((*tracker).run_feed(event));
- }
- }
- Ok(())
-}
-
-/// Calculate the statistics for the given log.
-pub fn calculate(log: &Log) -> StatResult<Statistics> {
- let mut agent_stats = HashMap::<u64, AgentStats>::new();
-
- let mut damage_tracker = trackers::DamageTracker::new(log);
- let mut log_start_tracker = trackers::LogStartTracker::new();
- let mut combat_time_tracker = trackers::CombatTimeTracker::new();
- let mut boon_tracker = trackers::BoonTracker::new();
-
- let mechanics = gamedata::get_mechanics(log.boss_id);
- let boss_addr = log.boss_agents().into_iter().map(|x| *x.addr()).collect();
- let mut mechanic_tracker = trackers::MechanicTracker::new(boss_addr, mechanics);
-
- run_trackers(
- log,
- &mut [
- &mut damage_tracker,
- &mut log_start_tracker,
- &mut combat_time_tracker,
- &mut boon_tracker,
- &mut mechanic_tracker,
- ],
- )?;
-
- let log_start_time = try_tracker!(log_start_tracker.finalize());
-
- let combat_times = try_tracker!(combat_time_tracker.finalize());
- for (agent_addr, &(enter_time, exit_time)) in &combat_times {
- let agent = agent_stats
- .entry(*agent_addr)
- .or_insert_with(Default::default);
- // XXX: This used to be enter_time - log_start_time, as it makes more
- // sense to have the time relative to the log start instead of the
- // Windows boot time. However, this also means that we need to modify
- // all event times before we do any tracking, as many trackers rely on
- // event.time to track information related to time.
- if enter_time != 0 {
- agent.enter_combat = enter_time;
- } else {
- agent.enter_combat = log_start_time;
- }
- if exit_time != 0 {
- agent.exit_combat = exit_time;
- }
- }
-
- let boon_logs = try_tracker!(boon_tracker.finalize());
- for (agent_addr, boon_log) in boon_logs {
- let agent = agent_stats
- .entry(agent_addr)
- .or_insert_with(Default::default);
- agent.boon_log = boon_log;
- }
-
- let damage_log = try_tracker!(damage_tracker.finalize());
- let mechanic_log = try_tracker!(mechanic_tracker.finalize());
-
- Ok(Statistics {
- damage_log,
- mechanic_log,
- agent_stats,
- })
-}
diff --git a/src/statistics/trackers.rs b/src/statistics/trackers.rs
deleted file mode 100644
index 16cb755..0000000
--- a/src/statistics/trackers.rs
+++ /dev/null
@@ -1,443 +0,0 @@
-//! evtclib tracker definitions.
-//!
-//! The idea behind a "tracker" is to have one object taking care of one
-//! specific thing. This makes it easier to organize the whole "statistic
-//! gathering loop", and it keeps each tracker somewhat small.
-//!
-//! It's also easy to define your own trackers if there are any statistics that
-//! you want to track. Just implement [`Tracker`](trait.Tracker.html). It
-//! doesn't matter what you track, it doesn't matter how many trackers you
-//! define.
-//!
-//! If you want to track stats separated by player or phases, consider writing
-//! your tracker in a way that it only tracks statistics for a single player,
-//! and then use a [`Multiplexer`](struct.Multiplexer.html) to automatically
-//! track it for every player/agent.
-//!
-//! You can use [`run_trackers`](../fn.run_trackers.html) to run multiple
-//! trackers on the same log.
-use std::collections::HashMap;
-use std::error::Error;
-
-use super::super::{Event, EventKind, Log};
-use super::boon::{BoonLog, BoonQueue};
-use super::damage::{DamageLog, DamageType};
-use super::gamedata::{self, Mechanic, Trigger};
-use super::mechanics::MechanicLog;
-
-use super::super::raw::CbtResult;
-
-use fnv::FnvHashMap;
-
-/// A tracker.
-///
-/// A tracker should be responsible for tracking a single statistic. Each
-/// tracker is fed each event. If an error is returned while feeding, the whole
-/// calculation will be aborted.
-pub trait Tracker {
- /// The resulting statistic that this tracker will return.
- type Stat;
- /// The error that this tracker might return.
- type Error: Error;
-
- /// Feed a single event into this tracker.
- ///
- /// The tracker will update its internal state.
- fn feed(&mut self, event: &Event) -> Result<(), Self::Error>;
-
- /// Finalize this tracker and get the statistics out.
- fn finalize(self) -> Result<Self::Stat, Self::Error>;
-}
-
-/// A helper trait that erases the types from a tracker.
-///
-/// This makes it able to use references like `&mut RunnableTracker` without
-/// having to specify the generic types, allowing you to e.g. store a bunch of
-/// them in a vector, regardless of their output type. Unless you want to do
-/// that, you probably don't want to use this trait directly.
-///
-/// Note that you do not need to implement this yourself. It is automatically
-/// implemented for all types that implement `Tracker`.
-///
-/// RunnableTrackers provide no way to extract their resources, and they wrap
-/// all errors in `Box<_>`, so you should always keep a "real" reference around
-/// if you plan on getting any data.
-pub trait RunnableTracker {
- /// See `Tracker.feed()`. Renamed to avoid conflicts.
- fn run_feed(&mut self, event: &Event) -> Result<(), Box<dyn Error>>;
-}
-
-impl<S, E: Error + 'static, T: Tracker<Stat = S, Error = E>> RunnableTracker for T {
- fn run_feed(&mut self, event: &Event) -> Result<(), Box<dyn Error>> {
- self.feed(event).map_err(|e| Box::new(e) as Box<dyn Error>)
- }
-}
-
-/// A tracker that tracks per-target damage of all agents.
-pub struct DamageTracker<'l> {
- log: &'l Log,
- damage_log: DamageLog,
-}
-
-impl<'t> DamageTracker<'t> {
- /// Create a new damage tracker for the given log.
- pub fn new(log: &Log) -> DamageTracker {
- DamageTracker {
- log,
- damage_log: DamageLog::new(),
- }
- }
-}
-
-impl<'t> Tracker for DamageTracker<'t> {
- type Stat = DamageLog;
- type Error = !;
-
- fn feed(&mut self, event: &Event) -> Result<(), Self::Error> {
- match event.kind {
- EventKind::Physical {
- source_agent_addr,
- destination_agent_addr,
- damage,
- skill_id,
- ..
- } => {
- let source = if let Some(master) = self.log.master_agent(source_agent_addr) {
- master.addr
- } else {
- source_agent_addr
- };
- self.damage_log.log(
- event.time,
- source,
- destination_agent_addr,
- DamageType::Physical,
- skill_id,
- damage as u64,
- );
- }
-
- EventKind::ConditionTick {
- source_agent_addr,
- destination_agent_addr,
- damage,
- condition_id,
- ..
- } => {
- let source = if let Some(master) = self.log.master_agent(source_agent_addr) {
- master.addr
- } else {
- source_agent_addr
- };
- self.damage_log.log(
- event.time,
- source,
- destination_agent_addr,
- DamageType::Condition,
- condition_id,
- damage as u64,
- );
- }
-
- _ => (),
- }
- Ok(())
- }
-
- fn finalize(self) -> Result<Self::Stat, Self::Error> {
- Ok(self.damage_log)
- }
-}
-
-/// Tracks when the log has been started.
-#[derive(Default)]
-pub struct LogStartTracker {
- event_time: u64,
-}
-
-impl LogStartTracker {
- /// Create a new log start tracker.
- pub fn new() -> LogStartTracker {
- LogStartTracker { event_time: 0 }
- }
-}
-
-impl Tracker for LogStartTracker {
- type Stat = u64;
- type Error = !;
-
- fn feed(&mut self, event: &Event) -> Result<(), Self::Error> {
- if let EventKind::LogStart { .. } = event.kind {
- self.event_time = event.time;
- }
- Ok(())
- }
-
- fn finalize(self) -> Result<Self::Stat, Self::Error> {
- Ok(self.event_time)
- }
-}
-
-/// A tracker that tracks the combat entry and exit times for each agent.
-#[derive(Default)]
-pub struct CombatTimeTracker {
- times: HashMap<u64, (u64, u64)>,
-}
-
-impl CombatTimeTracker {
- /// Create a new combat time tracker.
- pub fn new() -> CombatTimeTracker {
- Default::default()
- }
-}
-
-impl Tracker for CombatTimeTracker {
- // Maps from agent addr to (entry time, exit time)
- type Stat = HashMap<u64, (u64, u64)>;
- type Error = !;
-
- fn feed(&mut self, event: &Event) -> Result<(), Self::Error> {
- match event.kind {
- EventKind::EnterCombat { agent_addr, .. } => {
- self.times.entry(agent_addr).or_insert((0, 0)).0 = event.time;
- }
-
- EventKind::ExitCombat { agent_addr } => {
- self.times.entry(agent_addr).or_insert((0, 0)).1 = event.time;
- }
-
- _ => (),
- }
- Ok(())
- }
-
- fn finalize(self) -> Result<Self::Stat, Self::Error> {
- Ok(self.times)
- }
-}
-
-/// A tracker that tracks the total "boon area" per agent.
-///
-/// The boon area is defined as the amount of stacks multiplied by the time. So
-/// 1 stack of Might for 1000 milliseconds equals 1000 "stackmilliseconds" of
-/// Might. You can use this boon area to calculate the average amount of stacks
-/// by taking the boon area and dividing it by the combat time.
-///
-/// Note that this also tracks conditions, because internally, they're handled
-/// the same way.
-///
-/// This tracker only tracks the boons that are known to evtclib, that is the
-/// boons defined in `evtclib::statistics::gamedata::BOONS`.
-pub struct BoonTracker {
- boon_logs: FnvHashMap<u64, BoonLog>,
- boon_queues: FnvHashMap<u64, FnvHashMap<u32, BoonQueue>>,
- last_time: u64,
-}
-
-impl BoonTracker {
- /// Creates a new boon tracker for the given agent.
- pub fn new() -> BoonTracker {
- BoonTracker {
- boon_logs: Default::default(),
- boon_queues: Default::default(),
- last_time: 0,
- }
- }
-
- /// Updates the internal boon queues by the given amount of milliseconds.
- ///
- /// * `delta_t` - Amount of milliseconds to update.
- fn update_queues(&mut self, delta_t: u64) {
- if delta_t == 0 {
- return;
- }
-
- self.boon_queues
- .values_mut()
- .flat_map(|m| m.values_mut())
- .for_each(|queue| queue.simulate(delta_t));
- }
-
- fn cleanup_queues(&mut self) {
- // Throw away empty boon queues or to improve performance
- self.boon_queues
- .values_mut()
- .for_each(|m| m.retain(|_, q| !q.is_empty()));
- self.boon_queues.retain(|_, q| !q.is_empty());
- }
-
- fn update_logs(&mut self, time: u64) {
- for (agent, boons) in &self.boon_queues {
- let agent_log = self
- .boon_logs
- .entry(*agent)
- .or_insert_with(Default::default);
- for (boon_id, queue) in boons {
- agent_log.log(time, *boon_id, queue.current_stacks());
- }
- }
- }
-
- /// Get the boon queue for the given buff_id.
- ///
- /// If the queue does not yet exist, create it.
- ///
- /// * `agent_addr` - The address of the agent.
- /// * `buff_id` - The buff (or condition) id.
- fn get_queue(&mut self, agent_addr: u64, buff_id: u32) -> Option<&mut BoonQueue> {
- use std::collections::hash_map::Entry;
- let entry = self
- .boon_queues
- .entry(agent_addr)
- .or_insert_with(Default::default)
- .entry(buff_id);
- match entry {
- // Queue already exists
- Entry::Occupied(e) => Some(e.into_mut()),
- // Queue needs to be created, but only if we know about that boon.
- Entry::Vacant(e) => {
- let boon_queue = gamedata::get_boon(buff_id).map(gamedata::Boon::create_queue);
- if let Some(queue) = boon_queue {
- Some(e.insert(queue))
- } else {
- None
- }
- }
- }
- }
-}
-
-impl Tracker for BoonTracker {
- type Stat = HashMap<u64, BoonLog>;
- type Error = !;
-
- fn feed(&mut self, event: &Event) -> Result<(), Self::Error> {
- let delta_t = event.time - self.last_time;
- self.update_queues(delta_t);
-
- match event.kind {
- EventKind::BuffApplication {
- destination_agent_addr,
- buff_id,
- duration,
- ..
- } => {
- if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) {
- queue.add_stack(duration as u64);
- }
- }
-
- // XXX: Properly handle SINGLE and MANUAL removal types
- EventKind::BuffRemove {
- destination_agent_addr,
- buff_id,
- ..
- } => {
- if let Some(queue) = self.get_queue(destination_agent_addr, buff_id) {
- queue.clear();
- }
- }
-
- _ => (),
- }
-
- self.update_logs(event.time);
- self.last_time = event.time;
- self.cleanup_queues();
-
- Ok(())
- }
-
- fn finalize(self) -> Result<Self::Stat, Self::Error> {
- // Convert from FnvHashMap to HashMap in order to not leak
- // implementation details.
- Ok(self.boon_logs.into_iter().collect())
- }
-}
-
-/// A tracker that tracks boss mechanics for each player.
-pub struct MechanicTracker {
- log: MechanicLog,
- available_mechanics: Vec<&'static Mechanic>,
- boss_addresses: Vec<u64>,
-}
-
-impl MechanicTracker {
- /// Create a new mechanic tracker that watches over the given mechanics.
- pub fn new(boss_addresses: Vec<u64>, mechanics: Vec<&'static Mechanic>) -> MechanicTracker {
- MechanicTracker {
- log: MechanicLog::default(),
- available_mechanics: mechanics,
- boss_addresses,
- }
- }
-}
-
-impl MechanicTracker {
- fn is_boss(&self, addr: u64) -> bool {
- self.boss_addresses.contains(&addr)
- }
-}
-
-impl Tracker for MechanicTracker {
- type Stat = MechanicLog;
- type Error = !;
-
- fn feed(&mut self, event: &Event) -> Result<(), Self::Error> {
- for mechanic in &self.available_mechanics {
- match (&event.kind, &mechanic.1) {
- (
- EventKind::Physical {
- source_agent_addr,
- destination_agent_addr,
- skill_id,
- result,
- ..
- },
- Trigger::SkillOnPlayer(trigger_id),
- )
- if skill_id == trigger_id
- && self.is_boss(*source_agent_addr)
- && *result != CbtResult::Evade
- && *result != CbtResult::Absorb
- && *result != CbtResult::Block =>
- {
- self.log
- .increase(event.time, mechanic, *destination_agent_addr);
- }
-
- (
- EventKind::BuffApplication {
- destination_agent_addr,
- buff_id,
- ..
- },
- Trigger::BoonPlayer(trigger_id),
- )
- if buff_id == trigger_id =>
- {
- // Some buff applications are registered multiple times. So
- // instead of counting those quick successions separately
- // (and thus having a wrong count), we check if this
- // mechanic has already been logged "shortly before" (10 millisecons).
- if self
- .log
- .count_between(event.time - 10, event.time + 1, |m, w| {
- &m == mechanic && w == *destination_agent_addr
- })
- == 0
- {
- self.log
- .increase(event.time, mechanic, *destination_agent_addr);
- }
- }
- _ => (),
- }
- }
- Ok(())
- }
-
- fn finalize(self) -> Result<Self::Stat, Self::Error> {
- Ok(self.log)
- }
-}