1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
|
//! Analyzers for (challenge mote) fractal encounters.
use crate::{
analyzers::{helpers, Analyzer, Outcome},
EventKind, Log,
};
/// The ID of the invulnerability buff that Ai gets when she has been defeated.
pub const AI_INVULNERABILITY_ID: u32 = 895;
/// The ID of the skill with which we determine when Ai has phased.
pub const AI_PHASE_SKILL: u32 = 53_569;
/// The ID of the skill with which we determine Ai has the dark phase fight.
pub const AI_HAS_DARK_MODE_SKILL: u32 = 61_356;
/// Gets the timestamp when the second phase of Ai starts.
///
/// If the log is missing dark phase, `None` is returned.
///
/// If the whole log is in dark phase, `Some(0)` is returned.
fn get_dark_phase_start(log: &Log) -> Option<u64> {
// Determine if we even have a dark phase.
if !log.events().iter().any(|event|
matches!(event.kind(), EventKind::SkillUse { skill_id, ..} if *skill_id == AI_HAS_DARK_MODE_SKILL)
) {
return None;
};
// If we are here, either the whole log is in dark mode, or we phased.
let mut dark_phase_start = None;
for event in log.events() {
if let EventKind::SkillUse { skill_id, .. } = event.kind() {
if *skill_id == AI_PHASE_SKILL {
dark_phase_start = Some(event.time());
}
}
}
dark_phase_start.or(Some(0))
}
/// Analyzer for the fight of 100 CM, Ai, Keeper of the Peak.
///
/// This fight is special in that it consists of two phases, and the bosses each count as "success"
/// when they reach 1% health, i.e. they don't die.
#[derive(Debug, Clone, Copy)]
pub struct Ai<'log> {
log: &'log Log,
}
impl<'log> Ai<'log> {
/// Create a new [`Ai`] 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 {
Ai { log }
}
}
impl<'log> Analyzer for Ai<'log> {
fn log(&self) -> &Log {
self.log
}
fn is_cm(&self) -> bool {
// We assume that every Ai log is from CM, like the other fractal logs.
true
}
fn outcome(&self) -> Option<Outcome> {
let dark_phase_start = get_dark_phase_start(self.log);
if dark_phase_start.is_none() {
return Some(Outcome::Failure);
}
let dark_phase_start = dark_phase_start.unwrap();
for event in self.log.events() {
// Make sure we only count the invulnerability in dark phase
if event.time() < dark_phase_start {
continue;
}
if let EventKind::BuffApplication {
buff_id,
destination_agent_addr,
..
} = event.kind()
{
if *buff_id == AI_INVULNERABILITY_ID && self.log.is_boss(*destination_agent_addr) {
return Some(Outcome::Success);
}
}
}
Some(Outcome::Failure)
}
}
/// Health threshold for Skorvald to be detected as Challenge Mote.
pub const SKORVALD_CM_HEALTH: u64 = 5_551_340;
/// Character IDs for the anomalies in Skorvald's Challenge Mote.
pub static SKORVALD_CM_ANOMALY_IDS: &[u16] = &[17_599, 17_673, 17_770, 17_851];
/// Analyzer for the first boss of 99 CM, Skorvald.
///
/// The CM was detected by the boss's health, which was higher in the challenge mote.
///
/// The 2020-09-15 update which introduced a new fractal and shifted Shattered Observator CM to 99
/// which changed the bosses' maximal health, so this method no longer works. Instead, we rely on
/// the split phase to differentiate the "normal mode" flux anomalies from the "challenge mode"
/// flux anomalies, with the downside that the CM detection is only working if players make it to
/// the split phase.
#[derive(Debug, Clone, Copy)]
pub struct Skorvald<'log> {
log: &'log Log,
}
impl<'log> Skorvald<'log> {
/// Create a new [`Skorvald`] 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 {
Skorvald { log }
}
}
impl<'log> Analyzer for Skorvald<'log> {
fn log(&self) -> &Log {
self.log
}
fn is_cm(&self) -> bool {
// Shortcut for old logs for which this method still works.
if Some(true) == helpers::boss_health(self.log).map(|h| h >= SKORVALD_CM_HEALTH) {
return true;
}
self.log
.characters()
.any(|character| SKORVALD_CM_ANOMALY_IDS.contains(&character.id()))
}
fn outcome(&self) -> Option<Outcome> {
Outcome::from_bool(self.log.was_rewarded() || helpers::boss_is_dead(self.log))
}
}
/// Analyzer for fractals that don't require special logic.
///
/// This is used for Artsariiv, Arkk, MAMA, Siax and Ensolyss.
#[derive(Debug, Clone, Copy)]
pub struct GenericFractal<'log> {
log: &'log Log,
}
impl<'log> GenericFractal<'log> {
/// Create a new [`GenericFractal`] 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 {
GenericFractal { log }
}
}
impl<'log> Analyzer for GenericFractal<'log> {
fn log(&self) -> &Log {
self.log
}
fn is_cm(&self) -> bool {
// Besides Skorvald normal mode, we only get logs for the challenge mote encounters (at
// least, only for those we'll use this analyzer). So we can safely return true here in any
// case.
true
}
fn outcome(&self) -> Option<Outcome> {
Outcome::from_bool(self.log.was_rewarded() || helpers::boss_is_dead(self.log))
}
}
|