diff options
author | Daniel Schadt <kingdread@gmx.de> | 2018-04-25 13:32:11 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2018-04-25 13:32:11 +0200 |
commit | 7d35d1a4ecf4d7fe6689c83defd9163b7a66f915 (patch) | |
tree | 433250fe3db638068d3190a17ef2d3f31ddf7293 /src/statistics | |
parent | 4ea20d5a7d75c77a36bd4baf768226e3089b48ae (diff) | |
download | evtclib-7d35d1a4ecf4d7fe6689c83defd9163b7a66f915.tar.gz evtclib-7d35d1a4ecf4d7fe6689c83defd9163b7a66f915.tar.bz2 evtclib-7d35d1a4ecf4d7fe6689c83defd9163b7a66f915.zip |
add basic boon queue support
This is already pretty good to calculate the overall boon uptime/average
stacks.
Diffstat (limited to 'src/statistics')
-rw-r--r-- | src/statistics/boon.rs | 175 | ||||
-rw-r--r-- | src/statistics/mod.rs | 172 |
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; + } +} |