From c19921bdffb9122a31c5ff0cacfad7fd3b64a04f Mon Sep 17 00:00:00 2001 From: MarTrepodi Date: Thu, 22 Oct 2026 12:00:29 -0400 Subject: [PATCH] fix(statcalc): build units that start above one star GameDataBuilder only promoted a unit's rarity-1 definition to its base entry, so a unit with no ONE_STAR row was silently dropped from unitData even though its growth tables were collected. ADELPHIRANGER starts at 2 stars and was missing as a result. Use each unit's lowest-rarity definition as its base instead. The fields the builder reads (skills, gear tiers, relics, categories, crew) are identical across rarities, so this is equivalent for every unit that does have a 1-star row. Unknown rarity enums (normalised to 0) are still ignored. Co-Authored-By: Claude Opus 5.5 --- .../StatCalc/data_builder/_builder_base.py | 12 ++++++++---- tests/unit/test_builder_base.py | 11 +++++++++++ 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/swgoh_comlink/StatCalc/data_builder/_builder_base.py b/src/swgoh_comlink/StatCalc/data_builder/_builder_base.py index b9a0403..fdb9df3 100644 --- a/src/swgoh_comlink/StatCalc/data_builder/_builder_base.py +++ b/src/swgoh_comlink/StatCalc/data_builder/_builder_base.py @@ -238,16 +238,19 @@ def _build_unit_data( """Build per-unit metadata for characters and ships. The Comlink ``units`` field is an array of rarity groups, each - containing a ``unitDef`` array. Rarity-1 entries supply base - definitions (gear tiers, skills, etc.); all rarities contribute + containing a ``unitDef`` array. Each unit's lowest-rarity entry + supplies its base definition (gear tiers, skills, etc.) — usually + rarity 1, but some units (e.g. ``ADELPHIRANGER``) start at 2★ and + have no rarity-1 entry. All rarities contribute ``statProgressionId`` for growth modifier tables. """ base_units: dict[str, dict[str, Any]] = {} + base_rarity: dict[str, int] = {} stat_prog_map: dict[str, dict[str, str]] = {} total_units = 0 - # Filter for only obtainable units at rarity 1 + # Filter for only obtainable units base_unit_list = [unit for unit in raw_units if unit.get("obtainable", False) and unit.get("obtainableTime") == "0"] for unit in base_unit_list: @@ -264,7 +267,8 @@ def _build_unit_data( if prog_id: stat_prog_map.setdefault(base_id, {})[str(rarity)] = prog_id - if rarity == 1: + if 0 < rarity < base_rarity.get(base_id, 8): + base_rarity[base_id] = rarity base_units[base_id] = unit data: dict[str, dict[str, Any]] = {} diff --git a/tests/unit/test_builder_base.py b/tests/unit/test_builder_base.py index b4ee0e9..445d30c 100644 --- a/tests/unit/test_builder_base.py +++ b/tests/unit/test_builder_base.py @@ -507,6 +507,17 @@ def test_growth_modifiers_across_rarities(make_unit: Callable[..., dict[str, Any assert result["unitData"]["UNIT1"]["growthModifiers"] == {"1": {"2": 10}, "7": {"2": 70}} +@pytest.mark.parametrize("make_unit", [_char_unit, _ship_unit], ids=["character", "ship"]) +@pytest.mark.parametrize("rarities", [[2, 7], [7, 2], ["SEVEN_STAR", "TWO_STAR"]], ids=["asc", "desc", "enum"]) +def test_unit_without_one_star_row_uses_lowest_rarity_as_base(make_unit: Callable[..., dict[str, Any]], rarities): + """Units that start above 1★ (e.g. ADELPHIRANGER at 2★) have no rarity-1 row but must still be built.""" + units = [make_unit("STARTS_AT_TWO", rarity=r, statProgressionId=f"stattable_{r}") for r in rarities] + + result = _build_unit_data(units, {}, {}) + + assert list(result) == ["STARTS_AT_TWO"] + + @pytest.mark.parametrize( "excluded", [