Skip to content
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
16 changes: 14 additions & 2 deletions apps/api/src/gw2analytics_api/route_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,21 @@


def format_profession(profession: Profession | int) -> str:
"""Map a profession to its wire-format label."""
"""Map a profession to its wire-format label.

Returns the profession's display name (e.g. ``"Guardian"``,
``"Warrior"``) for known core professions, ``"UNKNOWN"`` for
value 0, and ``"PROF(N)"`` for unknown profession IDs (the
fallback for future professions not yet in the ``Profession``
enum).
"""
v = profession.value if isinstance(profession, Profession) else int(profession)
return "UNKNOWN" if v == 0 else f"PROF({v})"
if v == 0:
return "UNKNOWN"
try:
return Profession(v).name.title()
except (ValueError, KeyError):
return f"PROF({v})"


def format_elite_spec(elite: EliteSpec | int) -> str:
Expand Down
50 changes: 25 additions & 25 deletions apps/api/tests/routes/test_fights_readout.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ def test_readout_200_happy_path_with_player(client: TestClient) -> None:
assert a_row["subgroup"] == 0
assert a_row["name"] == f"W {suffix}"
assert a_row["account_name"] == f"synth.{a}"
assert a_row["profession"] != "UNKNOWN" # format_profession(Warrior=2) -> "PROF(2)"
assert a_row["profession"] != "UNKNOWN" # format_profession(Warrior=2) -> "Warrior"
assert a_row["elite_spec"] != "UNKNOWN" # format_elite_spec(Berserker=18) -> "Berserker"
assert a_row["is_commander"] is False
assert a_row["roles"] == ["DPS"]
Expand Down Expand Up @@ -269,7 +269,7 @@ def test_readout_aggregator_stun_break_events_wired() -> None:
name=f"W {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=True,
Expand Down Expand Up @@ -330,7 +330,7 @@ def test_readout_aggregator_account_name_none_passthrough() -> None:
name=f"W {a}",
subgroup=0,
account_name=None, # the canonical absent-account path
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -350,7 +350,7 @@ def test_readout_aggregator_account_name_none_passthrough() -> None:
assert a_readout.account_name is None
# The OTHER identity columns still hydrate correctly.
assert a_readout.name == f"W {a}"
assert a_readout.profession == "PROF(2)"
assert a_readout.profession == "Warrior"
assert a_readout.elite_spec == "Berserker"
assert a_readout.is_commander is False
# The pre-followup coerce of None to "" would have triggered
Expand Down Expand Up @@ -411,7 +411,7 @@ def test_readout_boon_uptimes_and_presence_pct() -> None:
name=f"BoonRecv {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -421,7 +421,7 @@ def test_readout_boon_uptimes_and_presence_pct() -> None:
name=f"BoonSrc {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -496,7 +496,7 @@ def test_readout_boon_uptimes_none_for_no_account() -> None:
name=f"NoSynth {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -573,7 +573,7 @@ def test_readout_phase6_v2_barrier_and_condi_split_live() -> None:
name=f"Phase6v2 {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -583,7 +583,7 @@ def test_readout_phase6_v2_barrier_and_condi_split_live() -> None:
name=f"Phase6v2 {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -691,7 +691,7 @@ def test_readout_down_contribution_dps_wired() -> None:
name=f"Killer {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -701,7 +701,7 @@ def test_readout_down_contribution_dps_wired() -> None:
name=f"Downed {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -768,7 +768,7 @@ def test_readout_cleave_targets() -> None:
name=f"Cleaver {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -778,7 +778,7 @@ def test_readout_cleave_targets() -> None:
name=f"Target1 {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand All @@ -788,7 +788,7 @@ def test_readout_cleave_targets() -> None:
name=f"Target2 {c}",
subgroup=0,
account_name=f"synth.{c}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -825,7 +825,7 @@ def test_readout_dist_to_commander_no_commander() -> None:
name=f"NoCmd {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -901,7 +901,7 @@ def test_readout_dual_role_heal_support() -> None:
name=f"HealSup {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Firebrand",
is_player=True,
is_commander=False,
Expand All @@ -911,7 +911,7 @@ def test_readout_dual_role_heal_support() -> None:
name=f"Minor {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -977,7 +977,7 @@ def test_readout_cleanser_role() -> None:
name=f"Cleanser {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Firebrand",
is_player=True,
is_commander=False,
Expand All @@ -987,7 +987,7 @@ def test_readout_cleanser_role() -> None:
name=f"Target {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -1037,7 +1037,7 @@ def test_readout_cc_role() -> None:
name=f"CCer {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -1047,7 +1047,7 @@ def test_readout_cc_role() -> None:
name=f"Target {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -1097,7 +1097,7 @@ def test_readout_strip_role() -> None:
name=f"Stripper {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -1107,7 +1107,7 @@ def test_readout_strip_role() -> None:
name=f"Target {b}",
subgroup=0,
account_name=f"synth.{b}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=False,
Expand Down Expand Up @@ -1183,7 +1183,7 @@ def test_readout_dist_to_commander_with_commander() -> None:
name=f"Player {a}",
subgroup=0,
account_name=f"synth.{a}",
profession="PROF(2)",
profession="Warrior",
elite_spec="Berserker",
is_player=True,
is_commander=False,
Expand All @@ -1193,7 +1193,7 @@ def test_readout_dist_to_commander_with_commander() -> None:
name=f"Cmd {c}",
subgroup=0,
account_name=f"synth.{c}",
profession="PROF(1)",
profession="Guardian",
elite_spec="Dragonhunter",
is_player=True,
is_commander=True,
Expand Down
10 changes: 5 additions & 5 deletions apps/api/tests/test_players.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ def test_players_filter_with_pagination() -> None:
test, so the "many Mesmer accounts from prior runs"
pollution is gone. We now seed 5 Mesmers deterministically
to exercise the cross-page consistency contract:
1. All rows on every page have ``profession == "PROF(7)"`` (Mesmer)
1. All rows on every page have ``profession == "Mesmer"`` (Mesmer)
-- the filter is applied to every page, not just page 1.
2. Page 1 + page 2 do not overlap -- the offset/limit
are consistent on the filtered set.
Expand All @@ -300,7 +300,7 @@ def test_players_filter_with_pagination() -> None:
# applied -- if the filter were broken, page 1 might
# include a Warrior or Necromancer).
for row in rows1:
assert row["profession"] == "PROF(7)", f"page 1 row should be Mesmer (PROF(7)), got {row}"
assert row["profession"] == "Mesmer", f"page 1 row should be Mesmer (PROF(7)), got {row}"
resp2 = client.get(
"/api/v1/players",
params={"profession": "MESMER", "limit": 2, "offset": 2},
Expand All @@ -309,7 +309,7 @@ def test_players_filter_with_pagination() -> None:
rows2 = resp2.json()
# Every row on page 2 is a Mesmer too.
for row in rows2:
assert row["profession"] == "PROF(7)", f"page 2 row should be Mesmer (PROF(7)), got {row}"
assert row["profession"] == "Mesmer", f"page 2 row should be Mesmer (PROF(7)), got {row}"
page1_accounts = {r["account_name"] for r in rows1}
page2_accounts = {r["account_name"] for r in rows2}
# Page 1 + page 2 must not overlap (the offset/limit
Expand Down Expand Up @@ -351,9 +351,9 @@ def test_players_filter_does_not_affect_other_responses() -> None:
# modal profession, not the Warrior filter value).
assert profile["fights_attended"] >= 1
# The ``profession`` field is the wire-format string label
# (``"PROF(7)"`` for Mesmer -- see :func:`_profession_label`
# (``"Mesmer"`` for Mesmer -- see :func:`_profession_label`
# in routes/players.py for the exact wire shape).
assert profile["profession"] == "PROF(7)"
assert profile["profession"] == "Mesmer"
# v0.10.3 plan 083: the per-fight breakdown now carries the
# role detection (detected_role / detected_tags). The Mesmer
# seeded above has damage-only events (the back-and-forth
Expand Down
9 changes: 8 additions & 1 deletion libs/gw2_analytics/src/gw2_analytics/role_detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,14 @@
# forces DPS as primary; with the override, HEAL takes
# priority when the player is on a healer spec and has
# meaningful healing output.
_R_HEAL_OVERRIDE: Final[float] = 0.15
#
# Raised from 0.15 to 0.30 in v0.16.1-api follow-up: the
# previous threshold was too permissive, tagging low-healing
# player (e.g. a Scourge whose weighted effort was only ~15 %
# healing) as HEAL despite minimal actual heal output. The
# new threshold requires at least 30 % of the weighted effort
# to come from healing before the override kicks in.
_R_HEAL_OVERRIDE: Final[float] = 0.30


# ---------------------------------------------------------------------------
Expand Down
Loading
Loading