aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md10
-rw-r--r--benches/analyzers.rs5
-rw-r--r--src/analyzers/mod.rs11
-rw-r--r--src/analyzers/strikes.rs284
-rw-r--r--src/gamedata.rs4
5 files changed, 307 insertions, 7 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 13357a1..8d4ac05 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,16 @@
All notable changes to this project will be documented in this file.
## Unreleased
+### Added
+- Various analyzers for the End of Dragons strike missions:
+ - `analyzers::strikes::CaptainMaiTrin`
+ - `analyzers::strikes::Ankka`
+ - `analyzers::strikes::MinisterLi`
+ - `analyzers::strikes::Dragonvoid`
+
+### Fixed
+- Success/failure detection for the End of Dragons strike missions.
+- Some Dragonvoid logs not being recognized as such.
## 0.7.0 - 2022-03-10
### Added
diff --git a/benches/analyzers.rs b/benches/analyzers.rs
index ee25253..43a5a88 100644
--- a/benches/analyzers.rs
+++ b/benches/analyzers.rs
@@ -62,4 +62,9 @@ benchmarks! {
(fractal_skorvald, "skorvald", "tests/logs/skorvald-20200920.zevtc"),
(strike_generic, "generic-strike", "tests/logs/whisper-20200424.zevtc"),
+
+ (strike_mai_trin, "maitrin", "tests/logs/mai-trin-20220303.zevtc"),
+ (strike_ankka, "ankka", "tests/logs/ankka-20220303.zevtc"),
+ (strike_li, "li", "tests/logs/minister-li-20220303.zevtc"),
+ (strike_dragonvoid, "dragonvoid", "tests/logs/dragonvoid-20220309.zevtc"),
}
diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs
index ed0f157..c27fefa 100644
--- a/src/analyzers/mod.rs
+++ b/src/analyzers/mod.rs
@@ -131,10 +131,11 @@ pub fn for_log<'l>(log: &'l Log) -> Option<Box<dyn Analyzer + 'l>> {
| Encounter::SuperKodanBrothers
| Encounter::FraenirOfJormag
| Encounter::Boneskinner
- | Encounter::WhisperOfJormag
- | Encounter::CaptainMaiTrin
- | Encounter::Ankka
- | Encounter::MinisterLi
- | Encounter::Dragonvoid => Some(Box::new(strikes::GenericStrike::new(log))),
+ | Encounter::WhisperOfJormag => Some(Box::new(strikes::GenericStrike::new(log))),
+
+ Encounter::CaptainMaiTrin => Some(Box::new(strikes::CaptainMaiTrin::new(log))),
+ Encounter::Ankka => Some(Box::new(strikes::Ankka::new(log))),
+ Encounter::MinisterLi => Some(Box::new(strikes::MinisterLi::new(log))),
+ Encounter::Dragonvoid => Some(Box::new(strikes::Dragonvoid::new(log))),
}
}
diff --git a/src/analyzers/strikes.rs b/src/analyzers/strikes.rs
index 8c22c49..9244124 100644
--- a/src/analyzers/strikes.rs
+++ b/src/analyzers/strikes.rs
@@ -1,7 +1,8 @@
//! Analyzers for Strike Mission logs.
use crate::{
analyzers::{helpers, Analyzer, Outcome},
- Log,
+ gamedata::Boss,
+ EventKind, Log,
};
/// Analyzer for strikes.
@@ -36,3 +37,284 @@ impl<'log> Analyzer for GenericStrike<'log> {
Outcome::from_bool(helpers::boss_is_dead(self.log))
}
}
+
+/// Analyzer for the Captain Mai Trin/Aetherblade Hideout strike.
+#[derive(Debug, Clone, Copy)]
+pub struct CaptainMaiTrin<'log> {
+ log: &'log Log,
+}
+
+impl<'log> CaptainMaiTrin<'log> {
+ pub const ECHO_OF_SCARLET_BRIAR: u16 = 24_768;
+ /// Determined buff that is used in Mai Trin's Strike.
+ ///
+ /// Thanks to ArenaNet's consistency, there are multiple versions of the Determined buff in
+ /// use.
+ ///
+ /// The chat link for this buff is `[&Bn8DAAA=]`.
+ pub const DETERMINED_ID: u32 = 895;
+
+ /// Create a new [`CaptainMaiTrin`] analyzer for the given log.
+ ///
+ /// **Do not** use this method unless you know what you are doing. Instead, rely on
+ /// [`Log::analyzer`]!
+ pub fn new(log: &'log Log) -> Self {
+ CaptainMaiTrin { log }
+ }
+}
+
+impl<'log> Analyzer for CaptainMaiTrin<'log> {
+ fn log(&self) -> &Log {
+ self.log
+ }
+
+ fn is_cm(&self) -> bool {
+ // EoD strike CMs are not implemented yet as of 2022-03-31
+ false
+ }
+
+ fn outcome(&self) -> Option<Outcome> {
+ check_reward!(self.log);
+
+ let scarlet = self
+ .log
+ .characters()
+ .find(|npc| npc.id() == Self::ECHO_OF_SCARLET_BRIAR)?;
+ let mai = self
+ .log
+ .characters()
+ .find(|npc| npc.id() == Boss::CaptainMaiTrin as u16)?;
+
+ for event in self.log.events() {
+ if let EventKind::BuffApplication {
+ destination_agent_addr,
+ buff_id,
+ ..
+ } = event.kind()
+ {
+ if *buff_id == Self::DETERMINED_ID
+ && *destination_agent_addr == mai.addr()
+ && event.time() > scarlet.first_aware()
+ {
+ return Some(Outcome::Success);
+ }
+ }
+ }
+
+ Some(Outcome::Failure)
+ }
+}
+
+/// Analyzer for the Ankka/Xunlai Jade Junkyard strike.
+#[derive(Debug, Clone, Copy)]
+pub struct Ankka<'log> {
+ log: &'log Log,
+}
+
+impl<'log> Ankka<'log> {
+ /// Determined buff that is used in Ankka's Strike.
+ ///
+ /// Thanks to ArenaNet's consistency, there are multiple versions of the Determined buff in
+ /// use.
+ ///
+ /// The chat link for this buff is `[&Bn8DAAA=]`.
+ pub const DETERMINED_ID: u32 = CaptainMaiTrin::DETERMINED_ID;
+ /// The minimum duration of [`DETERMINED_ID`] buff applications.
+ pub const DURATION_CUTOFF: i32 = i32::MAX;
+ /// The expected number of times that Ankka needs to phase before we consider it a success.
+ pub const EXPECTED_PHASE_COUNT: usize = 3;
+
+ /// Create a new [`Ankka`] analyzer for the given log.
+ ///
+ /// **Do not** use this method unless you know what you are doing. Instead, rely on
+ /// [`Log::analyzer`]!
+ pub fn new(log: &'log Log) -> Self {
+ Ankka { log }
+ }
+}
+
+impl<'log> Analyzer for Ankka<'log> {
+ fn log(&self) -> &Log {
+ self.log
+ }
+
+ fn is_cm(&self) -> bool {
+ // EoD strike CMs are not implemented yet as of 2022-03-31
+ false
+ }
+
+ fn outcome(&self) -> Option<Outcome> {
+ check_reward!(self.log);
+
+ let ankka = self
+ .log
+ .characters()
+ .find(|npc| npc.id() == Boss::Ankka as u16)?;
+
+ let phase_change_count = self
+ .log
+ .events()
+ .iter()
+ .filter(|event| {
+ if let EventKind::BuffApplication {
+ destination_agent_addr,
+ buff_id,
+ duration,
+ ..
+ } = event.kind()
+ {
+ *buff_id == Self::DETERMINED_ID
+ && *destination_agent_addr == ankka.addr()
+ && *duration == Self::DURATION_CUTOFF
+ } else {
+ false
+ }
+ })
+ .count();
+
+ Outcome::from_bool(phase_change_count == Self::EXPECTED_PHASE_COUNT)
+ }
+}
+
+/// Analyzer for the Minister Li/Kaineng Overlook strike.
+#[derive(Debug, Clone, Copy)]
+pub struct MinisterLi<'log> {
+ log: &'log Log,
+}
+
+impl<'log> MinisterLi<'log> {
+ /// Determined buff that is used in Minister Li's Strike.
+ ///
+ /// Thanks to ArenaNet's consistency, there are multiple versions of the Determined buff in
+ /// use.
+ ///
+ /// The chat link for this buff is `[&BvoCAAA=]`.
+ pub const DETERMINED_ID: u32 = 762;
+ /// The minimum number of times that Minister Li needs to phase before we consider it a success.
+ pub const MINIMUM_PHASE_COUNT: usize = 3;
+
+ /// Create a new [`MinisterLi`] analyzer for the given log.
+ ///
+ /// **Do not** use this method unless you know what you are doing. Instead, rely on
+ /// [`Log::analyzer`]!
+ pub fn new(log: &'log Log) -> Self {
+ MinisterLi { log }
+ }
+}
+
+impl<'log> Analyzer for MinisterLi<'log> {
+ fn log(&self) -> &Log {
+ self.log
+ }
+
+ fn is_cm(&self) -> bool {
+ // EoD strike CMs are not implemented yet as of 2022-03-31
+ false
+ }
+
+ fn outcome(&self) -> Option<Outcome> {
+ check_reward!(self.log);
+
+ let li = self
+ .log
+ .characters()
+ .find(|npc| npc.id() == Boss::MinisterLi as u16)?;
+
+ let phase_change_count = self
+ .log
+ .events()
+ .iter()
+ .filter(|event| {
+ if let EventKind::BuffApplication {
+ destination_agent_addr,
+ buff_id,
+ ..
+ } = event.kind()
+ {
+ *buff_id == Self::DETERMINED_ID && *destination_agent_addr == li.addr()
+ } else {
+ false
+ }
+ })
+ .count();
+
+ Outcome::from_bool(phase_change_count >= Self::MINIMUM_PHASE_COUNT)
+ }
+}
+
+/// Analyzer for the Dragonvoid/Harvest Temple strike.
+#[derive(Debug, Clone, Copy)]
+pub struct Dragonvoid<'log> {
+ log: &'log Log,
+}
+
+impl<'log> Dragonvoid<'log> {
+ pub const EXPECTED_TARGET_OFF_COUNT: usize = 2;
+
+ /// Create a new [`Dragonvoid`] analyzer for the given log.
+ ///
+ /// **Do not** use this method unless you know what you are doing. Instead, rely on
+ /// [`Log::analyzer`]!
+ pub fn new(log: &'log Log) -> Self {
+ Dragonvoid { log }
+ }
+}
+
+impl<'log> Analyzer for Dragonvoid<'log> {
+ fn log(&self) -> &Log {
+ self.log
+ }
+
+ fn is_cm(&self) -> bool {
+ // EoD strike CMs are not implemented yet as of 2022-03-31
+ false
+ }
+
+ fn outcome(&self) -> Option<Outcome> {
+ // check_reward is pointless because the reward is delayed.
+
+ // First, we find the right agent_addr
+ let mut first_voids = None;
+ for event in self.log.events() {
+ if let EventKind::AttackTarget {
+ agent_addr,
+ parent_agent_addr,
+ ..
+ } = event.kind()
+ {
+ if first_voids.is_none() {
+ first_voids = Some(parent_agent_addr);
+ } else if first_voids != Some(parent_agent_addr) {
+ // We find the amount of target off switches that occurred after a target on
+ // switch.
+ let mut is_on = false;
+ let mut target_off_count = 0;
+
+ // The nested loop over events is not ideal, but it is currently the easiest
+ // way to implement this logic without trying to cram it into a single loop.
+ for e in self.log.events() {
+ if let EventKind::Targetable {
+ agent_addr: taa,
+ targetable,
+ } = e.kind()
+ {
+ if *taa != *agent_addr {
+ continue;
+ }
+ if *targetable {
+ is_on = true;
+ } else if !targetable && is_on {
+ target_off_count += 1;
+ }
+ }
+ }
+
+ if target_off_count == Self::EXPECTED_TARGET_OFF_COUNT {
+ return Some(Outcome::Success);
+ }
+ }
+ }
+ }
+ Some(Outcome::Failure)
+ }
+}
diff --git a/src/gamedata.rs b/src/gamedata.rs
index c607147..f880ba6 100644
--- a/src/gamedata.rs
+++ b/src/gamedata.rs
@@ -48,6 +48,8 @@ impl FromStr for GameMode {
}
}
+static DRAGONVOID_IDS: &[u16] = &[Encounter::Dragonvoid as u16, 0xA9E0, 0x5F37];
+
/// Enum containing all encounters with their IDs.
///
/// An encounter is a fight or event for which a log can exist. An encounter consists of no, one or
@@ -229,7 +231,7 @@ impl Encounter {
match id {
_ if id == Encounter::TwistedCastle as u16 => Some(Encounter::TwistedCastle),
_ if id == Encounter::RiverOfSouls as u16 => Some(Encounter::RiverOfSouls),
- _ if id == Encounter::Dragonvoid as u16 => Some(Encounter::Dragonvoid),
+ _ if DRAGONVOID_IDS.contains(&id) => Some(Encounter::Dragonvoid),
_ => Boss::from_u16(id).map(Boss::encounter),
}
}