Balance Sweep Workflow (EvaluateBuild)¶
How an AI-assisted balance session runs. The moat is a faithful headless model of our combat math
(EternalDamageMath + UBuildEvaluatorLibrary, reference-tested against the live exec and the live GAS equip
path); AI proposes config diffs, a human approves — balance is never auto-committed.
The two surfaces¶
| Surface | Role | When |
|---|---|---|
| BuildSweep commandlet | Reproducible backbone: headless, CI-able, editor-independent. Regression baselines and sweeps run here. | Any time; no editor needed |
| MCP python (interactive) | Co-design surface: UBuildEvaluatorLibrary::EvaluateActorBuild / EvaluateBuild are BlueprintCallable → reachable via system_control execute_python over the stdio bridge. |
Editor-open sessions only |
There is also a human smoke surface: Cog window Eternal.Balance → "Evaluate PIE Player (Build Power)", and
the Build Lab editor tab (8th suite member) for composing/evaluating saved UBuildLoadoutAsset builds.
Build Lab ↔ preset bridge¶
Loadouts and character presets convert both ways without a PIE round-trip (name-paired BL_X ↔ Preset_X):
- Preset → Build Lab: "From Preset..." in the Build Lab list panel, the content-browser context menu on a
preset asset, or the Kit Composer's "Edit in Build Lab" jump — each opens the paired
BL_*, creating it from the preset's equipped items (state modules included) when missing. - Build Lab → preset: "Export to Preset..." in the details panel, or headless
Eternal.Builds.ExportPreset <Build> [PresetName] [-full]. Overwrite defaults to a gear-only merge (equipped items + slots replaced; inventory/stash/quests/glyphs/currency/display metadata survive);-full(or the menu toggle) replaces the whole payload. - Remnant state (
FRemnantItemState) rides onFBuildLoadoutEntry.StateModuleslosslessly and is authored in the composer's Remnant section (desire progress via the sealed clamp, echo/burden seating, awakened toggle). Eternal.Builds.CloneAtLevelskips hand-authored (Seed==0) slots — copied with only ItemLevel updated, named in the log; pass-forceto re-roll them too.- Slots the details panel draws come from
EItemizationGddSlot— the GDD slot list, not the equipment container's tag list: Right Hand, Left Hand, Head, Chest, Hands, Belt, Feet, Amulet, Ring 1, Ring 2, Flask, Edible.Flaskmaps to the container'sConsumableslot.TempEffectis a GDD slot with no equipment-container representation, so it is deliberately not drawn — the panel stops atEdible.
Running a sweep¶
& "C:\Program Files\Epic Games\UE_5.8\Engine\Binaries\Win64\UnrealEditor-Cmd.exe" `
"C:\Projects\Project-Eternal\ProjectEternal.uproject" -run=BuildSweep -unattended -nopause -nullrhi
- Evaluates all
UPresetCharacterAssetpayloads (labelledpreset:<Name>) plus anyUBuildLoadoutAssets you opt into via-buildsacross a context grid (default: areas 1/3/5 × Normal/Elite/Boss × Balanced). Presets are always swept; authored build assets are discovered only when-buildsasks for them, so they never silently enter an existing sweep. - Params:
-builds=<mode>— UBuildLoadoutAsset discovery (presets are always swept regardless):- absent → presets only (no build-asset discovery).
all→ everyUBuildLoadoutAssetin the project, via the asset registry./Game/Balance/Builds(any value starting with/) → only assets whose package path is under that Content prefix.Name1,Name2→ legacy name filter: build assets and presets whose asset name matches (back-compat).
-areas=1,3,5,-tiers=Normal,Elite,Boss,-out=<dir>.-strict→ promote any authoring-validation issue (pool-aware eligibility / affix budget / tier legality) to a non-zero exit code for CI. The report is still written first, so the offending rolls are inspectable.- Output:
Saved/BuildSweep/BuildSweep_<timestamp>.json(machine) +.md(human: per-context tables with an Archetype column — the build'sArchetypeTag,-/""when unset — TTK-in-band ✓/✗ against thePlayerBenchmarks.jsonTTK targets, plus archetype-parity DPS ratio). Each JSON row carries anarchetypefield (the build's own archetype; distinct fromcontext.archetype, which is the enemy archetype).
A balance session, step by step¶
- Human states the objective as a design statement — e.g. "Elite TTK at area 3 must land in the 12–20s band for all showcase presets", "no archetype's DPS may exceed 1.5× another's at equal iLvl".
- Claude sweeps: run the commandlet (baseline), read the report, optionally iterate interactively over MCP for targeted questions (single build, single stat marginal).
- Claude proposes a
Config/Balance/*.jsondiff (EnemyScaling / ThreatTiers / Archetypes / PlayerBenchmarks / Attributes) with a rationale tied to the report numbers, and re-runs the sweep against the edited config to show the predicted effect (the subsystem hot-reloads from disk; the commandlet always reads current files). - Human reviews: numbers are necessary, not sufficient — the evaluator values feel/positioning/commit windows at zero. Playtest the feel, then commit the config diff yourself. Never auto-commit balance.
Trust boundary (read before trusting a number)¶
- Reference-tested: per-hit damage math (
ProjectEternal.Unit.Combat.DamageMathruns the REAL exec through live ASCs), ailment DoT tick math (ProjectEternal.Unit.Combat.DoTMathruns the REALUExecCalc_DamageOverTimethrough live ASCs), items→stats composition (...Balance.BuildLoadoutComposerruns the real equip GE path + FSH-311 derivation layer). If these are green, the model matches the game. - Assumptions (documented in every
FBuildPower): steady-state light-attack chain (no combo/charge), expected-value crit and variance, status uptimes default 0, enemy damage typing default 100% physical. TTK is based onCombinedDPS(physical + ailment + cascade), flagged with the steady-state ramp assumption. - Modeled with documented assumptions: ailment DPS (Bleed/Ignite/Poison DoTs, reported as separate
Ailment DPS/Combined DPScolumns). The per-tick kernel is reference-tested against the live exec, but the independent-stacking DPS aggregation assumes canonical DoT tuning (hit-fraction, base duration, tick period, stack cap) that lives in theGE_*/GA_OnHit_*content, not on the sheet — flagged inConditionalNotes("AilmentDPS uses canonical DoT tuning"). Shock contributes no ailment DPS: it is a resistance-shred debuff, valued via the existingConditionalDamageVsShockedhit bonus, not a DoT. - Partially valued: status-cascade procs (Bleed/Ignite/Poison on-hit chance × ailment DPS) and
remnant echo/burden modifiers are now folded into
CombinedDPSvia the composer. The cascade valuation assumes canonical hit fractions and status uptime — the same assumption set as ailment DPS. Still unvalued (v1): non-cascade proc/granted-ability affixes, block/shield stats — checkUnvaluedNotes/ConditionalNotesbefore comparing builds that rely on these. - Known v1 gap: composition runs on an empty class baseline (no per-class base attributes yet), so
absolute EHP/TTD are meaningless until per-class baseline capture lands — DPS deltas and TTK against the
same baseline are still comparable. TODO tracked in
EvaluateBuild.plan.md.
Definition of done for combat features¶
Any new mechanic that changes hit damage, DoT tick damage, or stat aggregation MUST extend the pure functions
(EternalDamageMath, EternalDoTMath, BuildLoadoutComposition) and their reference specs — otherwise the sweep
silently lies (evaluator-drift risk, EvaluateBuild.plan.md / BuildAuthoring.plan.md §Risks).