diff options
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; +    } +} | 
