aboutsummaryrefslogtreecommitdiff
path: root/src/statistics
diff options
context:
space:
mode:
Diffstat (limited to 'src/statistics')
-rw-r--r--src/statistics/boon.rs175
-rw-r--r--src/statistics/mod.rs172
2 files changed, 347 insertions, 0 deletions
diff --git a/src/statistics/boon.rs b/src/statistics/boon.rs
new file mode 100644
index 0000000..65e5c89
--- /dev/null
+++ b/src/statistics/boon.rs
@@ -0,0 +1,175 @@
+use std::cmp;
+
+/// 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.
+#[derive(Clone, Debug)]
+pub struct BoonQueue {
+ capacity: u32,
+ queue: Vec<u64>,
+ boon_type: BoonType,
+}
+
+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,
+ }
+ }
+
+ 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) {
+ self.queue.push(duration);
+ self.fix_queue();
+ }
+
+ /// 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) {
+ 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();
+ }
+ }
+ }
+
+ /// Remove all stacks.
+ pub fn clear(&mut self) {
+ self.queue.clear();
+ }
+
+ /// 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),
+ }
+ }
+}
+
+#[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/mod.rs b/src/statistics/mod.rs
new file mode 100644
index 0000000..a9dd178
--- /dev/null
+++ b/src/statistics/mod.rs
@@ -0,0 +1,172 @@
+//! This module aids in the creation of actual boss statistics.
+use super::*;
+use std::collections::HashMap;
+
+pub mod boon;
+
+pub type StatResult<T> = Result<T, StatError>;
+
+quick_error! {
+ #[derive(Clone, Debug)]
+ pub enum StatError {
+ }
+}
+
+/// A struct containing the calculated statistics for the log.
+#[derive(Clone, Debug)]
+pub struct Statistics {
+ /// 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 {
+ /// Damage done per target during the fight.
+ ///
+ /// Maps from target address to the damage done to this target.
+ pub per_target_damage: HashMap<u64, DamageStats>,
+ /// Total damage dealt during the fight.
+ pub total_damage: DamageStats,
+ /// Damage directed to the boss.
+ pub boss_damage: DamageStats,
+ /// 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
+ }
+}
+
+/// Damage statistics for a given target.
+#[derive(Debug, Clone, Copy, Default)]
+pub struct DamageStats {
+ /// The total damage of the player, including all minions/pets/...
+ pub total_damage: u64,
+ /// The condition damage that the player dealt.
+ pub condition_damage: u64,
+ /// The power damage that the player dealt.
+ pub power_damage: u64,
+ /// The damage that was done by minions/pets/...
+ pub add_damage: u64,
+}
+
+// A support macro to introduce a new block.
+//
+// Doesn't really require a macro, but it's nicer to look at
+// with! { foo = bar }
+// rather than
+// { let foo = bar; ... }
+macro_rules! with {
+ ($name:ident = $expr:expr => $bl:block) => {{
+ let $name = $expr;
+ $bl
+ }};
+}
+
+/// Calculate the statistics for the given log.
+pub fn calculate(log: &Log) -> StatResult<Statistics> {
+ use super::EventKind::*;
+
+ let mut agent_stats = HashMap::<u64, AgentStats>::new();
+ let mut log_start_time = 0;
+
+ fn get_stats(map: &mut HashMap<u64, AgentStats>, source: u64, target: u64) -> &mut DamageStats {
+ map.entry(source)
+ .or_insert_with(Default::default)
+ .per_target_damage
+ .entry(target)
+ .or_insert_with(Default::default)
+ }
+
+ for event in log.events() {
+ match event.kind {
+ LogStart { .. } => {
+ log_start_time = event.time;
+ }
+
+ EnterCombat { agent_addr, .. } => {
+ agent_stats
+ .entry(agent_addr)
+ .or_insert_with(Default::default)
+ .enter_combat = event.time - log_start_time;
+ }
+
+ ExitCombat { agent_addr } => {
+ agent_stats
+ .entry(agent_addr)
+ .or_insert_with(Default::default)
+ .exit_combat = event.time - log_start_time;
+ }
+
+ Physical {
+ source_agent_addr,
+ destination_agent_addr,
+ damage,
+ ..
+ } => {
+ with! { stats = get_stats(&mut agent_stats, source_agent_addr, destination_agent_addr) => {
+ stats.total_damage += damage as u64;
+ stats.power_damage += damage as u64;
+ }}
+
+ if let Some(master) = log.master_agent(source_agent_addr) {
+ let master_stats =
+ get_stats(&mut agent_stats, master.addr, destination_agent_addr);
+ master_stats.total_damage += damage as u64;
+ master_stats.add_damage += damage as u64;
+ }
+ }
+
+ ConditionTick {
+ source_agent_addr,
+ destination_agent_addr,
+ damage,
+ ..
+ } => {
+ with! { stats = get_stats(&mut agent_stats, source_agent_addr, destination_agent_addr) => {
+ stats.total_damage += damage as u64;
+ stats.condition_damage += damage as u64;
+ }}
+
+ if let Some(master) = log.master_agent(source_agent_addr) {
+ let master_stats =
+ get_stats(&mut agent_stats, master.addr, destination_agent_addr);
+ master_stats.total_damage += damage as u64;
+ master_stats.add_damage += damage as u64;
+ }
+ }
+
+ _ => (),
+ }
+ }
+
+ let boss = log.boss();
+
+ for agent_stat in agent_stats.values_mut() {
+ tally_damage(agent_stat);
+ agent_stat.boss_damage = agent_stat
+ .per_target_damage
+ .get(&boss.addr)
+ .cloned()
+ .unwrap_or_else(Default::default);
+ }
+
+ Ok(Statistics { agent_stats })
+}
+
+/// Takes the per target damage stats and tallies them up into the total damage
+/// stats.
+fn tally_damage(stats: &mut AgentStats) {
+ for damage in stats.per_target_damage.values() {
+ stats.total_damage.total_damage += damage.total_damage;
+ stats.total_damage.power_damage += damage.power_damage;
+ stats.total_damage.condition_damage += damage.condition_damage;
+ stats.total_damage.add_damage += damage.add_damage;
+ }
+}