Skip to content

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_XPreset_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 on FBuildLoadoutEntry.StateModules losslessly and is authored in the composer's Remnant section (desire progress via the sealed clamp, echo/burden seating, awakened toggle).
  • Eternal.Builds.CloneAtLevel skips hand-authored (Seed==0) slots — copied with only ItemLevel updated, named in the log; pass -force to 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. Flask maps to the container's Consumable slot. TempEffect is a GDD slot with no equipment-container representation, so it is deliberately not drawn — the panel stops at Edible.

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 UPresetCharacterAsset payloads (labelled preset:<Name>) plus any UBuildLoadoutAssets you opt into via -builds across a context grid (default: areas 1/3/5 × Normal/Elite/Boss × Balanced). Presets are always swept; authored build assets are discovered only when -builds asks 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 → every UBuildLoadoutAsset in 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's ArchetypeTag, -/"" when unset — TTK-in-band ✓/✗ against the PlayerBenchmarks.json TTK targets, plus archetype-parity DPS ratio). Each JSON row carries an archetype field (the build's own archetype; distinct from context.archetype, which is the enemy archetype).

A balance session, step by step

  1. 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".
  2. Claude sweeps: run the commandlet (baseline), read the report, optionally iterate interactively over MCP for targeted questions (single build, single stat marginal).
  3. Claude proposes a Config/Balance/*.json diff (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).
  4. 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.DamageMath runs the REAL exec through live ASCs), ailment DoT tick math (ProjectEternal.Unit.Combat.DoTMath runs the REAL UExecCalc_DamageOverTime through live ASCs), items→stats composition (...Balance.BuildLoadoutComposer runs 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 on CombinedDPS (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 DPS columns). 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 the GE_*/GA_OnHit_* content, not on the sheet — flagged in ConditionalNotes ("AilmentDPS uses canonical DoT tuning"). Shock contributes no ailment DPS: it is a resistance-shred debuff, valued via the existing ConditionalDamageVsShocked hit 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 CombinedDPS via 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 — check UnvaluedNotes/ConditionalNotes before 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).