diff --git a/libs/gw2_evtc_parser/src/gw2_evtc_parser/parser.py b/libs/gw2_evtc_parser/src/gw2_evtc_parser/parser.py index 97c7d3e8..aa43df53 100644 --- a/libs/gw2_evtc_parser/src/gw2_evtc_parser/parser.py +++ b/libs/gw2_evtc_parser/src/gw2_evtc_parser/parser.py @@ -367,12 +367,6 @@ 9: {52, 63, 68, 79}, # Revenant } -#: v0.16.3: map legacy arcdps elite IDs to the current API-correct -#: ID. Old arcdps revisions sometimes wrote IDs that differ from -#: the GW2 v2 API IDs used by EVTC2025+. Currently identity (no -#: overrides needed for the known old-format IDs) — the EliteSpec -#: enum already uses API-correct values. -_LEGACY_ELITE_OVERRIDE: Final[dict[int, int]] = {} #: v0.11.0 hotfix: sanity cap for damage / heal / strip values. #: arcdps uses INT32_MAX (2,147,483,647) as a sentinel for "no @@ -1652,11 +1646,10 @@ def _decode_agent_2025(data: bytes, offset: int) -> Agent: def _decode_agent(data: bytes, offset: int) -> Agent: """Decode a single 96-byte legacy agent record at ``offset``. - The legacy path applies ``_LEGACY_ELITE_OVERRIDE`` to map old - arcdps IDs to API IDs, then validates the mapped value against - the profession. If validation fails, the raw value is tried - (handles collision IDs 55 and 63 where the raw value IS the - API ID for one profession). If both fail, degrades to BASE. + The elite spec is validated against the agent's profession via + ``_VALID_ELITE_BY_PROFESSION``, which resolves shared collision + IDs (55, 63, 73, 74, 75, 77) by profession membership. Falls + back to ``EliteSpec.BASE`` if validation fails. """ aid, prof_raw, elite_raw, _tough, _conc, _heal, _width, name_buf = _AGENT_STRUCT.unpack_from( data, offset @@ -1706,14 +1699,8 @@ def _decode_agent(data: bytes, offset: int) -> Agent: # against the profession. If the override fails validation, try # the raw value (handles collision IDs 55 and 63 where the raw # value IS the API ID for one profession). - mapped_elite = _LEGACY_ELITE_OVERRIDE.get(int(elite_raw), int(elite_raw)) valid_set = _VALID_ELITE_BY_PROFESSION.get(int(prof_raw)) - if valid_set is not None and mapped_elite in valid_set: - elite = EliteSpec(mapped_elite) - elif valid_set is not None and int(elite_raw) in valid_set: - # Raw value passes — handles collisions where the override - # maps away from a value that IS valid for this profession - # (e.g. Revenant with elite_raw=63 → 63 in Rev set → Renegade). + if valid_set is not None and int(elite_raw) in valid_set: elite = EliteSpec(int(elite_raw)) else: elite = EliteSpec.BASE diff --git a/web/tests/components/PlayerReadoutCells.test.tsx b/web/tests/components/PlayerReadoutCells.test.tsx index 59d65ca1..a9e07eed 100644 --- a/web/tests/components/PlayerReadoutCells.test.tsx +++ b/web/tests/components/PlayerReadoutCells.test.tsx @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { render, screen } from "@testing-library/react"; import React from "react"; +import { buildPlayerReadoutRow } from "../fixtures/playerReadoutRow"; import { CommanderCellRenderer, EliteSpecCellRenderer, @@ -13,7 +14,6 @@ import { getProfessionLabel, getEliteLabel, } from "@/components/icons/Professions"; -import type { PlayerReadoutOut } from "@/lib/api"; /** * F17 W.1 v2 — vitest specs for the wire-format-aware icon @@ -30,90 +30,9 @@ import type { PlayerReadoutOut } from "@/lib/api"; * literals which never matched the wire. */ -function makeRow(overrides: Partial): PlayerReadoutOut { - return { - agent_id: 1, - account_name: ":test.1234", - name: "Test Character", - profession: "PROF(1)", // Guardian - elite_spec: "BASE", - subgroup: 1, - is_commander: false, - roles: ["DPS"], - damage: { - dps_total: 0, - dps_power: 0, - dps_condi: 0, - strips: 0, - cc_applied: 0, - down_contribution_dps: 0, - kills: 0, - cleave_targets: 0, - kill_participation: 0, - }, - heal: { - heal_total: 0, - hps: 0, - barrier_total: 0, - barrier_ps: 0, - cleanses: 0, - stun_breaks: 0, - }, - boons: { - boons_out_rate: 0, - boons_in_rate: 0, - stability_out: 0, - alacrity_out: 0, - resistance_out: 0, - aegis_out: 0, - superspeed_out: 0, - stealth_out: 0, - might_uptime: null, - fury_uptime: null, - quickness_uptime: null, - alacrity_uptime: null, - protection_uptime: null, - regeneration_uptime: null, - vigor_uptime: null, - aegis_uptime: null, - stability_uptime: null, - swiftness_uptime: null, - resistance_uptime: null, - resolution_uptime: null, - superspeed_uptime: null, - stealth_uptime: null, - other_boons_out: {}, - outgoing_might: null, - outgoing_fury: null, - outgoing_quickness: null, - outgoing_alacrity: null, - outgoing_protection: null, - outgoing_regeneration: null, - outgoing_vigor: null, - outgoing_aegis: null, - outgoing_stability: null, - outgoing_swiftness: null, - outgoing_resistance: null, - outgoing_resolution: null, - outgoing_superspeed: null, - outgoing_stealth: null, - }, - defense: { - damage_taken: 0, - cc_taken: 0, - deaths: 0, - time_downed_ms: 0, - dodges: 0, - blocks: 0, - interrupts: 0, - barrier_absorbed: 0, - presence_pct: null, - dist_to_commander: null, - kill_participation: 0, - }, - ...overrides, - }; -} +// makeRow is aliased to buildPlayerReadoutRow for backward compat +// with existing test call-sites that import it as `makeRow`. +const makeRow = buildPlayerReadoutRow; /* ------------------------------------------------------------------ * * parseWireFormat diff --git a/web/tests/components/compare-readout-table.test.tsx b/web/tests/components/compare-readout-table.test.tsx index 8c40d628..2a9d369d 100644 --- a/web/tests/components/compare-readout-table.test.tsx +++ b/web/tests/components/compare-readout-table.test.tsx @@ -3,18 +3,21 @@ import { render, screen } from "@testing-library/react"; import React from "react"; import { CompareReadoutTable } from "@/components/CompareReadoutTable"; -import type { PlayerReadoutOut } from "@/lib/api"; - -function buildRow(overrides: Partial = {}): PlayerReadoutOut { - return { - agent_id: 1, - account_name: ":test.1234", +import { buildPlayerReadoutRow } from "../fixtures/playerReadoutRow"; + +/** + * Build a ``PlayerReadoutOut`` row with nonzero default values + * suitable for data-rendering assertions. The structural base + * (zero defaults) comes from the shared fixture; the test-local + * defaults here set the values that individual tests check + * (e.g. ``presence_pct: 95`` → ``getByText("95%")``). + */ +function buildRow( + overrides: Parameters[0] = {}, +) { + return buildPlayerReadoutRow({ name: "Test Player", - profession: "PROF(1)", elite_spec: "ELITE(27)", - subgroup: 1, - is_commander: false, - roles: ["DPS"], damage: { dps_total: 15000, dps_power: 10000, @@ -87,7 +90,7 @@ function buildRow(overrides: Partial = {}): PlayerReadoutOut { kill_participation: 10, }, ...overrides, - }; + }); } describe("CompareReadoutTable", () => { diff --git a/web/tests/fixtures/playerReadoutRow.ts b/web/tests/fixtures/playerReadoutRow.ts new file mode 100644 index 00000000..4f4d4c8e --- /dev/null +++ b/web/tests/fixtures/playerReadoutRow.ts @@ -0,0 +1,113 @@ +import type { PlayerReadoutOut } from "@/lib/api"; + +/** + * Shared test fixture builder for ``PlayerReadoutOut``. + * + * Returns a minimal row with all numeric fields set to 0 / null so + * individual tests can override only the fields they care about. + * The default uses ``PROF(1)`` (Guardian) / ``BASE`` (core spec), + * matching the wire format the API emits. + * + * Use the optional ``overrides`` param to customise the returned row + * for a specific test case — the spread ``...overrides`` happens + * **after** the defaults, so you can override any top-level field + * (damage, heal, boons, defense, etc.). + * + * ```ts + * // Default row — all zeros, Guardian core + * buildPlayerReadoutRow() + * + * // Customised row — Guardian Dragonhunter with 30k DPS + * buildPlayerReadoutRow({ + * name: "Top DPS", + * elite_spec: "ELITE(27)", + * damage: { dps_total: 30000, dps_power: 20000, dps_condi: 10000, ... }, + * }) + * ``` + */ +export function buildPlayerReadoutRow( + overrides: Partial = {}, +): PlayerReadoutOut { + return { + agent_id: 1, + account_name: ":test.1234", + name: "Test Character", + profession: "PROF(1)", // Guardian + elite_spec: "BASE", + subgroup: 1, + is_commander: false, + roles: ["DPS"], + damage: { + dps_total: 0, + dps_power: 0, + dps_condi: 0, + strips: 0, + cc_applied: 0, + down_contribution_dps: 0, + kills: 0, + cleave_targets: 0, + kill_participation: 0, + }, + heal: { + heal_total: 0, + hps: 0, + barrier_total: 0, + barrier_ps: 0, + cleanses: 0, + stun_breaks: 0, + }, + boons: { + boons_out_rate: 0, + boons_in_rate: 0, + stability_out: 0, + alacrity_out: 0, + resistance_out: 0, + aegis_out: 0, + superspeed_out: 0, + stealth_out: 0, + might_uptime: null, + fury_uptime: null, + quickness_uptime: null, + alacrity_uptime: null, + protection_uptime: null, + regeneration_uptime: null, + vigor_uptime: null, + aegis_uptime: null, + stability_uptime: null, + swiftness_uptime: null, + resistance_uptime: null, + resolution_uptime: null, + superspeed_uptime: null, + stealth_uptime: null, + other_boons_out: {}, + outgoing_might: null, + outgoing_fury: null, + outgoing_quickness: null, + outgoing_alacrity: null, + outgoing_protection: null, + outgoing_regeneration: null, + outgoing_vigor: null, + outgoing_aegis: null, + outgoing_stability: null, + outgoing_swiftness: null, + outgoing_resistance: null, + outgoing_resolution: null, + outgoing_superspeed: null, + outgoing_stealth: null, + }, + defense: { + damage_taken: 0, + cc_taken: 0, + deaths: 0, + time_downed_ms: 0, + dodges: 0, + blocks: 0, + interrupts: 0, + barrier_absorbed: 0, + presence_pct: null, + dist_to_commander: null, + kill_participation: 0, + }, + ...overrides, + }; +}