Skip to content

Commit 3f1a3d0

Browse files
SnoopLawgclaude
andcommitted
fix: gear optimizer was ignoring proposed gear (calc_stats hypothetical mode)
User caught it: the gear_target_optimizer's "162 SPD ceiling" for Venomage was wrong — she can reach ~263. Root cause: calc_stats(hero, artifacts, account) IGNORES the passed `artifacts` when the hero is in the computed-stats cache. It copies the mod's artifact_bonus column (the hero's CURRENTLY EQUIPPED gear), so the optimizer's oracle returned identical stats for every candidate build — the search was a no-op returning current stats. Fix: add calc_stats(..., hypothetical=True). It excludes the gear-DEPENDENT columns (artifact_bonus + the mastery_bonus Lore-of-Steel delta) and recomputes artifact / set / mastery / LoS bonuses from the PASSED gear on top of base_computed + gear-independent columns (affinity/arena/empower/blessing/ relic/faction/great-hall). gear_target_optimizer._stats_for now passes hypothetical=True. Default path unchanged — the sim and CB optimizers still get game-truth current stats (regression: same 2 pre-existing failures, no new ones). Verified: Venom max-SPD build now 263 (was 162); SPD=200 min reachable; recommender --optimize produces a real high-SPD damage build. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent a40dc9a commit 3f1a3d0

2 files changed

Lines changed: 43 additions & 17 deletions

File tree

tools/cb_optimizer.py

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,21 @@ def _apply_mastery_stat_bonuses(scaled, mastery_ids):
192192
scaled[stat_id] = scaled.get(stat_id, 0) + val * 100
193193

194194

195-
def calc_stats(hero, artifacts, account):
195+
def calc_stats(hero, artifacts, account, hypothetical=False):
196+
"""Compute a hero's total stats.
197+
198+
Default (hypothetical=False): if the hero is in the computed-stats cache,
199+
returns the GAME's current Total Stats (copies the mod's column bonuses,
200+
including artifact_bonus for the hero's CURRENTLY EQUIPPED gear). This is
201+
the game-truth path the sim uses — but it IGNORES the `artifacts` argument.
202+
203+
hypothetical=True (for the gear OPTIMIZER): actually evaluate the proposed
204+
`artifacts` list. Uses the game's `base_computed` + gear-INDEPENDENT columns
205+
(affinity/arena/empower/blessing/relic/faction/great-hall) and re-computes
206+
the artifact bonus, set bonuses, mastery stat-bonuses, and Lore-of-Steel
207+
from the passed gear. This is what lets the optimizer compare candidate
208+
builds — the default path returns the same (current) stats for any input.
209+
"""
196210
hero_id = hero.get("id", 0)
197211
base = hero.get("base_stats", {})
198212
element = hero.get("element", -1)
@@ -235,22 +249,29 @@ def calc_stats(hero, artifacts, account):
235249
# Demytha 749+1474+45+22+75+75=2440). Adding ALL columns from
236250
# the mod is game-truth — sim shouldn't recompute what the mod
237251
# already provides exactly. (DRY.)
238-
bonus_columns = (
239-
"blessing_bonus",
240-
"empower_bonus",
241-
"classic_arena_bonus",
242-
"great_hall_bonus",
243-
"relic_bonus",
244-
"affinity_bonus",
245-
"faction_guardians_bonus",
246-
"mastery_bonus",
247-
"artifact_bonus",
248-
)
252+
# Hypothetical (optimizer) mode: exclude the gear-DEPENDENT columns
253+
# (artifact_bonus carries the CURRENT gear; mastery_bonus carries the
254+
# Lore-of-Steel delta for the current gear). They get recomputed below
255+
# from the PASSED artifacts so candidate builds differ. The remaining
256+
# columns are gear-independent and stay game-truth.
257+
if hypothetical:
258+
bonus_columns = (
259+
"blessing_bonus", "empower_bonus", "classic_arena_bonus",
260+
"great_hall_bonus", "relic_bonus", "affinity_bonus",
261+
"faction_guardians_bonus",
262+
)
263+
else:
264+
bonus_columns = (
265+
"blessing_bonus", "empower_bonus", "classic_arena_bonus",
266+
"great_hall_bonus", "relic_bonus", "affinity_bonus",
267+
"faction_guardians_bonus", "mastery_bonus", "artifact_bonus",
268+
)
249269
# Skip the manual artifact computation + mastery loop below
250270
# when the mod has provided artifact_bonus/mastery_bonus —
251271
# those are GAME TRUTH and re-computing introduces drift.
252-
skip_manual_artifacts = "artifact_bonus" in computed
253-
skip_manual_masteries = "mastery_bonus" in computed
272+
# In hypothetical mode we WANT the manual recompute from passed gear.
273+
skip_manual_artifacts = ("artifact_bonus" in computed) and not hypothetical
274+
skip_manual_masteries = ("mastery_bonus" in computed) and not hypothetical
254275
stat_field_names = {HP: "HP", ATK: "ATK", DEF: "DEF", SPD: "SPD",
255276
CR: "CR", CD: "CD", RES: "RES", ACC: "ACC"}
256277
for col in bonus_columns:
@@ -305,7 +326,9 @@ def calc_stats(hero, artifacts, account):
305326
# summed the user's actual mastery bonuses (game truth, DRY).
306327
if hero.get("_project_full_masteries"):
307328
_apply_mastery_stat_bonuses(scaled, _ALL_STAT_BONUS_MASTERY_IDS)
308-
elif not (computed and "mastery_bonus" in computed):
329+
elif hypothetical or not (computed and "mastery_bonus" in computed):
330+
# Hypothetical mode excluded mastery_bonus column — re-apply the
331+
# user's actual stat-bonus masteries manually (gear-independent).
309332
_apply_mastery_stat_bonuses(scaled, hero.get("masteries", []) or [])
310333
project_glyph = hero.get("_project_max_glyphs", False)
311334
flat_b, pct_b, sets = {s:0 for s in range(1,9)}, {s:0 for s in range(1,9)}, {}
@@ -314,7 +337,7 @@ def calc_stats(hero, artifacts, account):
314337
# need detection for "has_lifesteal" etc. flags below, so we still
315338
# iterate `artifacts` to populate `sets` — just skip the flat/pct
316339
# accumulation.
317-
use_mod_artifact_bonus = bool(computed and "artifact_bonus" in computed)
340+
use_mod_artifact_bonus = bool(computed and "artifact_bonus" in computed) and not hypothetical
318341
for art in artifacts:
319342
s_id = art.get("set", 0)
320343
sets[s_id] = sets.get(s_id, 0) + 1

tools/gear_target_optimizer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ def score(self, stats, targets):
137137

138138
def _stats_for(self, hero, assignment):
139139
arts = [a for a in assignment.values() if a is not None]
140-
return calc_stats(hero, arts, self.account)
140+
# hypothetical=True: actually evaluate the PROPOSED gear. Without it
141+
# calc_stats returns the hero's CURRENT equipped stats for any input
142+
# (it copies the mod's artifact_bonus column), so the search is a no-op.
143+
return calc_stats(hero, arts, self.account, hypothetical=True)
141144

142145
def _proxy(self, art, targets):
143146
score = 0.0

0 commit comments

Comments
 (0)