diff options
-rw-r--r-- | src/gamedata.rs | 58 | ||||
-rw-r--r-- | src/lib.rs | 84 |
2 files changed, 142 insertions, 0 deletions
diff --git a/src/gamedata.rs b/src/gamedata.rs index 03942b6..cac6518 100644 --- a/src/gamedata.rs +++ b/src/gamedata.rs @@ -62,6 +62,41 @@ pub enum Boss { WhisperOfJormag = 0x58B7, } +impl Boss { + /// Returns the CM trigger for this boss. + pub fn cm_trigger(self) -> CmTrigger { + match self { + Boss::KeepConstruct => CmTrigger::Unknown, + + Boss::MursaatOverseer => CmTrigger::HpThreshold(30_000_000), + Boss::Samarog => CmTrigger::HpThreshold(40_000_000), + Boss::Deimos => CmTrigger::HpThreshold(42_000_000), + + Boss::SoullessHorror => CmTrigger::TimeBetweenBuffs(47414, 11_000), + Boss::Dhuum => CmTrigger::HpThreshold(40_000_000), + + Boss::ConjuredAmalgamate => CmTrigger::BuffPresent(53_075), + // This is Nikare's health, as the log is saved with his ID + Boss::LargosTwins => CmTrigger::HpThreshold(19_200_000), + Boss::Qadim => CmTrigger::HpThreshold(21_100_000), + + Boss::CardinalAdina => CmTrigger::HpThreshold(24_800_000), + Boss::CardinalSabir => CmTrigger::HpThreshold(32_400_000), + Boss::QadimThePeerless => CmTrigger::HpThreshold(51_000_000), + + Boss::Skorvald => CmTrigger::HpThreshold(5_551_340), + Boss::Artsariiv => CmTrigger::Always, + Boss::Arkk => CmTrigger::Always, + + Boss::MAMA => CmTrigger::Always, + Boss::Siax => CmTrigger::Always, + Boss::Ensolyss => CmTrigger::Always, + + _ => CmTrigger::None, + } + } +} + /// Error for when converting a string to the boss fails. #[derive(Debug, Clone, Hash, PartialEq, Eq, Error)] #[error("Invalid boss identifier: {0}")] @@ -126,6 +161,29 @@ impl FromStr for Boss { /// into account. pub const XERA_PHASE2_ID: u16 = 0x3F9E; +/// The trigger of how a boss challenge mote (CM) is determined. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum CmTrigger { + /// The boss does not have a CM available. + None, + /// The boss has a CM available but we cannot determine if it has been activated. + Unknown, + /// Logs from this boss always count as having the CM active. + Always, + /// The CM is determined by the boss's health being at or above the given threshold. + /// + /// This works since most bosses increase their HP pool in the CM variant. + HpThreshold(u32), + /// The CM is active if the given buff is present in the log. + /// + /// The buff can be either on player or the enemy. + BuffPresent(u32), + /// The time between buff applications falls below the given threshold. + /// + /// The first number is the buff id, the second number is the time threshold. + TimeBetweenBuffs(u32, u64), +} + /// Error for when converting a string to a profession fails. #[derive(Debug, Clone, PartialEq, Eq, Hash, Error)] #[error("Invalid profession identifier: {0}")] @@ -66,6 +66,7 @@ //! While there are legitimate use cases for writing/modification support, they are currently not //! implemented (but might be in a future version). +use std::collections::HashMap; use std::convert::TryFrom; use std::marker::PhantomData; @@ -79,6 +80,7 @@ pub mod event; pub use event::{Event, EventKind}; pub mod gamedata; +use gamedata::CmTrigger; pub use gamedata::{Boss, EliteSpec, Profession}; /// Any error that can occur during the processing of evtc files. @@ -775,6 +777,56 @@ impl Log { /// /// Use those functions only if necessary, and prefer to cache the result if it will be reused! impl Log { + /// Check whether the fight was done with challenge mote activated. + /// + /// This function always returns `false` if + /// * The fight was done without CM + /// * The fight does not have a CM + /// * We cannot determine whether the CM was active + /// * The boss is not known + pub fn is_cm(&self) -> bool { + let trigger = self + .encounter() + .map(Boss::cm_trigger) + .unwrap_or(CmTrigger::Unknown); + match trigger { + CmTrigger::HpThreshold(hp_threshold) => { + for event in self.events() { + if let EventKind::MaxHealthUpdate { + agent_addr, + max_health, + } = *event.kind() + { + if self.is_boss(agent_addr) && max_health >= hp_threshold as u64 { + return true; + } + } + } + false + } + + CmTrigger::BuffPresent(wanted_buff_id) => { + for event in self.events() { + if let EventKind::BuffApplication { buff_id, .. } = *event.kind() { + if buff_id == wanted_buff_id { + return true; + } + } + } + false + } + + CmTrigger::TimeBetweenBuffs(buff_id, threshold) => { + let tbb = time_between_buffs(&self.events, buff_id); + tbb != 0 && tbb <= threshold + } + + CmTrigger::Always => true, + + CmTrigger::None | CmTrigger::Unknown => false, + } + } + /// Get the timestamp of when the log was started. /// /// The returned value is a unix timestamp in the local time zone. @@ -915,3 +967,35 @@ fn set_agent_masters(data: &raw::Evtc, agents: &mut [Agent]) -> Result<(), EvtcE } Ok(()) } + +fn time_between_buffs(events: &[Event], wanted_buff_id: u32) -> u64 { + let mut time_maps: HashMap<u64, Vec<u64>> = HashMap::new(); + for event in events { + if let EventKind::BuffApplication { + destination_agent_addr, + buff_id, + .. + } = event.kind() + { + if *buff_id == wanted_buff_id { + time_maps + .entry(*destination_agent_addr) + .or_default() + .push(event.time()); + } + } + } + let timestamps = if let Some(ts) = time_maps.values().max_by_key(|v| v.len()) { + ts + } else { + return 0; + }; + timestamps + .iter() + .zip(timestamps.iter().skip(1)) + .map(|(a, b)| b - a) + // Arbitrary limit to filter out duplicated buff application events + .filter(|x| *x > 50) + .min() + .unwrap_or(0) +} |