Loadout Optimizer: stat-target planner — suggest armor to farm to meet stat goals (#11832) - #11940
Draft
Reuzehagel wants to merge 11 commits into
Draft
Loadout Optimizer: stat-target planner — suggest armor to farm to meet stat goals (#11832)#11940Reuzehagel wants to merge 11 commits into
Reuzehagel wants to merge 11 commits into
Conversation
…nager#11832) Feasibility spike for a planner that tells you what armor to farm to hit stat targets. Derives the Armor 3.0 archetype stat model empirically from owned items, enumerates the ~24 stat-distinct hypothetical pieces per slot, and benchmarks both a multiset enumerator (98k combos, ~150ms) and the unmodified LO worker (24^5 = 7.96M combos, ~10s) over that space.
The mid-2026 update added six archetypes (Siegebreaker, Skirmisher, Demolitionist, Colossus, Reaver, Powerhouse). Deriving the model only from owned items would silently miss archetypes the user has never had drop, so merge in archetype identities from the manifest plugs. Their primary/secondary stats exist only as localized description text, so the parser is en-only; production should generate this table in d2ai. Space grows to 48 blocks/slot; the multiset enumerator covers C(52,5)=2.6M compositions in ~3.5s.
Adds a collapsible menu section below Energy Options that plans over hypothetical tier-5 drops for the current stat minimums: verdict (reachable or short by N points), the archetype/tertiary recipe with counts and icons, and the auto stat mods it assumed. Recomputes as stat constraints change, deferred so slider drags stay responsive. The enumerator inner loop now hoists partial sums and skips ignored stats so a full 48-block plan runs in well under a second.
The Armor to Farm section now answers "what do I still need": it fixes the locked exotic to your best owned copy, keeps as many owned pieces as possible (searching farm-counts ascending), counts owned set pieces toward required set bonuses (farmed pieces cover the deficit, since all archetypes drop from all sources), and folds in subclass/mod stat contributions like the real worker does. A "Keep armor I already own" toggle switches between minimum-farm mode and pure ideal-drops theorycrafting.
"Combined with your:" now shows which roll each kept piece is, e.g. "Geomag Stabilizers (Grenadier / Class)", so duplicate copies are distinguishable. With the keep-owned toggle off, the verdict drops the word "new" since every piece is hypothetical.
Correctness: - Fix the "from set X" line rendering blank: passing count made i18next resolve scanner-generated empty plural keys; use a non-plural interpolation instead. - Fix block pruning dropping whole archetypes: every block has the same stat total so relevance sorts tied and kept insertion order; prune by per-archetype round-robin with ignored-tertiary dedupe, and bound the search with an exact ideal-drops pass over the FULL block list so pruning can never produce a false "unreachable". - Honor pinned items as required keeps; stop dropping pinned exotics. - Reserve the exotic slot (with a note) when the locked exotic has no available copy; select the exotic from its bucket so name-matched reissues work like item-filter does. - Mirror the worker's mod budget: auto mods use only general sockets left by locked general mods, and none when auto stat mods is off. - Show the kept pieces under an unreachable verdict so "Closest:" isn't followed by nothing; per-mode fine print with the real model tier instead of hardcoded "Tier 5". - Guard stat-less items out of candidate mapping (NaN poisoning). - Seed archetype identities from the manifest before item scanning so one odd roll can't poison an archetype. Performance: - Ideal-bound short-circuit caps the previously unbounded search on unreachable targets; upper-bound pruning skips hopeless keep-slot subsets; recursion uses per-depth scratch arrays (no allocation); multiset sums build lazily per farm-count. - Hoist per-item stat mapping out of the plan memo; use calculateAssumedMasterworkStats directly; memoize the manifest archetype scan per-defs. Cleanup: shared greedy-mod/tally/zero-stats helpers between planners, mapValues/sumBy/comparator reuse, test uses pruneBlocksForTargets instead of reimplementing it.
- Stat mods now model +5 minors and per-piece energy: major/minor general mod costs (from the auto-mod defs), locked general mods claiming sockets and energy, and per-piece budgets (10 minus bucket-specific mod costs; owned pieces use their assumed energy). Fast path skips the bookkeeping when no cost can exceed any budget, keeping the hot loop unchanged. - 'Any Exotic' plans each slot with the user's best owned exotic there and takes the best outcome, instead of quietly planning 5 legendaries. Pinned exotics decide the slot; no owned exotic gets an explanatory note. - Archetype primary/secondary stats now come from a generated armor-archetypes.json (d2ai-style; companion generator drafted in d2-additional-info). English description parsing remains only as a fallback for archetypes newer than the table. - Synthetic-fixture unit tests for planMinimumAcquisitions: pins, set-bonus deficits + graceful degrade, ideal-bound fallback, and mod/energy accounting. - Gated behind $featureFlags.loFarmingPlanner (beta/dev only).
The scanner can't extract keys from a computed ternary inside t(), so FarmingPlannerFinePrint/-Ideal never made it into the generated en.json and the runtime showed the key path. Split into two explicit t() calls.
…ility - Kept pieces (exotic included) render as real draggable item tiles with click-through to the item popup, instead of a text list. - When the Loadout Optimizer worker already found sets meeting the targets from owned armor, show 'already buildable' instead of a farm recipe. The worker models tuning mods, artifice sockets and every exotic copy, which the planner's model deliberately doesn't — so it's ground truth for owned armor and this removes a class of false 'you need N pieces' verdicts.
- planner.ts: the full planning orchestration (owned-candidate selection, exotic modes including Any Exotic) extracted into one pure function over structured-cloneable inputs. Pieces reference DimItems by id only; the component maps kept ids back to items for the tiles. - PlannerWorker.ts exposes it via comlink, same pattern as the LO worker. - usePlannerWorker hook: recompute on input change, keep the last plan while a new one runs, terminate a still-running stale computation rather than queueing behind it, clean up on unmount. - New unit tests for the orchestration: locked-exotic missing/best-copy, Any Exotic slot choice, no-exotic degrade, pinned-exotic override.
- Opt-in checkbox gates all planner work; the archetype model is only derived once enabled (was a full item+manifest scan on every LO mount) - Move armor-archetypes.json out of src/data/d2 (d2ai regenerates that dir) next to the planner that consumes it - Farmed-piece tuning strings; fine print no longer claims tuning is ignored - Share one ideal-bound computation across Any Exotic farmed candidates; hoist owned-candidate scoring out of the per-exotic loop - Dedup helpers (budget consumption, scratch refill), reuse existing exports (tuningStatBoost, totalFarmCount, zeroArmorStats, maxBy, Map.groupBy), flatten the verdict/render logic - Fix stale doc comments (tuning mods are modeled, not ignored) and drop reviewer-addressed comments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Changelog: Loadout Optimizer can now suggest which armor to farm to meet your stat goals.
What this is
When your stat targets aren't buildable from owned armor, a new opt-in "Armor to Farm" section in the Loadout Optimizer tells you the minimum set of new drops that would get you there. E.g. "keep these 2 pieces, farm 3× Powerhouse (Class tertiary), with at least 3 from [set]", including the stat mods and tuning to slot. When the targets are unreachable even with perfect drops, it says so, and shows how far off you are.
This implements the toggle discussed when #11832 was closed, and in the
#i-have-an-ideaDiscord thread.Combinatorics concern
The original issue was closed over combinatorial blowup, but the search space is small, because a future Armor 3.0 legendary's stat block is fully determined by (archetype, tertiary stat) at a given tier. That's 12 archetypes × 4 tertiaries = 48 stat-distinct pieces per slot, and the same 48 in every slot.
Since the pieces are interchangeable across slots, a candidate set is a choice of 5 from 48 with repetition, unordered: C(48+5-1, 5) = C(52,5) ≈ 2.6M, not 48⁵ ≈ 255M ordered ones. And because Destiny 2 is no longer receiving content updates, that archetype table is frozen, so it can't grow or rot.
Measured (Jest, single thread, full space, exact, no sampling):
How it works
hypothetical-items.ts: archetype stat model (from a local 12-entry table plus the user's own items), the multiset enumerator, and the minimum-acquisition search (keep owned pieces where possible, farm the rest; first feasible farm count wins).planner.ts: pure orchestration over structured-cloneable data (exotic modes, pins, set bonuses), run in a dedicated web worker (PlannerWorker.ts) via comlink, mirroring the LO worker setup.HypotheticalPlanner.tsx: the UI section. The opt-in checkbox gates all computation.loFarmingPlanner=!release. Everything is additive; the only touch outside the new folder is a 17-line hookup inLoadoutBuilder.tsx.Modeled / not modeled
Modeled: subclass and locked mod stats, auto +10/+5 general mods with real energy costs, locked general/bucket-specific mods claiming sockets and energy, set bonus requirements, farmed-piece tuning (+5 with a dump stat), owned-piece tuning variants, exotics (locked, Any Exotic, or farm-a-better-copy).
Not modeled: artifice, per-piece activity mod energy, exotic re-rolls. Archetype identities come from a hand-checked local table, with description parsing as a fallback for non-English manifests. Since the game is frozen, that's static data.
Tests
Disclosure
I used Claude Code for a large portion of this work. The idea, the (archetype × tertiary) -> 48-blocks insight, the UX decisions, and the verification are mine. I'm happy to walk through any part of the code and to make whatever changes you'd like.
Open questions for review