Skip to content

Commit 384c43c

Browse files
SnoopLawgclaude
andcommitted
fix: model tiered sub-set artifact bonuses (the ACC discrepancy root cause)
Chasing the ~20 ACC validation gap led to the real cause: 13 newer sets (id 35/36/47/48/58-66, e.g. IncreaseAccuracyAndSpeedWithSkillEffects, BlockReflectDebuffAndHpAndDef) have pieces=0 and store their bonus in a piece-count LADDER (`sub_sets`), where each tier's StatBonuses is the TOTAL at that count (not multiplied, not cumulative). The hero gets the highest tier whose `pieces` <= owned count. e.g. set 36: 1pc=+20 ACC, 2pc=+20 ACC & +10% SPD, 3pc=+40 ACC … up to 9pc. IL2CPP confirms via ArtifactSubSetInfo (ArtifactCount + List<StatBonus> StatBonuses). Our artifact_sets.json was stale (no sub_sets); the mod already emits them. Refreshed it, added SUB_SET_BONUSES (id -> sorted [(pieces, {stat: value})]) in gear_constants, and applied the highest-tier-<=-count lookup in calc_stats. Validation jumped: Venom/Geo/Arbiter ACC now EXACT (were -20/-20/-40); Ninja ACC improved 40 (one tier). Remaining: smaller DEF outliers (Cardiel/Ninja), likely Divine on ACC/DEF or another tier. Default calc_stats (sim) path unaffected — sub-set bonuses route through set_pct/set_flat which the mod-column path ignores; same 2 pre-existing regression failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1b0b0ad commit 384c43c

3 files changed

Lines changed: 57 additions & 5 deletions

File tree

data/static/artifact_sets.json

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
{
22
"_meta": {
3-
"fetched_at": "2026-05-01T18:14:45.259518+00:00",
4-
"scene": "Village",
5-
"mod_version": "2.0-bepinex",
6-
"unity": "6000.0.60f1"
3+
"generated_by": "/artifact-sets-truth (sub_sets 2026-06-23)",
4+
"note": "tiered sub-set bonuses included"
75
},
86
"sets": [
97
{

tools/cb_optimizer.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@
6464
# Set bonuses + names — sourced from data/static/artifact_sets.json via
6565
# gear_constants. SET_BONUSES is value-correct (live-game derived); SET_NAMES
6666
# is display-friendly. See gear_constants.py for details.
67-
from gear_constants import SET_BONUSES, SET_NAMES # noqa: E402
67+
from gear_constants import SET_BONUSES, SET_NAMES, SUB_SET_BONUSES # noqa: E402
6868

6969
# Import canonical slot/set names from central module
7070
try:
@@ -412,6 +412,26 @@ def calc_stats(hero, artifacts, account, hypothetical=False):
412412
flat_b[stat] += val * num_complete
413413
else:
414414
set_pct[stat] += val * num_complete
415+
# Tiered sub-set sets (id 35/36/47/48/58-66): pick the highest tier
416+
# whose `pieces` <= owned count; its StatBonuses is the TOTAL (not
417+
# multiplied, not cumulative). e.g. set 36 with 1 piece = +20 ACC.
418+
ladder = SUB_SET_BONUSES.get(s_id)
419+
if ladder:
420+
tier = None
421+
for pieces, sm in ladder:
422+
if cnt >= pieces:
423+
tier = sm
424+
else:
425+
break
426+
if tier:
427+
active_sets.add(s_id)
428+
for stat, val in tier.items():
429+
if stat in (ACC, RES):
430+
set_flat[stat] += val
431+
if not use_mod_artifact_bonus:
432+
flat_b[stat] += val
433+
else:
434+
set_pct[stat] += val
415435

416436
# Lore of Steel (mastery 500343, Support tree): +15% to all set
417437
# bonuses.

tools/gear_constants.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,9 @@ def to_artifact_stat_kind_id(stat_id, is_flat):
188188
SET_PIECES: dict[int, int] = {} # id → 2 / 4 (or 0 for special)
189189
SET_KIND_IDS: dict[int, str] = SET_INTERNAL_NAMES # legacy alias for existing consumers
190190
SET_BONUSES: dict[int, tuple[int, dict[int, float]]] = {}
191+
# Tiered sub-set ladders: id → [(pieces, {stat_id: value}), …] ascending.
192+
# Apply the highest tier whose `pieces` <= the hero's owned count.
193+
SUB_SET_BONUSES: dict[int, list] = {}
191194

192195
# Prefer the live game-truth file (data/static/artifact_rules.json) which is
193196
# generated by `tools/refresh_artifact_truth.py`. Falls back to the older
@@ -246,6 +249,37 @@ def to_artifact_stat_kind_id(stat_id, is_flat):
246249
_stat_map[_sid] = _v # game-truth value (overwrites/augments)
247250
if _stat_map:
248251
SET_BONUSES[_id] = (_pieces, _stat_map)
252+
253+
# SUB-SET (tiered) SETS — 13 newer sets (id 35/36/47/48/58-66) have
254+
# `pieces=0` and store their bonus in `sub_sets`: a piece-count ladder
255+
# where each tier's StatBonuses is the TOTAL at that count (NOT
256+
# cumulative, NOT multiplied). e.g. set 36: 1pc=+20 ACC, 2pc=+20 ACC &
257+
# +10% SPD, 3pc=+40 ACC & +10% SPD … up to 9pc. The hero gets the
258+
# highest tier whose `pieces` <= their owned count. Build a sorted
259+
# ladder per set: SUB_SET_BONUSES[id] = [(pieces, {stat_id: value}), …].
260+
# Values: absolute → raw (flat); percent → ×100 (matches SET_BONUSES).
261+
for _row in _rows:
262+
_id = _row.get("id")
263+
_subs = _row.get("sub_sets") or []
264+
if _id is None or not _subs:
265+
continue
266+
_ladder = []
267+
for _ss in _subs:
268+
_cnt = _ss.get("pieces")
269+
_sm = {}
270+
for _b in _ss.get("stat_bonuses") or []:
271+
_sid = _name_to_stat.get(_b.get("stat"))
272+
if _sid is None:
273+
continue
274+
_v = _b.get("value", 0)
275+
if not _b.get("absolute"):
276+
_v = round(_v * 100, 2)
277+
_sm[_sid] = _v
278+
if _cnt and _sm:
279+
_ladder.append((_cnt, _sm))
280+
if _ladder:
281+
_ladder.sort(key=lambda t: t[0])
282+
SUB_SET_BONUSES[_id] = _ladder
249283
except Exception:
250284
pass # keep the singular-stat table if the plural file is unavailable
251285

0 commit comments

Comments
 (0)