Skip to content

Commit

Permalink
fix several bugs in buffMissing
Browse files Browse the repository at this point in the history
should also fix corresponding bugs in `debuffMissing` but need to test
and confirm
  • Loading branch information
emallson committed Mar 4, 2023
1 parent 2eed30f commit dd26c4b
Show file tree
Hide file tree
Showing 7 changed files with 332 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ const rotation_noBoC_chpdfb = build([
condition: cnd.buffMissing(talents.CHARRED_PASSIONS_TALENT, {
duration: 8000,
// TODO: verify pandemic cap
pandemicCap: 8000,
pandemicCap: 1,
timeRemaining: 1500,
}),
},
Expand Down
251 changes: 251 additions & 0 deletions src/parser/shared/metrics/apl/conditions/buffPresent.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
import Spell from 'common/SPELLS/Spell';
import MAGIC_SCHOOLS from 'game/MAGIC_SCHOOLS';
import Combatant from 'parser/core/Combatant';
import {
AnyEvent,
ApplyBuffEvent,
CastEvent,
EventType,
RefreshBuffEvent,
RemoveBuffEvent,
} from 'parser/core/Events';
import { AbilityRange } from 'parser/core/modules/Abilities';
import Ability from 'parser/core/modules/Ability';
import SPELL_CATEGORY from 'parser/core/SPELL_CATEGORY';
import { Condition, PlayerInfo } from '../index';
import { buffMissing } from './buffPresent';

const dummyBuff: Spell = {
id: 1,
name: 'Buff',
icon: 'buff',
};

const dummyCast: Spell = {
id: 2,
name: 'Cast',
icon: 'cast',
};

const playerInfo: PlayerInfo = {
playerId: 1,
combatant: {} as unknown as Combatant,
defaultRange: AbilityRange.Melee,
abilities: [
new Ability(undefined, {
spell: dummyCast.id,
category: SPELL_CATEGORY.ROTATIONAL,
}),
],
};

function runCondition<T>(cnd: Condition<T>, events: AnyEvent[], initialState?: T): T {
let state = initialState ?? cnd.init(playerInfo);

for (const event of events) {
state = cnd.update(state, event);
}

return state;
}

function cast(timestamp: number, spell: Spell): CastEvent {
return {
timestamp,
type: EventType.Cast,
ability: {
guid: spell.id,
name: spell.name,
type: MAGIC_SCHOOLS.ids.PHYSICAL,
abilityIcon: spell.icon,
},
sourceID: 1,
sourceIsFriendly: true,
targetIsFriendly: true,
targetID: 100,
};
}

function buff(
timestamp: number,
spell: Spell,
type: EventType.ApplyBuff | EventType.RemoveBuff | EventType.RefreshBuff,
): ApplyBuffEvent | RemoveBuffEvent | RefreshBuffEvent {
return {
timestamp,
type,
ability: {
guid: spell.id,
name: spell.name,
type: MAGIC_SCHOOLS.ids.PHYSICAL,
abilityIcon: spell.icon,
},
sourceID: 1,
targetID: 1,
sourceIsFriendly: true,
targetIsFriendly: true,
};
}

describe('buffMissing', () => {
describe('non-pandemic case', () => {
it('should validate a cast that occurs when no buff has been seen', () => {
const cnd = buffMissing(dummyBuff);
const state = runCondition(cnd, []);

expect(cnd.validate(state, cast(1000, dummyCast), dummyCast, [])).toBe(true);
});

it('should validate a cast that occurs after a buff expires', () => {
const cnd = buffMissing(dummyBuff);
const state = runCondition(cnd, [
buff(1000, dummyBuff, EventType.ApplyBuff),
buff(9000, dummyBuff, EventType.RemoveBuff),
]);

expect(cnd.validate(state, cast(10000, dummyCast), dummyCast, [])).toBe(true);
});

it('should not validate a cast that occurs during a buff', () => {
const cnd = buffMissing(dummyBuff);
const state = runCondition(cnd, [buff(1000, dummyBuff, EventType.ApplyBuff)]);

expect(cnd.validate(state, cast(10000, dummyCast), dummyCast, [])).toBe(false);

const nextState = runCondition(cnd, [buff(9000, dummyBuff, EventType.RemoveBuff)], state);

expect(cnd.validate(nextState, cast(12000, dummyCast), dummyCast, [])).toBe(true);
});

it('should not validate a cast that occurs after a buff has been refreshed', () => {
const cnd = buffMissing(dummyBuff);
const state = runCondition(cnd, [
buff(1000, dummyBuff, EventType.ApplyBuff),
buff(9000, dummyBuff, EventType.RefreshBuff),
]);

expect(cnd.validate(state, cast(10000, dummyCast), dummyCast, [])).toBe(false);

const nextState = runCondition(cnd, [buff(9000, dummyBuff, EventType.RemoveBuff)], state);

expect(cnd.validate(nextState, cast(12000, dummyCast), dummyCast, [])).toBe(true);
});

it('should use buff duration data to validate the expiration', () => {
const cnd = buffMissing(dummyBuff, {
duration: 8000,
timeRemaining: 0,
pandemicCap: 1,
});
const state = runCondition(cnd, [
buff(100, dummyBuff, EventType.ApplyBuff),
buff(1000, dummyBuff, EventType.RefreshBuff),
]);

expect(cnd.validate(state, cast(7000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(9001, dummyCast), dummyCast, [])).toBe(true);

const nextState = runCondition(cnd, [buff(9000, dummyBuff, EventType.RemoveBuff)], state);

expect(cnd.validate(nextState, cast(12000, dummyCast), dummyCast, [])).toBe(true);
});
});

describe('pandemic case', () => {
it('should validate early casts based on `timeRemaining`', () => {
const cnd = buffMissing(dummyBuff, {
duration: 8000,
timeRemaining: 2000,
pandemicCap: 1,
});
const state = runCondition(cnd, [
buff(100, dummyBuff, EventType.ApplyBuff),
buff(1000, dummyBuff, EventType.RefreshBuff),
]);

expect(cnd.validate(state, cast(5000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(7001, dummyCast), dummyCast, [])).toBe(true);
expect(cnd.validate(state, cast(10000, dummyCast), dummyCast, [])).toBe(true);
});

it('should extend the tracked buff duration up to the pandemic cap and use that for validation', () => {
const cnd = buffMissing(dummyBuff, {
duration: 8000,
timeRemaining: 2000,
pandemicCap: 1.5,
});
const state = runCondition(cnd, [
buff(100, dummyBuff, EventType.ApplyBuff),
buff(1000, dummyBuff, EventType.RefreshBuff),
]);

// buff should expire at 1000 + 1.5 * 8000 = 1000 + 8000 + 4000 = 13000

expect(cnd.validate(state, cast(5000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(7001, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(11000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(11001, dummyCast), dummyCast, [])).toBe(true);
expect(cnd.validate(state, cast(13001, dummyCast), dummyCast, [])).toBe(true);
});

it('should do a partial extend if less than (cap - 100%) of the buff remains', () => {
const cnd = buffMissing(dummyBuff, {
duration: 8000,
// turning off early refresh to check duration calculations more easily
timeRemaining: 0,
pandemicCap: 1.5,
});
const state = runCondition(cnd, [
buff(1000, dummyBuff, EventType.ApplyBuff),
buff(6500, dummyBuff, EventType.RefreshBuff),
]);

// buff should expire at 6500 + 8000 + (8000 - (6500 - 1000)) = 14500 + 8000 - 5500 = 17000
// un-pandemic expiration would be at 14500

expect(cnd.validate(state, cast(14000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(15000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(17000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(17001, dummyCast), dummyCast, [])).toBe(true);
});

it('should correctly validate an event in the refresh window with partial extension', () => {
const cnd = buffMissing(dummyBuff, {
duration: 8000,
// turning off early refresh to check duration calculations more easily
timeRemaining: 2000,
pandemicCap: 1.5,
});
const state = runCondition(cnd, [
buff(1000, dummyBuff, EventType.ApplyBuff),
buff(6500, dummyBuff, EventType.RefreshBuff),
]);

// buff should expire at 6500 + 8000 + (8000 - (6500 - 1000)) = 14500 + 8000 - 5500 = 17000
// un-pandemic expiration would be at 14500
// refresh window starts at 15001
expect(cnd.validate(state, cast(14000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(15000, dummyCast), dummyCast, [])).toBe(false);
expect(cnd.validate(state, cast(15001, dummyCast), dummyCast, [])).toBe(true);
expect(cnd.validate(state, cast(17000, dummyCast), dummyCast, [])).toBe(true);
});

it("should respect event-based removal prior to tracked duration's expiration", () => {
const cnd = buffMissing(dummyBuff, {
duration: 8000,
// turning off early refresh to check duration calculations more easily
timeRemaining: 2000,
pandemicCap: 1.5,
});
const state = runCondition(cnd, [
buff(1000, dummyBuff, EventType.ApplyBuff),
buff(6500, dummyBuff, EventType.RefreshBuff),
]);

expect(cnd.validate(state, cast(10000, dummyCast), dummyCast, [])).toBe(false);

const nextState = runCondition(cnd, [buff(12000, dummyBuff, EventType.RemoveBuff)], state);
expect(cnd.validate(nextState, cast(12001, dummyCast), dummyCast, [])).toBe(true);
});
});
});
65 changes: 39 additions & 26 deletions src/parser/shared/metrics/apl/conditions/buffPresent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type Spell from 'common/SPELLS/Spell';
import { SpellLink } from 'interface';
import { EventType } from 'parser/core/Events';

import { Condition, tenseAlt } from '../index';
import { AplTriggerEvent, Condition, tenseAlt } from '../index';
import { buffDuration, DurationData, PandemicData } from './util';

export function buffPresent(spell: Spell, latencyOffset: number = 0): Condition<number | null> {
Expand Down Expand Up @@ -34,59 +34,72 @@ export function buffPresent(spell: Spell, latencyOffset: number = 0): Condition<
};
}

/**
* Internal helper function shared with debuff code for handling `buffMissing` / `debuffMissing` with pandemic.
*/
export function validateBuffMissing(
event: AplTriggerEvent,
ruleSpell: Spell,
state: DurationData | undefined,
pandemic: PandemicData | undefined,
): boolean {
if (state === undefined) {
// buff is missing
return true;
} else if (state.referenceTime + 200 > event.timestamp) {
// buff was *just* applied, possibly by this very spell. treat it as optional
return event.ability.guid === ruleSpell.id;
} else if (pandemic && state.timeRemaining !== undefined) {
// otherwise, return true if we can pandemic this buff
return (
ruleSpell.id === event.ability.guid &&
state.referenceTime + state.timeRemaining < event.timestamp + pandemic.timeRemaining
);
} else {
// finally, return false if we have no pandemic info. this means we only remove the buff when we get a RemoveBuff event.
return false;
}
}

/**
The rule applies when the buff `spell` is missing. The `optPandemic`
parameter gives the ability to allow early refreshes to prevent a buff
dropping, but this will not *require* early refreshes.
Unless otherwise specified by `optPandemic.pandemicCap`, the buff is
assumed to refresh to up to 3x duration.
**/
export function buffMissing(
spell: Spell,
optPandemic?: PandemicData,
): Condition<DurationData | null> {
const pandemic: PandemicData = {
timeRemaining: 0,
duration: 0,
...optPandemic,
};

): Condition<DurationData | undefined> {
return {
key: `buffMissing-${spell.id}`,
init: () => null,
init: () => undefined,
update: (state, event) => {
switch (event.type) {
case EventType.RefreshBuff:
case EventType.ApplyBuff:
if (event.ability.guid === spell.id) {
return {
referenceTime: event.timestamp,
timeRemaining: buffDuration(state?.timeRemaining, pandemic),
timeRemaining: optPandemic
? buffDuration(event.timestamp, state, optPandemic)
: undefined,
};
}
break;
case EventType.RemoveBuff:
if (event.ability.guid === spell.id) {
return null;
return undefined;
}
break;
}

return state;
},
validate: (state, event, ruleSpell) => {
if (state === null) {
// buff is missing
return true;
} else if (state.referenceTime + 200 > event.timestamp) {
// buff was *just* applied, possibly by this very spell. treat it as optional
return event.ability.guid === ruleSpell.id;
} else {
// otherwise, return true if we can pandemic this buff
return (
ruleSpell.id === event.ability.guid &&
state.referenceTime + state.timeRemaining < event.timestamp + pandemic.timeRemaining
);
}
},
validate: (state, event, ruleSpell) =>
validateBuffMissing(event, ruleSpell, state, optPandemic),
describe: (tense) => (
<>
<SpellLink id={spell.id} /> {tenseAlt(tense, 'is', 'was')} missing{' '}
Expand Down
Loading

0 comments on commit dd26c4b

Please sign in to comment.