Skip to content

Commit

Permalink
Merge branch 'dragonflight' of https://github.com/WoWAnalyzer/WoWAnal…
Browse files Browse the repository at this point in the history
…yzer into dragonflight
  • Loading branch information
Krealle committed Mar 21, 2024
2 parents 4fa9536 + 14e371d commit f4fa34a
Show file tree
Hide file tree
Showing 7 changed files with 277 additions and 1 deletion.
7 changes: 7 additions & 0 deletions src/CONTRIBUTORS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1808,6 +1808,13 @@ export const Trevor: Contributor = {
avatar: avatar('Trevor-avatar.png'),
};

export const Harrek: Contributor = {
nickname: 'Harrek',
discord: 'harrek',
github: 'Harreks',
avatar: avatar('Harrek-avatar.png'),
};

export const Jeff: Contributor = {
nickname: 'Jeff',
discord: 'muhnameizjeff#8143',
Expand Down
3 changes: 2 additions & 1 deletion src/analysis/retail/evoker/preservation/CHANGELOG.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { change, date } from 'common/changelog';
import SPELLS from 'common/SPELLS';
import { TALENTS_EVOKER } from 'common/TALENTS';
import { ToppleTheNun, Trevor, Tyndi, Vohrr, Vollmer } from 'CONTRIBUTORS';
import { ToppleTheNun, Trevor, Tyndi, Vohrr, Vollmer, Harrek } from 'CONTRIBUTORS';
import RESOURCE_TYPES from 'game/RESOURCE_TYPES';
import { ResourceLink, SpellLink } from 'interface';

export default [
change(date(2024, 3, 12), <>Implement <SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> module.</>, Harrek),
change(date(2024, 3, 7), <>Fix an issue with <SpellLink spell={TALENTS_EVOKER.POTENT_MANA_TALENT} /> module when no <SpellLink spell={TALENTS_EVOKER.SOURCE_OF_MAGIC_TALENT} /> was active during the fight.</>, Vollmer),
change(date(2024, 2, 18), <>Add spell cast time to <SpellLink spell={TALENTS_EVOKER.STASIS_TALENT}/> module</>, Trevor),
change(date(2024, 2, 10), <>Fix crash in <SpellLink spell={TALENTS_EVOKER.SOURCE_OF_MAGIC_TALENT} /> module.</>, Trevor),
Expand Down
2 changes: 2 additions & 0 deletions src/analysis/retail/evoker/preservation/CombatLogParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import FieldOfDreams from './modules/talents/FieldOfDreams';
import DreamFlight from './modules/talents/DreamFlight';
import ExhilBurst from './modules/talents/ExhilBurst';
import Stasis from './modules/talents/Stasis';
import TimeOfNeed from './modules/talents/TimeOfNeed';
import Lifebind from './modules/talents/Lifebind';
import EnergyLoop from './modules/talents/EnergyLoop';
import AlwaysBeCasting from './modules/core/AlwaysBeCasting';
Expand Down Expand Up @@ -116,6 +117,7 @@ class CombatLogParser extends CoreCombatLogParser {
fieldOfDreams: FieldOfDreams,
exhilBurst: ExhilBurst,
stasis: Stasis,
timeOfNeed: TimeOfNeed,
lifebind: Lifebind,
energyLoop: EnergyLoop,
fontOfMagic: FontOfMagic,
Expand Down
236 changes: 236 additions & 0 deletions src/analysis/retail/evoker/preservation/modules/talents/TimeOfNeed.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
import Analyzer, { Options, SELECTED_PLAYER } from 'parser/core/Analyzer';
import { getTimeOfNeedHealing } from '../../normalizers/CastLinkNormalizer';
import { TALENTS_EVOKER } from 'common/TALENTS';
import Events, { HealEvent, SummonEvent, DamageEvent } from 'parser/core/Events';
import SPELLS from 'common/SPELLS';
import LazyLoadStatisticBox from 'parser/ui/LazyLoadStatisticBox';
import STATISTIC_ORDER from 'parser/ui/STATISTIC_ORDER';
import STATISTIC_CATEGORY from 'parser/ui/STATISTIC_CATEGORY';
import { SpellIcon, SpellLink } from 'interface';
import { formatNumber } from 'common/format';
import fetchWcl from 'common/fetchWclApi';
import { WCLEventsResponse } from 'common/WCL_TYPES';
import Combatants from 'parser/shared/modules/Combatants';

//All possible results from a Time of Need proc
const Result = {
Proc: (
<>
Saved by <SpellLink spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} />
</>
),
Long: 'Saved by all the healing',
Not: 'Was not in danger of death',
Dead: 'Died anyway',
Bug: 'Time of Need fizzled out',
} as const;
type Results = typeof Result[keyof typeof Result];

interface TonEvent {
summon: SummonEvent;
verdantEmbrace: HealEvent | null;
livingFlames: HealEvent[];
livingFlameTotalHeal: number;
livingFlameTotalOverheal: number;
target: number;
result: Results;
}

class TimeOfNeed extends Analyzer {
static dependencies = {
combatants: Combatants,
};

protected combatants!: Combatants;
spawns: TonEvent[] = [];

constructor(options: Options) {
super(options);

this.active = this.selectedCombatant.hasTalent(TALENTS_EVOKER.TIME_OF_NEED_TALENT);

this.addEventListener(
Events.summon.by(SELECTED_PLAYER).spell(SPELLS.TIME_OF_NEED_SUMMON),
this.onSpawn,
);
}

onSpawn(event: SummonEvent) {
const healEvents = getTimeOfNeedHealing(event);

const currentEvent: TonEvent = {
summon: event,
verdantEmbrace: null,
livingFlames: [],
livingFlameTotalHeal: 0,
livingFlameTotalOverheal: 0,
target: 0,
result: Result.Not,
};

healEvents.forEach((heal) => {
if (heal.ability.guid === SPELLS.VERDANT_EMBRACE_HEAL.id) {
currentEvent.verdantEmbrace = heal;
currentEvent.target = heal.targetID;
} else if (heal.ability.guid === SPELLS.TIME_OF_NEED_LIVING_FLAME.id) {
currentEvent.livingFlames.push(heal);
currentEvent.livingFlameTotalHeal += heal.amount + (heal.absorbed || 0);
currentEvent.livingFlameTotalOverheal += heal.overheal ?? 0;
}
});

this.spawns.push(currentEvent);
}

parseDamageTaken(tonSpawn: TonEvent, eventList: WCLEventsResponse) {
//Check that ToN actually did any healing or if it bugged out
if (tonSpawn.verdantEmbrace === null && !tonSpawn.livingFlames.length) {
return Result.Bug;
}
//If there is no damage events, then Time of Need didn't save the player from anything
if (!eventList.events.length) {
return Result.Not;
}
const hits = eventList.events as DamageEvent[];
//Iterate all the damage taken events during the time window
for (const hit of hits) {
//Check that the info from the damage event is valid
if (hit.hitPoints !== undefined) {
//If the hitpoints after the hit are equal to 0, the player died anyway
if (!hit.hitPoints) {
return Result.Dead;
}
//Initialize the healing from Time of Need with the VE
let tonHealingAtThisPoint = tonSpawn.verdantEmbrace?.amount || 0;
//If there is no living flames casted, or this hit was before the first living flame landed
if (!tonSpawn.livingFlames.length || hit.timestamp < tonSpawn.livingFlames[0].timestamp) {
//Check if the player would've died from this hit without the Verdant Embrace healing
if (hit.hitPoints - tonHealingAtThisPoint <= 0) {
return Result.Proc; //Person was saved by the VE
}
} else {
//Add up all the healing done by the living flames that happened before this hit
tonSpawn.livingFlames.forEach((livingFlame) => {
if (livingFlame.timestamp < hit.timestamp) {
tonHealingAtThisPoint += livingFlame.amount;
}
});
//If the hit points after this hit minus the total ToN healing done to this point is equal or less than 0, then he was saved
if (hit.hitPoints > 0 && hit.hitPoints - tonHealingAtThisPoint <= 0) {
return Result.Long; //Mark as long save
}
}
}
}
return Result.Not;
}

async load() {
//Run every ToN spawn through parseDamageTaken()
for (const spawn of this.spawns) {
const target = this.combatants?.players[spawn.target];
const filter = target && target?.name ? `target.name = "${target.name}"` : '';
const damageTakenEvents = await fetchWcl<WCLEventsResponse>(
`report/events/damage-taken/${this.owner.report.code}`,
{
start: spawn.summon.timestamp,
end: spawn.summon.timestamp + 8000,
filter,
},
);
spawn.result = this.parseDamageTaken(spawn, damageTakenEvents);
}
}

statistic() {
return (
<LazyLoadStatisticBox
category={STATISTIC_CATEGORY.TALENTS}
position={STATISTIC_ORDER.OPTIONAL(60)}
loader={this.load.bind(this)}
label={
<>
<SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> events
</>
}
tooltip={
<div>
<SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> will cast one{' '}
<SpellLink spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} /> and several
<SpellLink spell={SPELLS.LIVING_FLAME_HEAL} />s on the player that caused the proc.
Depending on the amount of damage that player took during the 8 seconds
<SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> is active the event is
classified as one of the following:
<ul>
<li>
<b>
Saved by <SpellLink spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} />:
</b>{' '}
If the player would've died from damage after the{' '}
<SpellLink spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} /> but before any{' '}
<SpellLink spell={SPELLS.LIVING_FLAME_HEAL} />
s.
</li>
<li>
<b>Saved by all the healing:</b> If the player was saved by a combination of the{' '}
<SpellLink spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} /> and some{' '}
<SpellLink spell={SPELLS.LIVING_FLAME_HEAL} /> healing.
</li>
<li>
<b>Was not in danger of death:</b> If there wasn't any damage event big enough to
kill the player even without the{' '}
<SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> healing.
</li>
<li>
<b>Died anyway:</b> If they died anyway regardless of the{' '}
<SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> healing.
</li>
<li>
<b>Time of Need fizzled out:</b> The player might have died before{' '}
<SpellLink spell={TALENTS_EVOKER.TIME_OF_NEED_TALENT} /> had time to land the{' '}
<SpellLink spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} />, and thus did no healing
at all.
</li>
</ul>
</div>
}
value={<>Total of {this.spawns.length} events</>}
>
<table className="table table-condensed">
<thead>
<tr>
<th>Event Time</th>
<th>Healing (Overhealing)</th>
<th>Result</th>
</tr>
</thead>
<tbody>
{this.spawns.map((info, index) => (
<tr key={index}>
<td>{this.owner.formatTimestamp(info.summon.timestamp)}</td>
<td>
<div>
{formatNumber(
info.verdantEmbrace?.amount || 0 + (info.verdantEmbrace?.absorbed || 0),
)}{' '}
({formatNumber(info.verdantEmbrace?.overheal || 0)}){' '}
<SpellIcon spell={TALENTS_EVOKER.VERDANT_EMBRACE_TALENT} />
</div>
<div>
{formatNumber(info.livingFlameTotalHeal)} (
{formatNumber(info.livingFlameTotalOverheal)}){' '}
<SpellIcon spell={SPELLS.TIME_OF_NEED_LIVING_FLAME} />
</div>
<div>on {info.livingFlames.length} casts</div>
</td>
<td>{info.result}</td>
</tr>
))}
</tbody>
</table>
</LazyLoadStatisticBox>
);
}
}

export default TimeOfNeed;
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
RefreshBuffEvent,
RemoveBuffEvent,
RemoveBuffStackEvent,
SummonEvent,
} from 'parser/core/Events';
import { DUPLICATION_SPELLS, STASIS_CAST_IDS } from '../constants';
import { TIERS } from 'game/TIERS';
Expand Down Expand Up @@ -58,6 +59,7 @@ export const STASIS_FOR_RAMP = 'ForRamp';
export const ESSENCE_RUSH = 'EssenceRush';
export const T31_2PC = 'T31LFProc';
export const EB_REVERSION = 'EssenceBurstReversion';
export const TIME_OF_NEED_HEALING = 'TimeOfNeedHealing';

export enum ECHO_TYPE {
NONE,
Expand All @@ -76,6 +78,7 @@ const MAX_ESSENCE_BURST_DURATION = 32000; // 15s duration can refresh to 30s wit
const TA_BUFFER_MS = 6000 + CAST_BUFFER_MS; //TA pulses over 6s at 0% haste
const STASIS_BUFFER = 1000;
const T31_LF_AMOUNT = 3;
const TIME_OF_NEED_DURATION = 8000;

/*
This file is for attributing echo applications to hard casts or to temporal anomaly.
Expand Down Expand Up @@ -823,6 +826,19 @@ const EVENT_LINKS: EventLink[] = [
);
},
},
{
linkRelation: TIME_OF_NEED_HEALING,
linkingEventId: SPELLS.TIME_OF_NEED_SUMMON.id,
linkingEventType: EventType.Summon,
referencedEventId: [SPELLS.VERDANT_EMBRACE_HEAL.id, SPELLS.TIME_OF_NEED_LIVING_FLAME.id],
referencedEventType: EventType.Heal,
forwardBufferMs: TIME_OF_NEED_DURATION,
anySource: true,
anyTarget: true,
additionalCondition(linkingEvent, referencedEvent) {
return (linkingEvent as SummonEvent).targetID === (referencedEvent as HealEvent).sourceID;
},
},
];

/**
Expand Down Expand Up @@ -1062,4 +1078,8 @@ export function isEbFromReversion(
return HasRelatedEvent(event, EB_REVERSION);
}

export function getTimeOfNeedHealing(event: SummonEvent) {
return GetRelatedEvents<HealEvent>(event, TIME_OF_NEED_HEALING) ?? null;
}

export default CastLinkNormalizer;
10 changes: 10 additions & 0 deletions src/common/SPELLS/evoker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,16 @@ const spells = {
name: 'Trembling Earth',
icon: 'ability_evoker_eruption',
},
TIME_OF_NEED_LIVING_FLAME: {
id: 401382,
name: 'Living Flame (Past Self)',
icon: 'ability_evoker_livingflame',
},
TIME_OF_NEED_SUMMON: {
id: 368415,
name: 'Time of Need',
icon: 'ability_evoker_masterylifebinder_bronze',
},
} satisfies Record<string, Spell>;

export default spells;
Binary file added src/interface/images/avatars/Harrek-avatar.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit f4fa34a

Please sign in to comment.