aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2020-04-27 14:39:36 +0200
committerDaniel Schadt <kingdread@gmx.de>2020-04-27 14:39:36 +0200
commit08465ea1b8c1b9f90057bcc10fb8887ee57cac8c (patch)
tree7786e81336db4f814a323ed1cdacdced801f9abe
parent2a9aef0a371ffb860bfc48b691b9aaf0393e3df7 (diff)
downloadevtclib-08465ea1b8c1b9f90057bcc10fb8887ee57cac8c.tar.gz
evtclib-08465ea1b8c1b9f90057bcc10fb8887ee57cac8c.tar.bz2
evtclib-08465ea1b8c1b9f90057bcc10fb8887ee57cac8c.zip
remove statistics submodule
The way the trackers worked was rather... "adventurous", and while there were some good ideas and it mostly worked, the implementation and interface could do better. Additionally, it was incomplete, for example there were a lot of mechanics just missing. While I'm not against having this functionality provided by evtclib, I think it would be more worthwile with a better designed implementation & API, so this "proof of concept" implementation is gone until there is a better way of doing things. gamedata is being kept, as the boss identifiers are useful and applications shouldn't have to deal with keeping this low-level list themselves.
-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)
- }
-}