aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDaniel Schadt <kingdread@gmx.de>2021-11-13 20:32:14 +0100
committerDaniel Schadt <kingdread@gmx.de>2021-11-13 20:32:14 +0100
commit91902f7ddb1941a1bd078d786a52b91979fffc36 (patch)
treea6e5ef3a438c03d69764bfa3f685eac08f8e8285 /src
parentfebc33fd5272834b5290aeb9d8e3638aca886cda (diff)
downloadevtclib-91902f7ddb1941a1bd078d786a52b91979fffc36.tar.gz
evtclib-91902f7ddb1941a1bd078d786a52b91979fffc36.tar.bz2
evtclib-91902f7ddb1941a1bd078d786a52b91979fffc36.zip
Implement the River of Souls encounter
Diffstat (limited to 'src')
-rw-r--r--src/analyzers/mod.rs1
-rw-r--r--src/analyzers/raids/mod.rs2
-rw-r--r--src/analyzers/raids/w5.rs88
-rw-r--r--src/gamedata.rs19
4 files changed, 108 insertions, 2 deletions
diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs
index 6234a9a..a5f3cbf 100644
--- a/src/analyzers/mod.rs
+++ b/src/analyzers/mod.rs
@@ -100,6 +100,7 @@ pub fn for_log<'l>(log: &'l Log) -> Option<Box<dyn Analyzer + 'l>> {
Encounter::Deimos => Some(Box::new(raids::Deimos::new(log))),
Encounter::SoullessHorror => Some(Box::new(raids::SoullessHorror::new(log))),
+ Encounter::RiverOfSouls => Some(Box::new(raids::RiverOfSouls::new(log))),
Encounter::VoiceInTheVoid => Some(Box::new(raids::Dhuum::new(log))),
Encounter::ConjuredAmalgamate => Some(Box::new(raids::ConjuredAmalgamate::new(log))),
diff --git a/src/analyzers/raids/mod.rs b/src/analyzers/raids/mod.rs
index 7b636a7..824b957 100644
--- a/src/analyzers/raids/mod.rs
+++ b/src/analyzers/raids/mod.rs
@@ -16,7 +16,7 @@ mod w4;
pub use w4::{Cairn, Deimos, MursaatOverseer, Samarog};
mod w5;
-pub use w5::{Dhuum, SoullessHorror};
+pub use w5::{Dhuum, RiverOfSouls, SoullessHorror};
mod w6;
pub use w6::{ConjuredAmalgamate, Qadim, TwinLargos};
diff --git a/src/analyzers/raids/w5.rs b/src/analyzers/raids/w5.rs
index f914031..747bda2 100644
--- a/src/analyzers/raids/w5.rs
+++ b/src/analyzers/raids/w5.rs
@@ -1,7 +1,7 @@
//! Boss fight analyzers for Wing 5 (Hall of Chains)
use crate::{
analyzers::{helpers, Analyzer, Outcome},
- EventKind, Log,
+ Encounter, EventKind, Log,
};
pub const DESMINA_BUFF_ID: u32 = 47414;
@@ -54,6 +54,92 @@ impl<'log> Analyzer for SoullessHorror<'log> {
}
}
+/// Analyzer for the River of Souls escort event in Wing 5.
+#[derive(Debug, Clone, Copy)]
+pub struct RiverOfSouls<'log> {
+ log: &'log Log,
+}
+
+impl<'log> RiverOfSouls<'log> {
+ pub fn new(log: &'log Log) -> Self {
+ RiverOfSouls { log }
+ }
+}
+
+impl<'log> Analyzer for RiverOfSouls<'log> {
+ fn log(&self) -> &'log Log {
+ self.log
+ }
+
+ fn is_cm(&self) -> bool {
+ false
+ }
+
+ fn outcome(&self) -> Option<Outcome> {
+ const TRASH_IDS: &[u16] = &[0x4d97, 0x4bc7, 0x4d75, 0x4c05, 0x4bc8, 0x4cec];
+ check_reward!(self.log);
+
+ // First, let's get the Desmina NPC
+ let desmina = self
+ .log
+ .characters()
+ .find(|npc| npc.id() == Encounter::RiverOfSouls as u16)?;
+
+ // We need to see when our friendly Desmina exited combat, because if she didn't, the event
+ // failed.
+ let exit_combat = self
+ .log
+ .events()
+ .iter()
+ .find(|e| matches!(e.kind(), &EventKind::ExitCombat { agent_addr } if agent_addr == desmina.addr()));
+ if exit_combat.is_none() {
+ return Some(Outcome::Failure);
+ }
+
+ let trash_aware = self
+ .log
+ .characters()
+ .filter(|npc| TRASH_IDS.contains(&npc.id()))
+ .map(|npc| npc.last_aware())
+ .filter(|&i| i != u64::MAX)
+ .max()
+ .unwrap_or(0);
+
+ let desmina_despawn = self
+ .log()
+ .events()
+ .iter()
+ .find(|e| matches!(e.kind(), &EventKind::Despawn { agent_addr } if agent_addr == desmina.addr()));
+
+ Outcome::from_bool(
+ trash_aware != 0
+ && desmina_despawn.is_none()
+ // Add some leeway and see if we saw Desmina after all the trash was gone
+ && trash_aware + 500 <= desmina.last_aware()
+ && some_player_alive(self.log),
+ )
+ }
+}
+
+fn some_player_alive(log: &Log) -> bool {
+ let deaths_and_dcs = log
+ .events()
+ .iter()
+ .filter_map(|e| match *e.kind() {
+ EventKind::Despawn { agent_addr } => Some(agent_addr),
+ EventKind::ChangeDead { agent_addr } => Some(agent_addr),
+ _ => None,
+ })
+ .filter(|&addr| {
+ log.agent_by_addr(addr)
+ .map(|a| a.kind().is_player())
+ .unwrap_or(false)
+ })
+ .count();
+
+ deaths_and_dcs < log.players().count()
+}
+
pub const DHUUM_CM_HEALTH: u64 = 40_000_000;
/// Analyzer for the second fight of Wing 5, Dhuum.
diff --git a/src/gamedata.rs b/src/gamedata.rs
index 06d3803..3c48b71 100644
--- a/src/gamedata.rs
+++ b/src/gamedata.rs
@@ -28,6 +28,11 @@ pub enum Encounter {
// Wing 2
Slothasor = Boss::Slothasor as u16,
+ /// The "Protect the caged prisoners" event in Salvation Pass.
+ ///
+ /// Consists of [`Boss::Berg`], [`Boss::Zane`] and [`Boss::Narella`].
+ ///
+ /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Protect_the_caged_prisoners)
// Berg is the first encounter, which is why the logs are saved as "Berg".
BanditTrio = Boss::Berg as u16,
Matthias = Boss::Matthias as u16,
@@ -44,6 +49,10 @@ pub enum Encounter {
// Wing 5
SoullessHorror = Boss::SoullessHorror as u16,
+ /// The River of Souls is the Desmina escort event and an encounter that does not have a boss.
+ ///
+ /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Traverse_the_River_of_Souls)
+ RiverOfSouls = 0x4D74,
VoiceInTheVoid = Boss::Dhuum as u16,
// Wing 6
@@ -106,6 +115,7 @@ impl Encounter {
Encounter::Samarog => &[Boss::Samarog],
Encounter::Deimos => &[Boss::Deimos],
Encounter::SoullessHorror => &[Boss::SoullessHorror],
+ Encounter::RiverOfSouls => &[],
Encounter::VoiceInTheVoid => &[Boss::Dhuum],
Encounter::ConjuredAmalgamate => &[Boss::ConjuredAmalgamate],
Encounter::TwinLargos => &[Boss::Nikare, Boss::Kenut],
@@ -143,6 +153,10 @@ impl Encounter {
/// ```
#[inline]
pub fn from_header_id(id: u16) -> Option<Encounter> {
+ // For the encounter without boss, we do it manually.
+ if id == Encounter::RiverOfSouls as u16 {
+ return Some(Encounter::RiverOfSouls);
+ }
Boss::from_u16(id).map(Boss::encounter)
}
}
@@ -167,6 +181,7 @@ impl FromStr for Encounter {
let lower = s.to_lowercase();
match &lower as &str {
"trio" | "bandit trio" => Ok(Encounter::BanditTrio),
+ "river" | "river of souls" => Ok(Encounter::RiverOfSouls),
"largos" | "twins" | "largos twins" | "twin largos" => Ok(Encounter::TwinLargos),
"kodans" | "super kodan brothers" => Ok(Encounter::SuperKodanBrothers),
@@ -191,6 +206,7 @@ impl Display for Encounter {
Encounter::Samarog => "Samarog",
Encounter::Deimos => "Deimos",
Encounter::SoullessHorror => "Soulless Horror",
+ Encounter::RiverOfSouls => "River of Souls",
Encounter::VoiceInTheVoid => "Voice in the Void",
Encounter::ConjuredAmalgamate => "Conjured Amalgamate",
Encounter::TwinLargos => "Twin Largos",
@@ -807,6 +823,9 @@ mod tests {
("soulless horror", SoullessHorror),
("desmina", SoullessHorror),
("Desmina", SoullessHorror),
+ ("river", RiverOfSouls),
+ ("River", RiverOfSouls),
+ ("river of souls", RiverOfSouls),
("dhuum", VoiceInTheVoid),
("Dhuum", VoiceInTheVoid),
("ca", ConjuredAmalgamate),