From 91902f7ddb1941a1bd078d786a52b91979fffc36 Mon Sep 17 00:00:00 2001 From: Daniel Schadt Date: Sat, 13 Nov 2021 20:32:14 +0100 Subject: Implement the River of Souls encounter --- src/analyzers/mod.rs | 1 + src/analyzers/raids/mod.rs | 2 +- src/analyzers/raids/w5.rs | 88 +++++++++++++++++++++++++++++++++++++++- src/gamedata.rs | 19 +++++++++ tests/logs/river-20210412.zevtc | Bin 0 -> 1057561 bytes tests/parsing.rs | 18 ++++++++ 6 files changed, 126 insertions(+), 2 deletions(-) create mode 100644 tests/logs/river-20210412.zevtc 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> { 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 { + 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 { + // 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), diff --git a/tests/logs/river-20210412.zevtc b/tests/logs/river-20210412.zevtc new file mode 100644 index 0000000..343785b Binary files /dev/null and b/tests/logs/river-20210412.zevtc differ diff --git a/tests/parsing.rs b/tests/parsing.rs index 415c3e6..d69ef86 100644 --- a/tests/parsing.rs +++ b/tests/parsing.rs @@ -282,6 +282,24 @@ test! { ], } +test! { + name: parse_river, + log: "logs/river-20210412.zevtc", + boss: Encounter::RiverOfSouls, + players: &[ + (1, ":Baragos.2384", "Cicadania", Mesmer, Some(Chronomancer)), + (1, ":Jupp.4570", "Aldwor", Guardian, Some(Firebrand)), + (2, ":Dunje.4863", "Pallida Howhite", Warrior, Some(Berserker)), + (2, ":Taniniver BlindDragon.9503", "Dragon Kills You", Necromancer, Some(Scourge)), + (2, ":neko.9741", "Mordrem Cat", Ranger, Some(Druid)), + (2, ":Ricola.5183", "Glühstrumpf", Mesmer, Some(Chronomancer)), + (2, ":Faboss.2534", "Faboss Sensei", Revenant, Some(Renegade)), + (3, ":Glahs.2549", "Nala", Ranger, Some(Druid)), + (3, ":xyoz.6710", "Xaphwen", Mesmer, Some(Chronomancer)), + (3, ":Straimer.1093", "Deepfreeze Myself", Elementalist, Some(Tempest)), + ], +} + test! { name: parse_dhuum, log: "logs/dhuum-20200428.zevtc", -- cgit v1.2.3