Skip to content
This repository was archived by the owner on Oct 11, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
130 changes: 64 additions & 66 deletions libs/gw2_analytics/src/gw2_analytics/ei_compare.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,14 +223,19 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
damage_events = [event for event in event_list if isinstance(event, DamageEvent)]
actor_damage_events = [event for event in damage_events if event.src_master_instid == 0]
cc_events = [event for event in event_list if isinstance(event, CCEvent)]
down_events = [event for event in event_list if isinstance(event, DownEvent)]
death_events = [event for event in event_list if isinstance(event, DeathEvent)]
health_events = [event for event in event_list if isinstance(event, HealthUpdateEvent)]
up_events = [event for event in event_list if isinstance(event, UpEvent)]
outcome_events = [event for event in event_list if isinstance(event, CombatOutcomeEvent)]
down_rows = DownContributionAggregator().aggregate(
actor_damage_events,
[event for event in event_list if isinstance(event, DownEvent)],
[event for event in event_list if isinstance(event, DeathEvent)],
down_events,
death_events,
duration_ms / 1000,
health_events=[event for event in event_list if isinstance(event, HealthUpdateEvent)],
up_events=[event for event in event_list if isinstance(event, UpEvent)],
outcome_events=[event for event in event_list if isinstance(event, CombatOutcomeEvent)],
health_events=health_events,
up_events=up_events,
outcome_events=outcome_events,
cc_events=cc_events,
)
down_by_source = {row.source_agent_id: row for row in down_rows}
Expand All @@ -245,6 +250,41 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
for fight_agent in fight.agents:
if fight_agent.instance_id:
agent_ids_by_instance[fight_agent.instance_id].add(fight_agent.id)
down_events_by_source: dict[int, list[DownEvent]] = defaultdict(list)
death_events_by_source: dict[int, list[DeathEvent]] = defaultdict(list)
health_events_by_source: dict[int, list[HealthUpdateEvent]] = defaultdict(list)
up_events_by_source: dict[int, list[UpEvent]] = defaultdict(list)
outcome_events_by_target: dict[int, list[CombatOutcomeEvent]] = defaultdict(list)
block_count_by_source: dict[int, int] = defaultdict(int)
dodge_count_by_source: dict[int, int] = defaultdict(int)
downed_buff_times_by_target: dict[int, set[int]] = defaultdict(set)
for down_event in down_events:
down_events_by_source[down_event.source_agent_id].append(down_event)
for death_event in death_events:
death_events_by_source[death_event.source_agent_id].append(death_event)
for health_event in health_events:
health_events_by_source[health_event.source_agent_id].append(health_event)
for up_event in up_events:
up_events_by_source[up_event.source_agent_id].append(up_event)
for outcome_event in outcome_events:
outcome_events_by_target[outcome_event.target_agent_id].append(outcome_event)
for indexed_event in event_list:
if isinstance(indexed_event, BlockEvent):
block_count_by_source[indexed_event.source_agent_id] += 1
elif isinstance(indexed_event, DodgeEvent):
dodge_count_by_source[indexed_event.source_agent_id] += 1
elif (
isinstance(indexed_event, BoonApplyEvent)
and indexed_event.kind == "apply"
and indexed_event.skill_id == 770
):
downed_buff_times_by_target[indexed_event.target_agent_id].add(indexed_event.time_ms)

def indexed_events(index: dict[int, list[Any]], agent_ids: set[int]) -> list[Any]:
return [
indexed_event for agent_id in agent_ids for indexed_event in index.get(agent_id, [])
]

targets = expected.get("targets") if isinstance(expected.get("targets"), list) else []
buff_map = expected.get("buffMap")
condition_skill_ids: set[int] | None = (
Expand All @@ -271,11 +311,11 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
start_time_ms=origin,
healing_by_agent={agent.id: agent.healing for agent in fight.agents},
)
for event in event_list:
if isinstance(event, (BoonApplyEvent, BuffApplyEvent)):
tracker.process(event)
elif isinstance(event, DespawnEvent):
tracker.end_agent(event.source_agent_id, event.time_ms + 10)
for tracked_event in event_list:
if isinstance(tracked_event, (BoonApplyEvent, BuffApplyEvent)):
tracker.process(tracked_event)
elif isinstance(tracked_event, DespawnEvent):
tracker.end_agent(tracked_event.source_agent_id, tracked_event.time_ms + 10)
rotation = build_skill_rotation(
event_list,
duration_ms,
Expand Down Expand Up @@ -361,17 +401,11 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
defenses = player.get("defenses")
if isinstance(defenses, list) and defenses and isinstance(defenses[0], dict):
taken = [event for event in damage_events if event.target_agent_id in agent_ids]
downed = [
event
for event in event_list
if isinstance(event, DownEvent) and event.source_agent_id in agent_ids
]
downed = indexed_events(down_events_by_source, agent_ids)
outcome_downs = {
event.time_ms
for event in event_list
if isinstance(event, CombatOutcomeEvent)
and event.outcome == "downed"
and event.target_agent_id in agent_ids
for event in indexed_events(outcome_events_by_target, agent_ids)
if event.outcome == "downed"
}
defense_values = {
"damageTaken": sum(event.damage for event in taken),
Expand All @@ -382,26 +416,15 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
"powerDamageTaken": sum(
event.damage - _condition_damage(event, condition_skill_ids) for event in taken
),
"blockedCount": sum(
isinstance(event, BlockEvent) and event.source_agent_id == agent.id
for event in event_list
),
"evadedCount": sum(
isinstance(event, DodgeEvent) and event.source_agent_id == agent.id
for event in event_list
),
"blockedCount": block_count_by_source[agent.id],
"evadedCount": dodge_count_by_source[agent.id],
"downCount": (
len(outcome_downs)
if outcome_downs and not anonymous
else len(
{
event.time_ms
for event in event_list
if isinstance(event, BoonApplyEvent)
and event.kind == "apply"
and event.skill_id == 770
and event.target_agent_id in agent_ids
}
set().union(
*(downed_buff_times_by_target[agent_id] for agent_id in agent_ids)
)
)
)
or (
Expand All @@ -412,10 +435,7 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
else 0
),
"downDuration": sum(event.downtime_ms for event in downed),
"deadCount": sum(
isinstance(event, DeathEvent) and event.source_agent_id in agent_ids
for event in event_list
),
"deadCount": len(indexed_events(death_events_by_source, agent_ids)),
}
values["defenses"] = defense_values
_compare_fields(f"{prefix}.defenses", defenses[0], defense_values, differences)
Expand Down Expand Up @@ -556,34 +576,12 @@ def compare_elite_insights( # noqa: PLR0912, PLR0915
target_cc = [event for event in source_cc if event.target_agent_id in target_ids]
target_down_rows = DownContributionAggregator().aggregate(
actor_target_damage,
[
event
for event in event_list
if isinstance(event, DownEvent) and event.source_agent_id in target_ids
],
[
event
for event in event_list
if isinstance(event, DeathEvent) and event.source_agent_id in target_ids
],
indexed_events(down_events_by_source, target_ids),
indexed_events(death_events_by_source, target_ids),
duration_ms / 1000,
health_events=[
event
for event in event_list
if isinstance(event, HealthUpdateEvent)
and event.source_agent_id in target_ids
],
up_events=[
event
for event in event_list
if isinstance(event, UpEvent) and event.source_agent_id in target_ids
],
outcome_events=[
event
for event in event_list
if isinstance(event, CombatOutcomeEvent)
and event.target_agent_id in target_ids
],
health_events=indexed_events(health_events_by_source, target_ids),
up_events=indexed_events(up_events_by_source, target_ids),
outcome_events=indexed_events(outcome_events_by_target, target_ids),
cc_events=target_cc,
)
target_down_by_source = {row.source_agent_id: row for row in target_down_rows}
Expand Down
117 changes: 67 additions & 50 deletions libs/gw2_analytics/src/gw2_analytics/rotation.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from __future__ import annotations

from bisect import bisect_left, bisect_right
from collections import defaultdict
from collections.abc import Collection, Iterable

from pydantic import BaseModel, ConfigDict
Expand Down Expand Up @@ -226,12 +228,55 @@ def build_skill_rotation( # noqa: PLR0912, PLR0915
if not event_list:
return []
origin = start_time_ms if start_time_ms is not None else min(e.time_ms for e in event_list)
swaps = [e for e in event_list if isinstance(e, WeaponSwapEvent)]
event_times = [event.time_ms for event in event_list]
swaps_by_agent: dict[int, list[WeaponSwapEvent]] = defaultdict(list)
activations_by_agent: dict[int, list[SkillActivationEvent]] = defaultdict(list)
spawn_owner_by_target: dict[int, int] = {}
for indexed_event in event_list:
if isinstance(indexed_event, WeaponSwapEvent):
swaps_by_agent[indexed_event.source_agent_id].append(indexed_event)
elif isinstance(indexed_event, SkillActivationEvent):
activations_by_agent[indexed_event.source_agent_id].append(indexed_event)
elif isinstance(indexed_event, SpawnEvent):
spawn_owner_by_target.setdefault(
indexed_event.target_agent_id,
indexed_event.source_agent_id,
)
swap_times_by_agent = {
agent_id: [swap.time_ms for swap in agent_swaps]
for agent_id, agent_swaps in swaps_by_agent.items()
}
active: dict[tuple[int, int], SkillActivationEvent] = {}
casts: list[SkillCast] = []
last_instant: dict[tuple[int, int], int] = {}
active_buff_until: dict[tuple[int, int], int] = {}

def nearby_events(time_ms: int, radius_ms: int) -> list[Event]:
return event_list[
bisect_left(event_times, time_ms - radius_ms) : bisect_right(
event_times,
time_ms + radius_ms,
)
]

def nearby_swap(agent_id: int, time_ms: int, radius_ms: int = 5) -> WeaponSwapEvent | None:
agent_swaps = swaps_by_agent.get(agent_id, [])
agent_swap_times = swap_times_by_agent.get(agent_id, [])
for swap in agent_swaps[
bisect_left(agent_swap_times, time_ms - radius_ms) : bisect_right(
agent_swap_times,
time_ms + radius_ms,
)
]:
if abs(swap.time_ms - time_ms) < radius_ms:
return swap
return None

def next_swap_time_after(agent_id: int, time_ms: int) -> int:
agent_swap_times = swap_times_by_agent.get(agent_id, [])
index = bisect_right(agent_swap_times, time_ms + 10)
return agent_swap_times[index] if index < len(agent_swap_times) else 1 << 63

def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None:
key = (source, skill_id)
if time_ms - last_instant.get(key, -(1 << 63)) >= icd:
Expand Down Expand Up @@ -272,23 +317,19 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
)
)
elif isinstance(event, WeaponSwapEvent):
next_swap_time = next(
(
swap.time_ms
for swap in swaps
if swap.source_agent_id == event.source_agent_id
and swap.time_ms > event.time_ms + 10
),
1 << 63,
)
next_swap_time = next_swap_time_after(event.source_agent_id, event.time_ms)
agent_activations = activations_by_agent.get(event.source_agent_id, [])
activation_times = [activation.time_ms for activation in agent_activations]
for kit_skill, bundle_skills in _ENGINEER_KIT_BUNDLES.items():
if (
sum(
isinstance(other, SkillActivationEvent)
and other.source_agent_id == event.source_agent_id
and event.time_ms + 10 <= other.time_ms < next_swap_time
and other.skill_id in bundle_skills
for other in event_list
other.skill_id in bundle_skills
for other in agent_activations[
bisect_left(activation_times, event.time_ms + 10) : bisect_left(
activation_times,
next_swap_time,
)
]
)
>= 2
):
Expand All @@ -309,11 +350,7 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
if instant is not None:
skill_id, before_swap = instant
time_ms = event.time_ms
if before_swap and any(
swap.source_agent_id == event.target_agent_id
and abs(swap.time_ms - event.time_ms) < 5
for swap in swaps
):
if before_swap and nearby_swap(event.target_agent_id, event.time_ms):
time_ms -= 1
add_instant(event.target_agent_id, skill_id, time_ms)
mapped = (
Expand All @@ -328,19 +365,11 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
mapped = None
if mapped is not None:
mapped_time = event.time_ms
nearby_swap = next(
(
swap
for swap in swaps
if swap.source_agent_id == event.target_agent_id
and abs(swap.time_ms - event.time_ms) < 5
),
None,
)
if nearby_swap is not None and event.skill_id in _BEFORE_SWAP_BUFFS:
mapped_time = nearby_swap.time_ms - 1
elif nearby_swap is not None and event.skill_id in _AFTER_SWAP_BUFFS:
mapped_time = max(mapped_time, nearby_swap.time_ms + 1)
swap = nearby_swap(event.target_agent_id, event.time_ms)
if swap is not None and event.skill_id in _BEFORE_SWAP_BUFFS:
mapped_time = swap.time_ms - 1
elif swap is not None and event.skill_id in _AFTER_SWAP_BUFFS:
mapped_time = max(mapped_time, swap.time_ms + 1)
add_instant(event.target_agent_id, mapped, mapped_time)
if event.kind == "apply":
active_buff_until[buff_key] = max(
Expand All @@ -356,15 +385,7 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
and event.skill_id == 59536
and event.target_agent_id in siege_turtle_agent_ids
):
owner = next(
(
spawn.source_agent_id
for spawn in event_list
if isinstance(spawn, SpawnEvent)
and spawn.target_agent_id == event.target_agent_id
),
0,
)
owner = spawn_owner_by_target.get(event.target_agent_id, 0)
if owner:
add_instant(owner, 65418, event.time_ms)
elif isinstance(event, DamageEvent) and event.skill_id in _DAMAGE_CASTS:
Expand Down Expand Up @@ -394,7 +415,7 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
and other.source_agent_id == caster
and other.target_agent_id in clone_agent_ids
and abs(other.time_ms - event.time_ms) < 30
for other in event_list
for other in nearby_events(event.time_ms, 29)
)
else -27
)
Expand All @@ -409,7 +430,7 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
and other.skill_id == 10243
and other.target_agent_id == caster
and abs(other.time_ms - event.time_ms) < 10
for other in event_list
for other in nearby_events(event.time_ms, 9)
)
else 10191
if caster in mesmer_agent_ids
Expand All @@ -428,11 +449,7 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
continue
needs_related = by_dst or event.guid in _SECONDARY_EFFECTS
needs_related = needs_related or event.guid in _MESMER_SHATTER_EFFECTS
related = (
[other for other in event_list if abs(other.time_ms - event.time_ms) < 10]
if needs_related
else []
)
related = nearby_events(event.time_ms, 9) if needs_related else []
if (
event.guid == "122BA55CCDF2B643929F6C4A97226DC9"
and sum(
Expand Down Expand Up @@ -472,7 +489,7 @@ def add_instant(source: int, skill_id: int, time_ms: int, icd: int = 50) -> None
and other.source_agent_id == caster
and other.skill_id == 38767
and abs(other.time_ms - event.time_ms) < 50
for other in event_list
for other in nearby_events(event.time_ms, 49)
):
add_instant(caster, effect_skill_id, event.time_ms)

Expand Down
Loading