aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/gamedata.rs58
-rw-r--r--src/lib.rs84
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}")]
diff --git a/src/lib.rs b/src/lib.rs
index e28e740..ebf211d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -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)
+}