diff --git a/libs/gw2_analytics/src/gw2_analytics/rotation.py b/libs/gw2_analytics/src/gw2_analytics/rotation.py index 636069df..7f276dd9 100644 --- a/libs/gw2_analytics/src/gw2_analytics/rotation.py +++ b/libs/gw2_analytics/src/gw2_analytics/rotation.py @@ -955,17 +955,62 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None owner = spawn_owner_by_target.get(event.target_agent_id) or event.source_agent_id if owner: add_instant(owner, 12658, event.time_ms) + # Spiteful Spirit (29560) - EI has two finders: + # 1. DamageCastFinder: disabled when effect data exists (UsingDisableWithEffectData) + # 2. EffectCastFinder for UnholyBurst: triggers on effect with + # DesertShroud/related hit checks elif isinstance(event, DamageEvent) and event.skill_id == 29560: source_is_necro = ( not professions or professions.get(event.source_agent_id) is Profession.NECROMANCER ) - if source_is_necro and not any( - isinstance(other, EffectEvent) - and other.guid == "C4E8DD3234E0C647993857940ED79AC1" - and other.source_agent_id == event.source_agent_id - and abs(other.time_ms - event.time_ms) < 50 - for other in nearby_events(event.time_ms, 49) - ): + if not source_is_necro: + continue + + # Check if there's any UnholyBurst effect in the fight (global check) + has_unholy_burst_in_fight = any( + isinstance(other, EffectEvent) and other.guid == "C4E8DD3234E0C647993857940ED79AC1" + for other in event_list + ) + + if has_unholy_burst_in_fight: + # EI uses EffectCastFinder: only emit when UnholyBurst effect is present + # with DesertShroud and related hit checks + unholy_burst_nearby = any( + isinstance(other, EffectEvent) + and other.guid == "C4E8DD3234E0C647993857940ED79AC1" + and other.source_agent_id == event.source_agent_id + and abs(other.time_ms - event.time_ms) < 100 + for other in nearby_events(event.time_ms, 100) + ) + if not unholy_burst_nearby: + continue + + # DesertShroud check: no DesertShroud buff removal within 50ms + desert_shroud_removal = any( + isinstance(other, BoonApplyEvent) + and other.kind == "remove_all" + and other.skill_id == 40052 # DesertShroudBuff + and other.source_agent_id == event.source_agent_id + and abs(other.time_ms - event.time_ms) < 50 + for other in nearby_events(event.time_ms, 50) + ) + if desert_shroud_removal: + continue + + # Related hit check: no UnholyBurst hit from same caster within 10ms + related_hit = any( + isinstance(other, DamageEvent) + and other.source_agent_id == event.source_agent_id + and other.skill_id == 38767 # UnholyBurst + and abs(other.time_ms - event.time_ms) < 10 + for other in nearby_events(event.time_ms, 10) + ) + if related_hit: + continue + + add_instant(event.source_agent_id, event.skill_id, event.time_ms) + else: + # No UnholyBurst effect data in fight: use DamageCastFinder add_instant(event.source_agent_id, event.skill_id, event.time_ms) elif isinstance(event, DamageEvent) and event.skill_id in _DAMAGE_CASTS: add_instant( diff --git a/libs/gw2_analytics/tests/test_ei_compare.py b/libs/gw2_analytics/tests/test_ei_compare.py index 502e23fe..539d1ee1 100644 --- a/libs/gw2_analytics/tests/test_ei_compare.py +++ b/libs/gw2_analytics/tests/test_ei_compare.py @@ -2,8 +2,6 @@ from typing import Any, cast -import pytest - from gw2_analytics.ei_compare import _skill_stats, compare_elite_insights from gw2_core import ( Agent, @@ -23,7 +21,6 @@ def _rows(result: dict[str, object]) -> dict[str, dict[str, Any]]: return {str(row["key"]): row for row in cast("list[dict[str, Any]]", result["results"])} -@pytest.mark.skip(reason="Pre-existing flaky test - deadCount mismatch for Non Squad Player 5") def test_compare_elite_insights_keeps_first_anonymous_agent_for_shared_instance() -> None: fight = Fight( id="fight", diff --git a/scripts/ei-parity/known-deltas.json b/scripts/ei-parity/known-deltas.json index 29277218..f69920aa 100644 --- a/scripts/ei-parity/known-deltas.json +++ b/scripts/ei-parity/known-deltas.json @@ -1,406 +1,4 @@ { "schema_version": 1, - "rules": [ - { - "id": "71356-first-heal-skip", - "selector": { - "skill_id": 71356, - "bucket": "players.rotation" - }, - "constraint": { - "max_abs_delta": 0.001 - }, - "reason": "EI skips first heal of 71356 per caster (EXTHealingCastFinder edge case)", - "remove_when": "fixed" - }, - { - "id": "13980-first-heal-skip", - "selector": { - "skill_id": 13980, - "bucket": "players.rotation" - }, - "constraint": { - "max_abs_delta": 0.001 - }, - "reason": "EI skips first heal of 13980 per caster (EXTHealingCastFinder edge case)", - "remove_when": "fixed" - }, - { - "id": "krill-group-slice-9617", - "selector": { - "account": "krill le faucheur.1679", - "slice": 9617, - "bucket": "players.group" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "EI group=1 for first slice of multi-slice WvW player", - "remove_when": "fixed" - }, - { - "id": "coligroup-slice-10523", - "selector": { - "account": "Coli.7136", - "slice": 10523, - "bucket": "players.group" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "EI group=1 for first slice of multi-slice WvW player", - "remove_when": "fixed" - }, - { - "id": "mikey-group-slice-3731", - "selector": { - "account": "Mikey.4982", - "slice": 3731, - "bucket": "players.group" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "EI group=1 for first slice of multi-slice WvW player", - "remove_when": "fixed" - }, - { - "id": "sharpsteel-group-slice-214", - "selector": { - "account": "SharpSteel.3051", - "slice": 214, - "bucket": "players.group" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "EI group=1 for first slice of multi-slice WvW player", - "remove_when": "fixed" - }, - { - "id": "empiria-group-slice-11322", - "selector": { - "account": "empiria.8961", - "slice": 11322, - "bucket": "players.group" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "EI group=1 for first slice of multi-slice WvW player", - "remove_when": "fixed" - }, - { - "id": "krill-group-slice-5561", - "selector": { - "account": "krill le faucheur.1679", - "slice": 5561, - "bucket": "players.group" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "EI group=1 for first slice of multi-slice WvW player", - "remove_when": "fixed" - }, - { - "id": "SagerKing.3907-slice-downContribution", - "selector": { - "account": "SagerKing.3907", - "slice": 1, - "bucket": "players.statsAll.downContribution" - }, - "constraint": { - "max_abs_delta": 449 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "EstaticFear.7692-slice-downContribution", - "selector": { - "account": "EstaticFear.7692", - "slice": 3, - "bucket": "players.statsAll.downContribution" - }, - "constraint": { - "max_abs_delta": 592 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Kurupt.6378-slice-appliedCrowdControlDownContribution", - "selector": { - "account": "Kurupt.6378", - "slice": 0, - "bucket": "players.statsAll.appliedCrowdControlDownContribution" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Kurupt.6378-slice-appliedCrowdControlDurationDownContribution", - "selector": { - "account": "Kurupt.6378", - "slice": 0, - "bucket": "players.statsAll.appliedCrowdControlDurationDownContribution" - }, - "constraint": { - "max_abs_delta": 1000 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Kurupt.6378-slice-downContribution", - "selector": { - "account": "Kurupt.6378", - "slice": 0, - "bucket": "players.statsAll.downContribution" - }, - "constraint": { - "max_abs_delta": 2657 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "LuiStheGamers.5132-slice-downContribution", - "selector": { - "account": "LuiStheGamers.5132", - "slice": 1, - "bucket": "players.statsAll.downContribution" - }, - "constraint": { - "max_abs_delta": 40 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "creative.1094-slice-appliedCrowdControl", - "selector": { - "account": "creative.1094", - "slice": 0, - "bucket": "players.statsAll.appliedCrowdControl" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "creative.1094-slice-appliedCrowdControlDuration", - "selector": { - "account": "creative.1094", - "slice": 0, - "bucket": "players.statsAll.appliedCrowdControlDuration" - }, - "constraint": { - "max_abs_delta": 2000 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Schoko.1536-slice-downContribution", - "selector": { - "account": "Schoko.1536", - "slice": 2, - "bucket": "players.statsAll.downContribution" - }, - "constraint": { - "max_abs_delta": 2436 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Nikola.8512-slice-appliedCrowdControlDownContribution", - "selector": { - "account": "Nikola.8512", - "slice": 1, - "bucket": "players.statsAll.appliedCrowdControlDownContribution" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Nikola.8512-slice-appliedCrowdControlDurationDownContribution", - "selector": { - "account": "Nikola.8512", - "slice": 1, - "bucket": "players.statsAll.appliedCrowdControlDurationDownContribution" - }, - "constraint": { - "max_abs_delta": 1500 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "Nikola.8512-slice-downContribution", - "selector": { - "account": "Nikola.8512", - "slice": 1, - "bucket": "players.statsAll.downContribution" - }, - "constraint": { - "max_abs_delta": 1177 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats", - "remove_when": "fixed" - }, - { - "id": "EstaticFear.7692-targets-downContribution", - "selector": { - "account": "EstaticFear.7692", - "slice": 3, - "bucket": "players.statsTargets.downContribution" - }, - "constraint": { - "max_abs_delta": 592 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Kurupt.6378-targets-appliedCrowdControlDownContribution", - "selector": { - "account": "Kurupt.6378", - "slice": 0, - "bucket": "players.statsTargets.appliedCrowdControlDownContribution" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Kurupt.6378-targets-appliedCrowdControlDurationDownContribution", - "selector": { - "account": "Kurupt.6378", - "slice": 0, - "bucket": "players.statsTargets.appliedCrowdControlDurationDownContribution" - }, - "constraint": { - "max_abs_delta": 1000 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Kurupt.6378-targets-downContribution", - "selector": { - "account": "Kurupt.6378", - "slice": 0, - "bucket": "players.statsTargets.downContribution" - }, - "constraint": { - "max_abs_delta": 2657 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "LuiStheGamers.5132-targets-downContribution", - "selector": { - "account": "LuiStheGamers.5132", - "slice": 1, - "bucket": "players.statsTargets.downContribution" - }, - "constraint": { - "max_abs_delta": 40 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "creative.1094-targets-appliedCrowdControl", - "selector": { - "account": "creative.1094", - "slice": 0, - "bucket": "players.statsTargets.appliedCrowdControl" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "creative.1094-targets-appliedCrowdControlDuration", - "selector": { - "account": "creative.1094", - "slice": 0, - "bucket": "players.statsTargets.appliedCrowdControlDuration" - }, - "constraint": { - "max_abs_delta": 2000 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Schoko.1536-targets-downContribution", - "selector": { - "account": "Schoko.1536", - "slice": 2, - "bucket": "players.statsTargets.downContribution" - }, - "constraint": { - "max_abs_delta": 2436 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Nikola.8512-targets-appliedCrowdControlDownContribution", - "selector": { - "account": "Nikola.8512", - "slice": 1, - "bucket": "players.statsTargets.appliedCrowdControlDownContribution" - }, - "constraint": { - "max_abs_delta": 1 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Nikola.8512-targets-appliedCrowdControlDurationDownContribution", - "selector": { - "account": "Nikola.8512", - "slice": 1, - "bucket": "players.statsTargets.appliedCrowdControlDurationDownContribution" - }, - "constraint": { - "max_abs_delta": 1500 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - }, - { - "id": "Nikola.8512-targets-downContribution", - "selector": { - "account": "Nikola.8512", - "slice": 1, - "bucket": "players.statsTargets.downContribution" - }, - "constraint": { - "max_abs_delta": 1177 - }, - "reason": "Slice-matching divergence for down/CC contribution in stats (targets)", - "remove_when": "fixed" - } - ] -} \ No newline at end of file + "rules": [] +}