diff options
author | Daniel Schadt <kingdread@gmx.de> | 2020-09-28 13:28:29 +0200 |
---|---|---|
committer | Daniel Schadt <kingdread@gmx.de> | 2020-09-28 13:28:29 +0200 |
commit | ba10db6e8120fe9315bf0dec99e9dee188b8332c (patch) | |
tree | 9d3a2d831aa0b9ecc20cc21010f2d782fba55d2e /src | |
parent | 3b79ad8fa9b4a8c9c535b417129e3f70769074e0 (diff) | |
parent | 132bc6e276bf996b8a67990ad32042b8023d8786 (diff) | |
download | evtclib-ba10db6e8120fe9315bf0dec99e9dee188b8332c.tar.gz evtclib-ba10db6e8120fe9315bf0dec99e9dee188b8332c.tar.bz2 evtclib-ba10db6e8120fe9315bf0dec99e9dee188b8332c.zip |
Merge branch 'boss-encounter-split' into master
Diffstat (limited to 'src')
-rw-r--r-- | src/analyzers/mod.rs | 56 | ||||
-rw-r--r-- | src/analyzers/raids/mod.rs | 2 | ||||
-rw-r--r-- | src/analyzers/raids/w6.rs | 16 | ||||
-rw-r--r-- | src/gamedata.rs | 525 | ||||
-rw-r--r-- | src/lib.rs | 33 |
5 files changed, 530 insertions, 102 deletions
diff --git a/src/analyzers/mod.rs b/src/analyzers/mod.rs index 28724a2..ff04728 100644 --- a/src/analyzers/mod.rs +++ b/src/analyzers/mod.rs @@ -24,7 +24,7 @@ //! [`Log::analyzer`][Log::analyzer] (or [`for_log`][for_log]) and the methods defined in //! [`Analyzer`][Analyzer]. -use crate::{Boss, Log}; +use crate::{Encounter, Log}; pub mod fractals; pub mod helpers; @@ -82,41 +82,43 @@ pub fn for_log<'l>(log: &'l Log) -> Option<Box<dyn Analyzer + 'l>> { let boss = log.encounter()?; match boss { - Boss::ValeGuardian | Boss::Gorseval | Boss::Sabetha => { + Encounter::ValeGuardian | Encounter::Gorseval | Encounter::Sabetha => { Some(Box::new(raids::GenericRaid::new(log))) } - Boss::Slothasor | Boss::Matthias => Some(Box::new(raids::GenericRaid::new(log))), + Encounter::Slothasor | Encounter::Matthias => Some(Box::new(raids::GenericRaid::new(log))), - Boss::KeepConstruct => Some(Box::new(raids::GenericRaid::new(log))), - Boss::Xera => Some(Box::new(raids::Xera::new(log))), + Encounter::KeepConstruct => Some(Box::new(raids::GenericRaid::new(log))), + Encounter::Xera => Some(Box::new(raids::Xera::new(log))), - Boss::Cairn => Some(Box::new(raids::Cairn::new(log))), - Boss::MursaatOverseer => Some(Box::new(raids::MursaatOverseer::new(log))), - Boss::Samarog => Some(Box::new(raids::Samarog::new(log))), - Boss::Deimos => Some(Box::new(raids::Deimos::new(log))), + Encounter::Cairn => Some(Box::new(raids::Cairn::new(log))), + Encounter::MursaatOverseer => Some(Box::new(raids::MursaatOverseer::new(log))), + Encounter::Samarog => Some(Box::new(raids::Samarog::new(log))), + Encounter::Deimos => Some(Box::new(raids::Deimos::new(log))), - Boss::SoullessHorror => Some(Box::new(raids::SoullessHorror::new(log))), - Boss::Dhuum => Some(Box::new(raids::Dhuum::new(log))), + Encounter::SoullessHorror => Some(Box::new(raids::SoullessHorror::new(log))), + Encounter::VoiceInTheVoid => Some(Box::new(raids::Dhuum::new(log))), - Boss::ConjuredAmalgamate => Some(Box::new(raids::ConjuredAmalgamate::new(log))), - Boss::LargosTwins => Some(Box::new(raids::LargosTwins::new(log))), - Boss::Qadim => Some(Box::new(raids::Qadim::new(log))), + Encounter::ConjuredAmalgamate => Some(Box::new(raids::ConjuredAmalgamate::new(log))), + Encounter::TwinLargos => Some(Box::new(raids::TwinLargos::new(log))), + Encounter::Qadim => Some(Box::new(raids::Qadim::new(log))), - Boss::CardinalAdina => Some(Box::new(raids::CardinalAdina::new(log))), - Boss::CardinalSabir => Some(Box::new(raids::CardinalSabir::new(log))), - Boss::QadimThePeerless => Some(Box::new(raids::QadimThePeerless::new(log))), + Encounter::CardinalAdina => Some(Box::new(raids::CardinalAdina::new(log))), + Encounter::CardinalSabir => Some(Box::new(raids::CardinalSabir::new(log))), + Encounter::QadimThePeerless => Some(Box::new(raids::QadimThePeerless::new(log))), - Boss::Ai => Some(Box::new(fractals::Ai::new(log))), - Boss::Skorvald => Some(Box::new(fractals::Skorvald::new(log))), - Boss::Artsariiv | Boss::Arkk | Boss::MAMA | Boss::Siax | Boss::Ensolyss => { - Some(Box::new(fractals::GenericFractal::new(log))) - } + Encounter::Ai => Some(Box::new(fractals::Ai::new(log))), + Encounter::Skorvald => Some(Box::new(fractals::Skorvald::new(log))), + Encounter::Artsariiv + | Encounter::Arkk + | Encounter::MAMA + | Encounter::Siax + | Encounter::Ensolyss => Some(Box::new(fractals::GenericFractal::new(log))), - Boss::IcebroodConstruct - | Boss::VoiceOfTheFallen - | Boss::FraenirOfJormag - | Boss::Boneskinner - | Boss::WhisperOfJormag => Some(Box::new(strikes::GenericStrike::new(log))), + Encounter::IcebroodConstruct + | Encounter::SuperKodanBrothers + | Encounter::FraenirOfJormag + | Encounter::Boneskinner + | Encounter::WhisperOfJormag => Some(Box::new(strikes::GenericStrike::new(log))), } } diff --git a/src/analyzers/raids/mod.rs b/src/analyzers/raids/mod.rs index bb3824b..7b636a7 100644 --- a/src/analyzers/raids/mod.rs +++ b/src/analyzers/raids/mod.rs @@ -19,7 +19,7 @@ mod w5; pub use w5::{Dhuum, SoullessHorror}; mod w6; -pub use w6::{ConjuredAmalgamate, LargosTwins, Qadim}; +pub use w6::{ConjuredAmalgamate, Qadim, TwinLargos}; mod w7; pub use w7::{CardinalAdina, CardinalSabir, QadimThePeerless}; diff --git a/src/analyzers/raids/w6.rs b/src/analyzers/raids/w6.rs index 8701a63..97a2094 100644 --- a/src/analyzers/raids/w6.rs +++ b/src/analyzers/raids/w6.rs @@ -1,7 +1,7 @@ //! Boss fight analyzers for Wing 6 (Mythwright Gambit) use crate::{ analyzers::{helpers, Analyzer, Outcome}, - gamedata::{KENUT_ID, NIKARE_ID}, + gamedata::Boss, EventKind, Log, }; @@ -59,21 +59,21 @@ pub const LARGOS_CM_HEALTH: u64 = 19_200_000; /// /// The CM is detected by the boss's health, which is higher in the challenge mote. #[derive(Debug, Clone, Copy)] -pub struct LargosTwins<'log> { +pub struct TwinLargos<'log> { log: &'log Log, } -impl<'log> LargosTwins<'log> { - /// Create a new [`LargosTwins`] analyzer for the given log. +impl<'log> TwinLargos<'log> { + /// Create a new [`TwinLargos`] 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 { - LargosTwins { log } + TwinLargos { log } } } -impl<'log> Analyzer for LargosTwins<'log> { +impl<'log> Analyzer for TwinLargos<'log> { fn log(&self) -> &Log { self.log } @@ -100,9 +100,9 @@ impl<'log> Analyzer for LargosTwins<'log> { continue; }; - if agent.id() == NIKARE_ID { + if agent.id() == Boss::Nikare as u16 { nikare_dead = true; - } else if agent.id() == KENUT_ID { + } else if agent.id() == Boss::Kenut as u16 { kenut_dead = true; } } diff --git a/src/gamedata.rs b/src/gamedata.rs index 392bd01..8d0bb7f 100644 --- a/src/gamedata.rs +++ b/src/gamedata.rs @@ -6,79 +6,407 @@ use std::{ }; use thiserror::Error; -/// Enum containing all bosses with their IDs. +/// 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 +/// multiple bosses. Most encounters map 1:1 to a boss (like Vale Guardian), however there are some +/// encounters with multiple bosses (like Twin Largos), and even encounters without bosses (like +/// the River of Souls, currently not implemented.). +/// +/// This enum is non-exhaustive to ensure that future encounters can be added without +/// inducing a breaking change. +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)] +#[non_exhaustive] +#[repr(u16)] +pub enum Encounter { + // Wing 1 + ValeGuardian = Boss::ValeGuardian as u16, + Gorseval = Boss::Gorseval as u16, + Sabetha = Boss::Sabetha as u16, + + // Wing 2 + Slothasor = Boss::Slothasor as u16, + Matthias = Boss::Matthias as u16, + + // Wing 3 + KeepConstruct = Boss::KeepConstruct as u16, + Xera = Boss::Xera as u16, + + // Wing 4 + Cairn = Boss::Cairn as u16, + MursaatOverseer = Boss::MursaatOverseer as u16, + Samarog = Boss::Samarog as u16, + Deimos = Boss::Deimos as u16, + + // Wing 5 + SoullessHorror = Boss::SoullessHorror as u16, + VoiceInTheVoid = Boss::Dhuum as u16, + + // Wing 6 + ConjuredAmalgamate = Boss::ConjuredAmalgamate as u16, + TwinLargos = Boss::Nikare as u16, + Qadim = Boss::Qadim as u16, + + // Wing 7 + CardinalAdina = Boss::CardinalAdina as u16, + CardinalSabir = Boss::CardinalSabir as u16, + QadimThePeerless = Boss::QadimThePeerless as u16, + + // 100 CM (Sunqua Peak) + Ai = Boss::Ai as u16, + + // 99 CM (Shattered Observatory) + Skorvald = Boss::Skorvald as u16, + Artsariiv = Boss::Artsariiv as u16, + Arkk = Boss::Arkk as u16, + + // 98 CM (Nightmare) + MAMA = Boss::MAMA as u16, + Siax = Boss::Siax as u16, + Ensolyss = Boss::Ensolyss as u16, + + // Strike missions + IcebroodConstruct = Boss::IcebroodConstruct as u16, + /// Internal name for the "Voice of the Fallen and Claw of the Fallen" strike mission. + SuperKodanBrothers = Boss::VoiceOfTheFallen as u16, + FraenirOfJormag = Boss::FraenirOfJormag as u16, + Boneskinner = Boss::Boneskinner as u16, + WhisperOfJormag = Boss::WhisperOfJormag as u16, +} + +impl Encounter { + /// Return all possible bosses that can appear in this encounter. + /// + /// This returns the possible boss IDs, not actual agents. For a similar function check + /// [`Log::boss_agents`][crate::Log::boss_agents]. + /// + /// Note that not all of them have to be present in a log, for example if the encounter stopped + /// before all of them spawned. + pub fn bosses(self) -> &'static [Boss] { + match self { + Encounter::ValeGuardian => &[Boss::ValeGuardian], + Encounter::Gorseval => &[Boss::Gorseval], + Encounter::Sabetha => &[Boss::Sabetha], + Encounter::Slothasor => &[Boss::Slothasor], + Encounter::Matthias => &[Boss::Matthias], + Encounter::KeepConstruct => &[Boss::KeepConstruct], + Encounter::Xera => &[Boss::Xera, Boss::Xera2], + Encounter::Cairn => &[Boss::Cairn], + Encounter::MursaatOverseer => &[Boss::MursaatOverseer], + Encounter::Samarog => &[Boss::Samarog], + Encounter::Deimos => &[Boss::Deimos], + Encounter::SoullessHorror => &[Boss::SoullessHorror], + Encounter::VoiceInTheVoid => &[Boss::Dhuum], + Encounter::ConjuredAmalgamate => &[Boss::ConjuredAmalgamate], + Encounter::TwinLargos => &[Boss::Nikare, Boss::Kenut], + Encounter::Qadim => &[Boss::Qadim], + Encounter::CardinalAdina => &[Boss::CardinalAdina], + Encounter::CardinalSabir => &[Boss::CardinalSabir], + Encounter::QadimThePeerless => &[Boss::QadimThePeerless], + Encounter::Ai => &[Boss::Ai], + Encounter::Skorvald => &[Boss::Skorvald], + Encounter::Artsariiv => &[Boss::Artsariiv], + Encounter::Arkk => &[Boss::Arkk], + Encounter::MAMA => &[Boss::MAMA], + Encounter::Siax => &[Boss::Siax], + Encounter::Ensolyss => &[Boss::Ensolyss], + Encounter::IcebroodConstruct => &[Boss::IcebroodConstruct], + Encounter::SuperKodanBrothers => &[Boss::VoiceOfTheFallen, Boss::ClawOfTheFallen], + Encounter::FraenirOfJormag => &[Boss::FraenirOfJormag], + Encounter::Boneskinner => &[Boss::Boneskinner], + Encounter::WhisperOfJormag => &[Boss::WhisperOfJormag], + } + } +} + +/// Error for when converting a string to an encounter fails. +#[derive(Debug, Clone, Hash, PartialEq, Eq, Error)] +#[error("Invalid encounter identifier: {0}")] +pub struct ParseEncounterError(String); + +impl FromStr for Encounter { + type Err = ParseEncounterError; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + // Parsing an encounter is in most cases the same as parsing a boss, as the encounters map + // 1:1 to a boss. For the special cases where the encounter as such has a specific name + // (such as Twin Largos), this parses strictly more bosses (so "Kenut" would be parsed as + // Encounter::TwinLargos, which is fine). The special cases are then added later (so that + // "Twin Largos" also is parsed as Encounter::TwinLargos). + if let Ok(boss) = Boss::from_str(s) { + return Ok(boss.encounter()); + } + let lower = s.to_lowercase(); + match &lower as &str { + "largos" | "twins" | "largos twins" | "twin largos" => Ok(Encounter::TwinLargos), + "kodans" | "super kodan brothers" => Ok(Encounter::SuperKodanBrothers), + + _ => Err(ParseEncounterError(s.to_owned())), + } + } +} + +impl Display for Encounter { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + let name = match *self { + Encounter::ValeGuardian => "Vale Guardian", + Encounter::Gorseval => "Gorseval", + Encounter::Sabetha => "Sabetha", + Encounter::Slothasor => "Slothasor", + Encounter::Matthias => "Matthias Gabrel", + Encounter::KeepConstruct => "Keep Construct", + Encounter::Xera => "Xera", + Encounter::Cairn => "Cairn the Indomitable", + Encounter::MursaatOverseer => "Mursaat Overseer", + Encounter::Samarog => "Samarog", + Encounter::Deimos => "Deimos", + Encounter::SoullessHorror => "Soulless Horror", + Encounter::VoiceInTheVoid => "Voice in the Void", + Encounter::ConjuredAmalgamate => "Conjured Amalgamate", + Encounter::TwinLargos => "Twin Largos", + Encounter::Qadim => "Qadim", + Encounter::CardinalAdina => "Cardinal Adina", + Encounter::CardinalSabir => "Cardinal Sabir", + Encounter::QadimThePeerless => "Qadim the Peerless", + Encounter::Ai => "Ai Keeper of the Peak", + Encounter::Skorvald => "Skorvald the Shattered", + Encounter::Artsariiv => "Artsariiv", + Encounter::Arkk => "Arkk", + Encounter::MAMA => "MAMA", + Encounter::Siax => "Siax the Corrupted", + Encounter::Ensolyss => "Ensolyss of the Endless Torment", + Encounter::IcebroodConstruct => "Icebrood Construct", + Encounter::SuperKodanBrothers => "Super Kodan Brothers", + Encounter::FraenirOfJormag => "Fraenir of Jormag", + Encounter::Boneskinner => "Boneskinner", + Encounter::WhisperOfJormag => "Whisper of Jormag", + }; + write!(f, "{}", name) + } +} + +/// Enum containing all boss IDs. +/// +/// For a high-level event categorization, take a look at the [`Encounter`] enum. The IDs listed +/// here are for a more fine-grained control, e.g. if you specifically need to differentiate +/// between Nikare and Kenut in the Twin Largos encounter. +/// +/// This enum is non-exhaustive to ensure that future bosses can be added without +/// inducing a breaking change. #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, FromPrimitive)] +#[non_exhaustive] +#[repr(u16)] pub enum Boss { // Wing 1 + /// Vale Guardian, first boss of Spirit Vale. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Vale_Guardian) ValeGuardian = 0x3C4E, + /// Gorseval, second boss of Spirit Vale. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Gorseval_the_Multifarious) Gorseval = 0x3C45, + /// Sabetha, third boss of Spirit Vale. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Sabetha_the_Saboteur) Sabetha = 0x3C0F, // Wing 2 + /// Slothasor, first boss of Salvation Pass. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Slothasor) Slothasor = 0x3EFB, + /// Matthias, third boss of Salvation Pass. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Matthias_Gabrel) Matthias = 0x3EF3, // Wing 3 + /// Keep Construct, second boss of the Stronghold of the Faithful. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Keep_Construct) KeepConstruct = 0x3F6B, - /// Xera ID for phase 1. + /// Xera, third boss of the Stronghold of the Faithful. /// - /// This is only half of Xera's ID, as there will be a second agent for the - /// second phase. This agent will have another ID, see - /// [`XERA_PHASE2_ID`](constant.XERA_PHASE2_ID.html). + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Xera) Xera = 0x3F76, + /// ID for Xera in the second phase. + /// + /// The original Xera will despawn, and after the tower phase, a separate spawn will take over. + /// This new Xera will have [`Boss::Xera2`] as ID. Care needs to be taken when calculating boss + /// damage on this encounter, as both Xeras have to be taken into account. + Xera2 = 0x3F9E, // Wing 4 + /// Cairn, first boss of the Bastion of the Penitent. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Cairn_the_Indomitable) Cairn = 0x432A, + /// Mursaat Overseer, second boss of the Bastion of the Penitent. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Mursaat_Overseer) MursaatOverseer = 0x4314, + /// Samarog, third boss of the Bastion of the Penitent. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Samarog) Samarog = 0x4324, + /// Deimos, fourth boss of the Bastion of the Penitent. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Deimos) Deimos = 0x4302, // Wing 5 + /// Soulless Horror, first boss of the Hall of Chains. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Soulless_Horror) SoullessHorror = 0x4D37, + /// Dhuum, second boss of the Hall of Chains. + /// + /// The encounter to this boss is called [Voice in the Void][Encounter::VoiceInTheVoid]. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Dhuum) Dhuum = 0x4BFA, // Wing 6 + /// Conjured Amalgamate, first boss of Mythwright Gambit. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Conjured_Amalgamate) ConjuredAmalgamate = 0xABC6, - /// This is the ID of Nikare, as that is what the Twin Largos logs are identified by. + /// Nikare, part of the [Twin Largos][Encounter::TwinLargos] encounter in Mythwright Gamit. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Nikare) + Nikare = 0x5271, + /// Kenut, part of the [Twin Largos][Encounter::TwinLargos] encounter in Mythwright Gamit. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Nikare) + Kenut = 0x5261, + /// Qadim, third boss in Mythwright Gambit. /// - /// If you want Nikare specifically, consider using [`NIKARE_ID`][NIKARE_ID], and similarly, if - /// you need Kenut, you can use [`KENUT_ID`][KENUT_ID]. - LargosTwins = 0x5271, + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Qadim) Qadim = 0x51C6, // Wing 7 + /// Cardinal Adina, one of the first two bosses in the Key of Ahdashim. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Cardinal_Adina) CardinalAdina = 0x55F6, + /// Cardinal Sabir, one of the first two bosses in the Key of Ahdashim. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Cardinal_Sabir) CardinalSabir = 0x55CC, + /// Qadim the Peerless, third boss in the Key of Ahdashim. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Qadim_the_Peerless) QadimThePeerless = 0x55F0, // 100 CM (Sunqua Peak) + /// Ai, Keeper of the Peak, boss of the Sunqua Peak CM fractal. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Ai,_Keeper_of_the_Peak) Ai = 0x5AD6, // 99 CM (Shattered Observatory) + /// Skorvald the Shattered, first boss in the Shattered Observatory. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Skorvald_the_Shattered) Skorvald = 0x44E0, + /// Artsariiv, second boss in the Shattered Observatory CM. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Artsariiv) Artsariiv = 0x461D, + /// Arkk, third boss in the Shattered Observatory. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Arkk) Arkk = 0x455F, // 98 CM (Nightmare) + /// MAMA, first boss in the Nightmare CM fractal. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/MAMA) MAMA = 0x427D, + /// Siax the Corrupted, second boss in the Nightmare CM fractal. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Siax_the_Corrupted) Siax = 0x4284, + /// Ensolyss of the Endless Torment, third boss in the Nightmare CM fractal. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Ensolyss_of_the_Endless_Torment) Ensolyss = 0x4234, // Strike missions + /// Legendary Icebrood Construct, boss of the Shiverpeaks Pass strike mission. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Legendary_Icebrood_Construct) IcebroodConstruct = 0x568A, - /// This is the ID of the Voice of the Fallen. + /// The Voice of the Fallen, part of the Voice of the Fallen and Claw of the Fallen strike + /// mission. /// - /// The strike mission itself contains two bosses, the Voice of the Fallen and the Claw of the - /// Fallen. Consider using either [`VOICE_OF_THE_FALLEN_ID`][VOICE_OF_THE_FALLEN_ID] or - /// [`CLAW_OF_THE_FALLEN_ID`][CLAW_OF_THE_FALLEN_ID] if you refer to one of those bosses - /// specifically. + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Voice_of_the_Fallen) VoiceOfTheFallen = 0x5747, + /// The Claw of the Fallen, part of the Voice of the Fallen and Claw of the Fallen strike + /// mission. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Claw_of_the_Fallen) + ClawOfTheFallen = 0x57D1, + /// The Fraenir of Jormag, boss of the Fraenir of Jormag strike mission. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Fraenir_of_Jormag) FraenirOfJormag = 0x57DC, + /// The Boneskinner, boss of the Boneskinner strike mission. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Boneskinner) Boneskinner = 0x57F9, + /// The Whisper of Jormag, boss of the Whisper of Jormag strike mission. + /// + /// [Guild Wars 2 Wiki](https://wiki.guildwars2.com/wiki/Whisper_of_Jormag) WhisperOfJormag = 0x58B7, } -/// Error for when converting a string to the boss fails. +impl Boss { + /// Get the encounter ID in which this boss can appear. + /// + /// This is the counterpart to [`Encounter::bosses`]. + pub fn encounter(self) -> Encounter { + match self { + Boss::ValeGuardian => Encounter::ValeGuardian, + Boss::Gorseval => Encounter::Gorseval, + Boss::Sabetha => Encounter::Sabetha, + Boss::Slothasor => Encounter::Slothasor, + Boss::Matthias => Encounter::Matthias, + Boss::KeepConstruct => Encounter::KeepConstruct, + Boss::Xera => Encounter::Xera, + Boss::Xera2 => Encounter::Xera, + Boss::Cairn => Encounter::Cairn, + Boss::MursaatOverseer => Encounter::MursaatOverseer, + Boss::Samarog => Encounter::Samarog, + Boss::Deimos => Encounter::Deimos, + Boss::SoullessHorror => Encounter::SoullessHorror, + Boss::Dhuum => Encounter::VoiceInTheVoid, + Boss::ConjuredAmalgamate => Encounter::ConjuredAmalgamate, + Boss::Nikare => Encounter::TwinLargos, + Boss::Kenut => Encounter::TwinLargos, + Boss::Qadim => Encounter::Qadim, + Boss::CardinalAdina => Encounter::CardinalAdina, + Boss::CardinalSabir => Encounter::CardinalSabir, + Boss::QadimThePeerless => Encounter::QadimThePeerless, + Boss::Ai => Encounter::Ai, + Boss::Skorvald => Encounter::Skorvald, + Boss::Artsariiv => Encounter::Artsariiv, + Boss::Arkk => Encounter::Arkk, + Boss::MAMA => Encounter::MAMA, + Boss::Siax => Encounter::Siax, + Boss::Ensolyss => Encounter::Ensolyss, + Boss::IcebroodConstruct => Encounter::IcebroodConstruct, + Boss::VoiceOfTheFallen => Encounter::SuperKodanBrothers, + Boss::ClawOfTheFallen => Encounter::SuperKodanBrothers, + Boss::FraenirOfJormag => Encounter::FraenirOfJormag, + Boss::Boneskinner => Encounter::Boneskinner, + Boss::WhisperOfJormag => Encounter::WhisperOfJormag, + } + } +} + +/// Error for when converting a string to an encounter fails. #[derive(Debug, Clone, Hash, PartialEq, Eq, Error)] #[error("Invalid boss identifier: {0}")] pub struct ParseBossError(String); @@ -105,10 +433,11 @@ impl FromStr for Boss { "deimos" => Ok(Boss::Deimos), "desmina" | "sh" | "soulless horror" => Ok(Boss::SoullessHorror), - "dhuum" => Ok(Boss::Dhuum), + "dhuum" | "voice in the void" => Ok(Boss::Dhuum), "ca" | "conjured amalgamate" => Ok(Boss::ConjuredAmalgamate), - "largos" | "twins" | "largos twins" => Ok(Boss::LargosTwins), + "nikare" => Ok(Boss::Nikare), + "kenut" => Ok(Boss::Kenut), "qadim" => Ok(Boss::Qadim), "adina" | "cardinal adina" => Ok(Boss::CardinalAdina), @@ -126,7 +455,8 @@ impl FromStr for Boss { "ensolyss" | "ensolyss of the endless torment" => Ok(Boss::Ensolyss), "icebrood" | "icebrood construct" => Ok(Boss::IcebroodConstruct), - "kodans" | "super kodan brothers" => Ok(Boss::VoiceOfTheFallen), + "voice" | "voice of the fallen" => Ok(Boss::VoiceOfTheFallen), + "claw" | "claw of the fallen" => Ok(Boss::ClawOfTheFallen), "fraenir" | "fraenir of jormag" => Ok(Boss::FraenirOfJormag), "boneskinner" => Ok(Boss::Boneskinner), "whisper" | "whisper of jormag" => Ok(Boss::WhisperOfJormag), @@ -146,6 +476,7 @@ impl Display for Boss { Boss::Matthias => "Matthias Gabrel", Boss::KeepConstruct => "Keep Construct", Boss::Xera => "Xera", + Boss::Xera2 => "Xera", Boss::Cairn => "Cairn the Indomitable", Boss::MursaatOverseer => "Mursaat Overseer", Boss::Samarog => "Samarog", @@ -153,7 +484,8 @@ impl Display for Boss { Boss::SoullessHorror => "Soulless Horror", Boss::Dhuum => "Dhuum", Boss::ConjuredAmalgamate => "Conjured Amalgamate", - Boss::LargosTwins => "Twin Largos", + Boss::Nikare => "Nikare", + Boss::Kenut => "Kenut", Boss::Qadim => "Qadim", Boss::CardinalAdina => "Cardinal Adina", Boss::CardinalSabir => "Cardinal Sabir", @@ -166,7 +498,8 @@ impl Display for Boss { Boss::Siax => "Siax the Corrupted", Boss::Ensolyss => "Ensolyss of the Endless Torment", Boss::IcebroodConstruct => "Icebrood Construct", - Boss::VoiceOfTheFallen => "Super Kodan Brothers", + Boss::VoiceOfTheFallen => "Voice of the Fallen", + Boss::ClawOfTheFallen => "Claw of the Fallen", Boss::FraenirOfJormag => "Fraenir of Jormag", Boss::Boneskinner => "Boneskinner", Boss::WhisperOfJormag => "Whisper of Jormag", @@ -174,25 +507,6 @@ impl Display for Boss { write!(f, "{}", name) } } - -/// ID for Xera in the second phase. -/// -/// The original Xera will despawn, and after the tower phase, a separate spawn -/// will take over. This new Xera will have this ID. Care needs to be taken when -/// calculating boss damage on this encounter, as both Xeras have to be taken -/// into account. -pub const XERA_PHASE2_ID: u16 = 0x3F9E; - -/// The ID of Nikare in the Twin Largos fight. -pub const NIKARE_ID: u16 = Boss::LargosTwins as u16; -/// The ID of Kenut in the Twin Largos fight. -pub const KENUT_ID: u16 = 21089; - -/// The ID of the Voice of the Fallen. -pub const VOICE_OF_THE_FALLEN_ID: u16 = Boss::VoiceOfTheFallen as u16; -/// The ID of the Claw of the Fallen. -pub const CLAW_OF_THE_FALLEN_ID: u16 = 22481; - /// Error for when converting a string to a profession fails. #[derive(Debug, Clone, PartialEq, Eq, Hash, Error)] #[error("Invalid profession identifier: {0}")] @@ -369,6 +683,119 @@ mod tests { use super::*; #[test] + fn test_encounter_parsing_ok() { + use Encounter::*; + let tests: &[(&'static str, Encounter)] = &[ + ("vg", ValeGuardian), + ("VG", ValeGuardian), + ("vale guardian", ValeGuardian), + ("Vale Guardian", ValeGuardian), + ("gorse", Gorseval), + ("Gorse", Gorseval), + ("gorseval", Gorseval), + ("Gorseval", Gorseval), + ("sab", Sabetha), + ("sabetha", Sabetha), + ("Sabetha", Sabetha), + ("sloth", Slothasor), + ("slothasor", Slothasor), + ("Slothasor", Slothasor), + ("matthias", Matthias), + ("Matthias", Matthias), + ("kc", KeepConstruct), + ("KC", KeepConstruct), + ("keep construct", KeepConstruct), + ("Keep Construct", KeepConstruct), + ("xera", Xera), + ("Xera", Xera), + ("cairn", Cairn), + ("Cairn", Cairn), + ("mo", MursaatOverseer), + ("MO", MursaatOverseer), + ("mursaat overseer", MursaatOverseer), + ("Mursaat Overseer", MursaatOverseer), + ("samarog", Samarog), + ("Samarog", Samarog), + ("deimos", Deimos), + ("Deimos", Deimos), + ("sh", SoullessHorror), + ("soulless horror", SoullessHorror), + ("desmina", SoullessHorror), + ("Desmina", SoullessHorror), + ("dhuum", VoiceInTheVoid), + ("Dhuum", VoiceInTheVoid), + ("ca", ConjuredAmalgamate), + ("conjured amalgamate", ConjuredAmalgamate), + ("Conjured Amalgamate", ConjuredAmalgamate), + ("largos", TwinLargos), + ("twins", TwinLargos), + ("largos twins", TwinLargos), + ("qadim", Qadim), + ("Qadim", Qadim), + ("adina", CardinalAdina), + ("cardinal adina", CardinalAdina), + ("Cardinal Adina", CardinalAdina), + ("sabir", CardinalSabir), + ("cardinal sabir", CardinalSabir), + ("Cardinal Sabir", CardinalSabir), + ("qadimp", QadimThePeerless), + ("qadim the peerless", QadimThePeerless), + ("Qadim The Peerless", QadimThePeerless), + ("Ai", Ai), + ("ai", Ai), + ("skorvald", Skorvald), + ("Skorvald", Skorvald), + ("artsariiv", Artsariiv), + ("Artsariiv", Artsariiv), + ("arkk", Arkk), + ("Arkk", Arkk), + ("mama", MAMA), + ("MAMA", MAMA), + ("siax", Siax), + ("SIAX", Siax), + ("ensolyss", Ensolyss), + ("Ensolyss", Ensolyss), + ("Ensolyss of the Endless Torment", Ensolyss), + ("icebrood", IcebroodConstruct), + ("Icebrood Construct", IcebroodConstruct), + ("fraenir", FraenirOfJormag), + ("Fraenir of Jormag", FraenirOfJormag), + ("boneskinner", Boneskinner), + ("kodans", SuperKodanBrothers), + ("whisper", WhisperOfJormag), + ("Whisper of Jormag", WhisperOfJormag), + ]; + + for (input, expected) in tests { + assert_eq!( + input.parse(), + Ok(*expected), + "parsing input {:?} failed", + input + ); + } + } + + #[test] + fn test_encounter_parsing_err() { + let tests = &[ + "", + "vga", + "VGA", + "foovg", + "valeguardian", + "ValeGuardian", + "slotha", + "slot", + "slothasora", + "cardinal", + ]; + for test in tests { + assert!(test.parse::<Encounter>().is_err()); + } + } + + #[test] fn test_boss_parsing_ok() { use Boss::*; let tests: &[(&'static str, Boss)] = &[ @@ -413,9 +840,10 @@ mod tests { ("ca", ConjuredAmalgamate), ("conjured amalgamate", ConjuredAmalgamate), ("Conjured Amalgamate", ConjuredAmalgamate), - ("largos", LargosTwins), - ("twins", LargosTwins), - ("largos twins", LargosTwins), + ("kenut", Kenut), + ("Kenut", Kenut), + ("nikare", Nikare), + ("Nikare", Nikare), ("qadim", Qadim), ("Qadim", Qadim), ("adina", CardinalAdina), @@ -427,6 +855,8 @@ mod tests { ("qadimp", QadimThePeerless), ("qadim the peerless", QadimThePeerless), ("Qadim The Peerless", QadimThePeerless), + ("Ai", Ai), + ("ai", Ai), ("skorvald", Skorvald), ("Skorvald", Skorvald), ("artsariiv", Artsariiv), @@ -445,7 +875,12 @@ mod tests { ("fraenir", FraenirOfJormag), ("Fraenir of Jormag", FraenirOfJormag), ("boneskinner", Boneskinner), - ("kodans", VoiceOfTheFallen), + ("claw", ClawOfTheFallen), + ("Claw", ClawOfTheFallen), + ("Claw of the Fallen", ClawOfTheFallen), + ("voice", VoiceOfTheFallen), + ("Voice", VoiceOfTheFallen), + ("Voice of the Fallen", VoiceOfTheFallen), ("whisper", WhisperOfJormag), ("Whisper of Jormag", WhisperOfJormag), ]; @@ -473,6 +908,10 @@ mod tests { "slot", "slothasora", "cardinal", + // The following are encounters, make sure we don't parse them as bosses. + "twins", + "kodans", + "twin largos", ]; for test in tests { assert!(test.parse::<Boss>().is_err()); @@ -104,7 +104,8 @@ mod processing; pub use processing::{process, process_file, process_stream, Compression}; pub mod gamedata; -pub use gamedata::{Boss, EliteSpec, Profession}; +use gamedata::Boss; +pub use gamedata::{EliteSpec, Encounter, Profession}; pub mod analyzers; pub use analyzers::{Analyzer, Outcome}; @@ -758,20 +759,12 @@ impl Log { /// This correctly returns multiple agents on encounters where multiple /// agents are needed. pub fn boss_agents(&self) -> Vec<&Agent> { - let boss_ids = if self.boss_id == Boss::Xera as u16 { - vec![self.boss_id, gamedata::XERA_PHASE2_ID] - } else if self.boss_id == Boss::LargosTwins as u16 { - vec![gamedata::NIKARE_ID, gamedata::KENUT_ID] - } else if self.encounter() == Some(Boss::VoiceOfTheFallen) { - vec![ - gamedata::VOICE_OF_THE_FALLEN_ID, - gamedata::CLAW_OF_THE_FALLEN_ID, - ] - } else { - vec![self.boss_id] - }; + let bosses = self + .encounter() + .map(Encounter::bosses) + .unwrap_or(&[] as &[_]); self.npcs() - .filter(|c| boss_ids.contains(&c.character().id)) + .filter(|c| bosses.iter().any(|boss| *boss as u16 == c.character().id)) .map(Agent::erase) .collect() } @@ -791,16 +784,10 @@ impl Log { /// /// Some logs don't have an encounter set or have an ID that is unknown to us (for example, if /// people set up arcdps with custom IDs). Therefore, this method can only return the encounter - /// if we know about it in [`Boss`][Boss]. + /// if we know about it in [`Encounter`]. #[inline] - pub fn encounter(&self) -> Option<Boss> { - // Sometimes, encounters of the strike mission "Voice of the Fallen and Claw of the Fallen" - // are saved with the ID of the Claw and sometimes with the Voice. Therefore, we need to - // unify those cases. - if self.boss_id == gamedata::CLAW_OF_THE_FALLEN_ID { - return Some(Boss::VoiceOfTheFallen); - } - Boss::from_u16(self.boss_id) + pub fn encounter(&self) -> Option<Encounter> { + Boss::from_u16(self.boss_id).map(Boss::encounter) } /// Return an analyzer suitable to analyze the given log. |