diff options
| author | Daniel Schadt <kingdread@gmx.de> | 2020-07-23 02:47:52 +0200 | 
|---|---|---|
| committer | Daniel Schadt <kingdread@gmx.de> | 2020-07-23 02:47:52 +0200 | 
| commit | 962e2b9f8e17a50c7d7d37a424591b0df62f265c (patch) | |
| tree | 61efe53eae3768aca9d5ce1eb89c91f5adaceeba /src | |
| parent | 0978345648cf9cdad6222f583dd21497b409d07e (diff) | |
| download | evtclib-962e2b9f8e17a50c7d7d37a424591b0df62f265c.tar.gz evtclib-962e2b9f8e17a50c7d7d37a424591b0df62f265c.tar.bz2 evtclib-962e2b9f8e17a50c7d7d37a424591b0df62f265c.zip | |
implement proper outcome for w1-w4
It turns out that `was_rewarded` is a pretty bad heuristic if you ever
kill a boss a second time per week (basically, was_rewarded=false does
not imply that the boss was unsuccessful). Therefore, we need a proper
detection of when a fight failed and when a fight succeeded.
This is the first batch that implements this as part of the Analyzer
trait for bosses of wings 1 to 4.
Diffstat (limited to 'src')
| -rw-r--r-- | src/analyzers/helpers.rs | 45 | ||||
| -rw-r--r-- | src/analyzers/mod.rs | 44 | ||||
| -rw-r--r-- | src/analyzers/raids/mod.rs | 34 | ||||
| -rw-r--r-- | src/analyzers/raids/w3.rs | 30 | ||||
| -rw-r--r-- | src/analyzers/raids/w4.rs | 102 | 
5 files changed, 252 insertions, 3 deletions
| diff --git a/src/analyzers/helpers.rs b/src/analyzers/helpers.rs index ec09355..674d752 100644 --- a/src/analyzers/helpers.rs +++ b/src/analyzers/helpers.rs @@ -1,7 +1,7 @@  //! This module contains helper methods that are used in different analyzers.  use std::collections::HashMap; -use crate::{EventKind, Log}; +use crate::{AgentKind, EventKind, Log};  /// Returns the maximum health of the boss agent.  /// @@ -24,6 +24,49 @@ pub fn boss_health(log: &Log) -> Option<u64> {      health  } +/// Checks if any of the boss NPCs have died. +/// +/// Death is determined by checking for the [`EventKind::ChangeDead`][EventKind::ChangeDead] event, +/// and whether a NPC is a boss is determined by the [`Log::is_boss`][Log::is_boss] method. +pub fn boss_is_dead(log: &Log) -> bool { +    log.events().iter().any(|ev| match ev.kind() { +        EventKind::ChangeDead { agent_addr } if log.is_boss(*agent_addr) => true, +        _ => false, +    }) +} + +/// Checks whether the players exit combat after the boss. +/// +/// This is useful to determine the success state of some fights. +pub fn players_exit_after_boss(log: &Log) -> bool { +    let mut player_exit = 0u64; +    let mut boss_exit = 0u64; + +    for event in log.events() { +        if let EventKind::ExitCombat { agent_addr } = event.kind() { +            let agent = if let Some(a) = log.agent_by_addr(*agent_addr) { +                a +            } else { +                continue; +            }; + +            match agent.kind() { +                AgentKind::Player(_) if event.time() >= player_exit => { +                    player_exit = event.time(); +                } +                AgentKind::Character(_) +                    if event.time() >= boss_exit && log.is_boss(*agent_addr) => +                { +                    boss_exit = event.time(); +                } +                _ => (), +            } +        } +    } +    // Safety margin +    boss_exit != 0 && player_exit > boss_exit + 1000 +} +  /// Checks if the given buff is present in the log.  pub fn buff_present(log: &Log, wanted_buff_id: u32) -> bool {      for event in log.events() { diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index 5ad88ec..c440136 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -23,6 +23,33 @@ pub mod fractals;  pub mod helpers;  pub mod raids; +/// The outcome of a fight. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Outcome { +    /// The fight succeeded. +    Success, +    /// The fight failed, i.e. the group wiped. +    Failure, +} + +impl Outcome { +    /// A function that turns a boolean into an [`Outcome`][Outcome]. +    /// +    /// This is a convenience function that can help implementing +    /// [`Analyzer::outcome`][Analyzer::outcome], which is also why this function returns an Option +    /// instead of the outcome directly. +    /// +    /// This turns `true` into [`Outcome::Success`][Outcome::Success] and `false` into +    /// [`Outcome::Failure`][Outcome::Failure]. +    pub fn from_bool(b: bool) -> Option<Outcome> { +        if b { +            Some(Outcome::Success) +        } else { +            Some(Outcome::Failure) +        } +    } +} +  /// An [`Analyzer`][Analyzer] is something that implements fight-dependent analyzing of the log.  pub trait Analyzer {      /// Returns a reference to the log being analyzed. @@ -30,6 +57,14 @@ pub trait Analyzer {      /// Checks whether the fight was done with the challenge mote activated.      fn is_cm(&self) -> bool; + +    /// Returns the outcome of the fight. +    /// +    /// Note that not all logs need to have an outcome, e.g. WvW or Golem logs may return `None` +    /// here. +    fn outcome(&self) -> Option<Outcome> { +        None +    }  }  /// Returns the correct [`Analyzer`][Analyzer] for the given log file. @@ -39,6 +74,15 @@ pub fn for_log<'l>(log: &'l Log) -> Option<Box<dyn Analyzer + 'l>> {      let boss = log.encounter()?;      match boss { +        Boss::ValeGuardian | Boss::Gorseval | Boss::Sabetha => { +            Some(Box::new(raids::GenericRaid::new(log))) +        } + +        Boss::Slothasor | Boss::Matthias => Some(Box::new(raids::GenericRaid::new(log))), + +        Boss::KeepConstruct => Some(Box::new(raids::GenericRaid::new(log))), +        Boss::Xera => Some(Box::new(raids::Xera::new(log))), +          Boss::Cairn => Some(Box::new(raids::Cairn::new(log))),          Boss::MursaatOverseer => Some(Box::new(raids::MursaatOverseer::new(log))),          Boss::Samarog => Some(Box::new(raids::Samarog::new(log))), diff --git a/src/analyzers/raids/mod.rs b/src/analyzers/raids/mod.rs index 91b0dba..33d54ce 100644 --- a/src/analyzers/raids/mod.rs +++ b/src/analyzers/raids/mod.rs @@ -1,3 +1,11 @@ +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    Log, +}; + +mod w3; +pub use w3::Xera; +  mod w4;  pub use w4::{Cairn, Deimos, MursaatOverseer, Samarog}; @@ -9,3 +17,29 @@ pub use w6::{ConjuredAmalgamate, LargosTwins, Qadim};  mod w7;  pub use w7::{CardinalAdina, CardinalSabir, QadimThePeerless}; + +/// A generic raid analyzer that works for bosses without special interactions. +#[derive(Debug, Clone, Copy)] +pub struct GenericRaid<'log> { +    log: &'log Log, +} + +impl<'log> GenericRaid<'log> { +    pub fn new(log: &'log Log) -> Self { +        GenericRaid { log } +    } +} + +impl<'log> Analyzer for GenericRaid<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    } +} diff --git a/src/analyzers/raids/w3.rs b/src/analyzers/raids/w3.rs new file mode 100644 index 0000000..82c007d --- /dev/null +++ b/src/analyzers/raids/w3.rs @@ -0,0 +1,30 @@ +use crate::{ +    analyzers::{helpers, Analyzer, Outcome}, +    Log, +}; + +/// Analyzer for the final fight of Wing 3, Xera. +#[derive(Debug, Clone, Copy)] +pub struct Xera<'log> { +    log: &'log Log, +} + +impl<'log> Xera<'log> { +    pub fn new(log: &'log Log) -> Self { +        Xera { log } +    } +} + +impl<'log> Analyzer for Xera<'log> { +    fn log(&self) -> &Log { +        self.log +    } + +    fn is_cm(&self) -> bool { +        false +    } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::players_exit_after_boss(self.log)) +    } +} diff --git a/src/analyzers/raids/w4.rs b/src/analyzers/raids/w4.rs index efdab8f..e753e49 100644 --- a/src/analyzers/raids/w4.rs +++ b/src/analyzers/raids/w4.rs @@ -1,7 +1,7 @@  //! Boss fight analyzers for Wing 4 (Bastion of the Penitent).  use crate::{ -    analyzers::{helpers, Analyzer}, -    Log, +    analyzers::{helpers, Analyzer, Outcome}, +    EventKind, Log,  };  pub const CAIRN_CM_BUFF: u32 = 38_098; @@ -29,6 +29,10 @@ impl<'log> Analyzer for Cairn<'log> {      fn is_cm(&self) -> bool {          helpers::buff_present(self.log, CAIRN_CM_BUFF)      } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    }  }  pub const MO_CM_HEALTH: u64 = 30_000_000; @@ -57,6 +61,10 @@ impl<'log> Analyzer for MursaatOverseer<'log> {              .map(|h| h >= MO_CM_HEALTH)              .unwrap_or(false)      } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    }  }  pub const SAMAROG_CM_HEALTH: u64 = 40_000_000; @@ -85,6 +93,10 @@ impl<'log> Analyzer for Samarog<'log> {              .map(|h| h >= SAMAROG_CM_HEALTH)              .unwrap_or(false)      } + +    fn outcome(&self) -> Option<Outcome> { +        Outcome::from_bool(helpers::boss_is_dead(self.log)) +    }  }  pub const DEIMOS_CM_HEALTH: u64 = 42_000_000; @@ -113,4 +125,90 @@ impl<'log> Analyzer for Deimos<'log> {              .map(|h| h >= DEIMOS_CM_HEALTH)              .unwrap_or(false)      } + +    fn outcome(&self) -> Option<Outcome> { +        // The idea for Deimos is that we first need to figure out when the 10% split happens (if +        // it even happens), then we can find the time when 10%-Deimos becomes untargetable and +        // then we can compare this time to the player exit time. + +        let split_time = deimos_10_time(self.log); +        // We never got to 10%, so this is a fail. +        if split_time == 0 { +            return Some(Outcome::Failure); +        } + +        let at_address = deimos_at_address(self.log); +        if at_address == 0 { +            return Some(Outcome::Failure); +        } + +        let mut player_exit = 0u64; +        let mut at_exit = 0u64; +        for event in self.log.events() { +            match event.kind() { +                EventKind::ExitCombat { agent_addr } +                    if self +                        .log +                        .agent_by_addr(*agent_addr) +                        .map(|a| a.kind().is_player()) +                        .unwrap_or(false) +                        && event.time() >= player_exit => +                { +                    player_exit = event.time(); +                } + +                EventKind::Targetable { +                    agent_addr, +                    targetable, +                } if *agent_addr == at_address && !targetable && event.time() >= at_exit => { +                    at_exit = event.time(); +                } + +                _ => (), +            } +        } + +        // Safety margin +        Outcome::from_bool(player_exit > at_exit + 1000) +    } +} + +// Extracts the timestamp when Deimos's 10% phase started. +// +// This function may panic when passed non-Deimos logs! +fn deimos_10_time(log: &Log) -> u64 { +    let mut first_aware = 0u64; + +    for event in log.events() { +        if let EventKind::Targetable { targetable, .. } = event.kind() { +            if *targetable { +                first_aware = event.time(); +                println!("First aware: {}", first_aware); +            } +        } +    } + +    first_aware +} + +// Returns the attack target address for the 10% Deimos phase. +// +// Returns 0 when the right attack target is not found. +fn deimos_at_address(log: &Log) -> u64 { +    for event in log.events().iter().rev() { +        if let EventKind::AttackTarget { +            agent_addr, +            parent_agent_addr, +            .. +        } = event.kind() +        { +            let parent = log.agent_by_addr(*parent_agent_addr); +            if let Some(parent) = parent { +                if Some("Deimos") == parent.as_gadget().map(|g| g.name()) { +                    return *agent_addr; +                } +            } +        } +    } +    0  } | 
