| name | moe-progress |
| description | Use when editing the 14th_ua MoE Calculator's two centre-screen TRANSIENT battle bars — the Progress Bar (Moving Average variant) and the Damage Efficiency bar — their radio-alternative gating, MoEProgress.js/.css/.html, MoEProgressVertical.css, MoEEfficiency.js/.css/.html, MoEEfficiencyVertical.css, the shared MoEBarTransient.js fade/hold/peek/size/drag/orientation state machine, bridge/bar_window.py's BarHost/_BarView/_BarWindow, progress_view.py, efficiency_view.py, ProgressVM/EfficiencyVM in view_models.py, the vertical vs horizontal orientation variants, the Default/Large size modes, the Ctrl+drag absolute-placement reposition, and the gen_bar_tuner*.ps1 CSS generators + their check_*.js gates. For the in-battle corner overlay see moe-battle. For the hangar bar see moe-garage. For the MSA controls themselves (Progress Bar / Transitions / Layout groups) see moe-settings. For build/hot-reload of this window (no hot-reload — full relaunch) see moe-build-release. |
14th_ua MoE Calculator — centre-screen transient bars (feature map)
Boundary (read this first, it is the #1 confusion)
"Progress Bar" in the MSA/UI names the master checkbox progress_bar_enabled that gates
both centre bars. The variant radio progress_bar_variant picks which one opens:
0 = Efficiency (default), 1 = Moving Average. At most one is ever open —
battle_bridge._window_gates() (battle_bridge.py:145-157) is the single place deciding all
three battle windows. Neither centre bar is the corner in-battle overlay (MoEBattle.* /
battle_view.py, gated by BATTLE_KEY) — that is moe-battle. Three coexisting widgets,
not a rename.
File inventory
| File | Scope | Role |
|---|
MoEProgressView.html, MoEProgress.css, MoEProgressVertical.css, MoEProgress.js | Progress-only | Moving Average bar front-end |
bridge/progress_view.py, res_map MoEProgressView.json (itemID MoEProgressView) | Progress-only | window instantiation + registration |
MoEEfficiencyView.html, MoEEfficiency.css, MoEEfficiencyVertical.css, MoEEfficiency.js | Efficiency-only | Damage Efficiency bar front-end (mirror) |
bridge/efficiency_view.py, res_map MoEEfficiencyView.json (itemID MoEEfficiencyView) | Efficiency-only | window instantiation + registration |
MoEBarTransient.js | SHARED | fade/hold/peek/size/drag/orientation state machine for BOTH bars |
MoEBattle.ttf, checker.png | SHARED | glyph font + halftone backdrop asset, reused from the corner overlay |
bridge/bar_window.py (BarHost/_BarView/_BarWindow) | SHARED | ONE hosting implementation, instantiated once per bar |
bridge/battle_bridge.py, bridge/view_models.py, bridge/mod_settings.py | SHARED | lifecycle, VM classes, settings getters |
adapter/battle_adapter.py, adapter/battle_input.py | SHARED | efficiency read, Ctrl/Alt input sampling |
domain/positioning.py, domain/battle_builder.py, domain/constants.py | SHARED | placement math, CD/EWMA/axis math, anchor constants |
Data flow
battle_bridge._on_efficiency_updated / _on_summary_feedback → _schedule_refresh() →
refresh() (battle_bridge.py:634) → battle_adapter.build_battle_snapshot() →
build_battle_model(snap) → push_progress(bar.viewModel, snap, model) /
push_efficiency(eff.viewModel, snap, model) (battle_bridge.py:695-850+). push_progress
computes marks_from_percentile, mark_axis, ewma_project_raw, battles_to_axis_hi,
progress_axis_lo, writing inside rvm.transaction(). push_efficiency has no
has-baseline gate, unlike push_progress — its axis is the tank's requirement table alone,
so it doesn't need a career baseline to draw.
VM slot table — ProgressVM (bridge/view_models.py:200-373), properties=16, commands=0
| # | field | type | notes |
|---|
| 0 | visible | bool | |
| 1 | marks | Number | held marks 0..3 |
| 2 | axisLo | Real | display floor (progress_axis_lo), not the mark's own requirement |
| 3 | axisHi | Real | requirement chased (the 100 stop at 3 marks) |
| 4 | preAvg | Number | career moving-avg combined damage |
| 5 | projAvg | Real | this battle folded in (EWMA) |
| 6 | hasData | bool | mark axis usable |
| 7 | altHeld | bool | Alt down → pull bar up (additive trigger) |
| 8 | barSize | Number | 0 default / 1 Large |
| 9 | transEvents | bool | animate battle-event enter/exit |
| 10 | transManual | bool | animate Alt-peek enter/exit |
| 11 | showEvents | bool | may a battle event raise the bar at all |
| 12 | holdMs | Number | hold duration, default 5000 |
| 13 | ctrlHeld | bool | Ctrl down → hold up for reposition |
| 14 | etaBattles | Number | battles to axisHi, -1 = no data |
| 15 | vertical | bool | draw vertical composition |
EfficiencyVM (bridge/view_models.py:375-532), properties=18, commands=0 — its OWN model,
deliberately NOT sharing ProgressVM's two-end mark axis (it plots ONE battle's combined damage
against ALL FOUR requirement stops):
| # | field | type | notes |
|---|
| 0 | visible | bool | |
| 1 | damage | Number | this battle's combined damage |
| 2 | barX | Real | damage on the axis, 0..100% (domain.efficiency_bar_x) |
| 3 | band | Number | 0..4 highest requirement passed (>=), selects .mp-b-{w,g,t,v,au} |
| 4-7 | r65/r85/r95/r100 | Real ×4 | the four requirement stops |
| 8 | hasData | bool | all four requirements present + ascending |
| 9 | altHeld | bool | |
| 10 | battleEpoch | Number | monotonic per-battle counter, resets the JS damage-delta latch |
| 11 | barSize | Number | |
| 12-14 | transEvents/transManual/showEvents | bool ×3 | same wire meaning as ProgressVM's |
| 15 | holdMs | Number | |
| 16 | ctrlHeld | bool | |
| 17 | vertical | bool | |
No damageDelta slot on Efficiency — the "last increment" caption is derived and latched in
MoEEfficiency.js off successive damage pushes; adding one would have renumbered every
property after it, which is exactly the hand-maintained-index hazard the module header warns
about (battleEpoch was therefore APPENDED, not filed next to damage).
Read-polarity rule (both VMs): vertical/ctrlHeld read === true (fail-soft to horizontal /
not-held — the shipped composition IS horizontal and NOT pinned up); transEvents/
transManual/showEvents read !== false (fail-soft to animated / shown). axisLo/axisHi/
projAvg (Progress) and barX/r65..r100 (Efficiency) must be _addRealProperty/
_setReal — projAvg moves ~a couple damage points per battle (k≈0.02), and an int truncation
quantises the JS change-detect signal away entirely (this already shipped once as "the bar
never showed").
DOM + CSS
MoEProgressView.html's static markup is ONLY <div id="moe-bar-box"> — a sizing shim, sized
to BOX_W_REM + 2*PAD_REM × BOX_H_REM + 2*PAD_REM = 380×92rem (MoEProgress.js:147-149),
mirrored exactly in MoEProgress.css. #moe-bar-root is JS-created in ensureRoot()
(MoEProgress.js:371) and appended to document.body — the box must exist at the FIRST layout
pass or the engine's size calculation has nothing to measure and clobbers the surface (see
Placement below).
Horizontal MARKUP (MoEProgress.js:307-329): backdrop, track (fill + 4 ticks
mp-end.mp-left/mp-right, mp-pre, mp-proj), 3 captions mp-capP (projected avg) /
mp-capC (current combined damage + delta) / mp-capR (requirement + eta/battles glyph).
Order-dependent: capV() is a first-match querySelector for .mp-v, so the mark-pair-first
ordering inside capR is load-bearing (check_progress_js.js's battles-pair-comes-first
mutation exists exactly to catch a swap here).
Vertical V_MARKUP (MoEProgress.js:355-369): numeral-before-icon, capR's two groups
reordered. Class prefix .mpv-*, gated by body.mpv. Both stylesheets are <link>ed
unconditionally (MoEProgressView.html:22-23) — orientation is a JS mount-time branch on
the vertical VM field, not a second res_map layout (a new itemID would cost every user a
one-time client restart). The two sheets are namespace-DISJOINT (.mp-* vs .mpv-*); only
body, #moe-bar-box, #moe-bar-root are shared subjects, and body.mpv at (1,1,1)
out-ranks the horizontal .mp-lg #moe-bar-root at (1,1,0) — the vertical sheet is linked
SECOND so source order also backs it up.
Size: barSize → MoEBarTransient.applySize() toggles document.body.classList.toggle ("mp-lg", large) and rewrites the root font-size, SIZE_F=1.25. The .mp-lg block
(MoEProgress.css:761-806) re-declares ONLY cross-axis (x) lengths at SIZE_XF=4/3; y-lengths
ride the root font alone. Double-applying either is the classic bug. .mp-s1 is a SEPARATE
orthogonal body class for interface-scale legibility (px > 0 && px < 1.5); the two combine as
the compound selector .mp-s1.mp-lg (never descendant) — MoEProgress.css:866,868.
Generated-CSS pipeline (exact commands)
pwsh tools\dev\gen_bar_tuner.ps1 -EmitCss [-CssOut TASKS/refs/MoEProgress.css]
pwsh tools\dev\gen_bar_tuner_vertical.ps1 -EmitCss [-CssOut TASKS/refs/MoEProgressVertical.css]
The emit is written to gitignored TASKS/refs/. Shipped MoEProgress.css = emit + 2
hand-additions (marked ADDITION n OF 2 at MoEProgress.css:11,26 — the @font-face and the
#moe-bar-box rule). Shipped MoEProgressVertical.css = emit + 6 hand-edits (marked
HAND-EDIT n/6, header at MoEProgressVertical.css:22-24). Efficiency's vertical sheet
(MoEEfficiencyVertical.css) = emit + 5 hand-edits. Never paste a fresh emit over a shipped
sheet — see the memory emitcss-is-not-the-whole-shipped-stylesheet.
The Efficiency tuners have no separate PowerShell generator — eff_bar_tuner.html /
eff_bar_tuner_vertical.html are self-contained HTML tuners; the Node checkers evaluate their
cssOut() headlessly instead of shelling a -EmitCss switch.
Gates, all node tools\dev\<script>.js, exit 1 on failure, most support --probe-all /
--list-mutations / --mutate=<key>:
check_progress_js.js — Progress DOM/timing/size/drag-gate assertions against a large,
named MUTATIONS table (each key breaks exactly one real behaviour this bar shipped a bug
around — read the table itself in the file rather than trust a stale count anywhere else).
check_efficiency_js.js — the Efficiency mirror.
check_bar_orientation_js.js — vertical DOM/surface/run-identity for BOTH bars.
check_bar_vertical.js — pins cssOut() byte-for-byte against the checked-in
TASKS/refs/MoEProgressVertical.css (re-run -EmitCss after any generator edit or this
fails on a stale artifact — it is gitignored and absent in a fresh clone).
check_vertical_css_handedits.js — shipped vertical CSS == fresh emit + exactly the
documented hand-edits, for both bars.
lib/gf_check_shim.js is the shared harness (assertion helpers eq/ok, a minimal DOM, a
virtual clock, the jsConst/jsFactor scrapers) — not a checker itself.
Note the doc drift: tools/dev/README.md:652 says "the shipped vertical CSS is emit + exactly
5 hand-edits" for BOTH bars; the CSS header and check_vertical_css_handedits.js correctly
encode 6 for Progress / 5 for Efficiency. Trust the checker and the CSS header, not
that one README line.
Placement / window
bridge/progress_view.py:25-28 builds BarHost("MoEProgressView", ProgressVM, PROGRESS_ANCHOR_Y_FRAC, PROGRESS_ANCHOR_X_OFFSET, PROGRESS_ANCHOR_Y_SHIFT, PROGRESS_ANCHOR_Y_SHIFT_LARGE, PROGRESS_MM_TRACK_X, PROGRESS_MM_TRACK_X_LARGE, PROGRESS_MM_GAP_BOTTOM, "[moe-bar]"); efficiency_view.py is the byte-identical shape off its
own EFFICIENCY_* constants. BarHost hosts a WindowLayer.WINDOW content-sized window — same
flags/layer reasoning as the corner overlay (moe-battle's hosting-model section): NOT
WINDOW_FULLSCREEN, because a full-screen surface steals the whole-screen mouse hit-test
whenever the cursor is raised.
_resolve() (bar_window.py:277-360) has two live top-level branches now, alignment reduced
(v23) to PROGRESS_ALIGN_FIXED (0, default) / PROGRESS_ALIGN_FREE (1) — see moe-settings's
"The Fixed-alignment redesign (v23)" section for the full collapse story and why it shipped.
Fixed resolves internally, purely by Orientation: Horizontal → the Damage Log anchor
(anchor_centred_reduced, centred horizontally, proportionally down the viewport), Vertical → the
Minimap anchor (anchor_minimap, to the left of the minimap, measured to the visible TRACK box for
a vertical bar — no horizontal tuner has a minimap placement at all, since Fixed+Horizontal never
resolves there). PROGRESS_ALIGN_DAMAGE_LOG / PROGRESS_ALIGN_MINIMAP (both still 0/1) are
INTERNAL anchor selectors now, never a stored progress_bar_alignment() value — _resolve
branches on vertical directly rather than re-testing a locally-assigned "resolved anchor"
variable, because PROGRESS_ALIGN_MINIMAP and PROGRESS_ALIGN_FREE are BOTH 1 (two different,
never-crossed vocabularies) and re-testing would wrongly match Minimap as Free. The stored X/Y
stepper pair is composed on top of Fixed's anchor via anchor_offset; offset (0,0) IS the shipped
placement byte-for-byte, so Fixed needs no sentinel. Free is its own third branch, unchanged in
shape from v22 — not composed via anchor_offset at all.
Free's stored pair is an ANCHOR POINT, not a top-left (v22, TASKS/in-battle-bar-layout-auto-set-redesign.md
Trap 3 Fix B / DECISION 2 — kept as-is by the v23 redesign). domain/positioning.py's
free_top_left(pair, surface, vertical)
converts it at placement time — bottom-centre for horizontal (pair - (surface_w // 2, surface_h)), bottom-right for vertical (pair - (surface_w, surface_h)) — using THIS
placement's live surface size, so a Default↔Large size flip re-anchors the bar instead of growing
it off to one side (rule 5's size-invariance). free_anchor_point(top_left, surface, vertical) is
the exact inverse, used ONLY to write the pair back (BarHost._materialise, and BarHost.drag's
gesture-end persist). The conversion is placement-only, never written back on a size change —
the engine's compiled clamp (memory [[engine-clamps-every-wulf-window-to-screen-and-the-mod-depends-on-it]])
would bake a crossed-edge clamp in forever if it were. A pre-v22 store's Free pin is still a
literal top-left until the bar's next mount converts it once (progress_bar_pos_frame() == POS_FRAME_LEGACY, mod_settings.py:698-710) — no arithmetic migration at bump time, because no
surface exists to convert against outside a live battle (same wall as materialise-on-mount below).
Free DOES have an auto sentinel (bar_window.py:346-353, corrected 2026-08-08). Under
PROGRESS_ALIGN_FREE the exact pair (0, 0) is rewritten to this ORIENTATION's default alignment
before the branch runs (Horizontal → Damage Log, Vertical → Minimap) — it does NOT mean the screen
corner. That is what lets an explicit Orientation flip zero the stored pair
(mod_settings._derive_layout / _on_changed) without moving the bar somewhere it was never
tuned for.
The one lost capability is pinning a bar at exactly logical (0, 0) — accepted. This claim has
flipped repeatedly; trust the code, and see
[[unclamping-drag-is-constrained-by-the-auto-placement-sentinel]] plus
TASKS/in-battle-bar-layout-auto-set-redesign.md, where (0,0) additionally comes to mean
"Free, not yet materialised".
Materialise-on-mount (bar_window.BarHost._materialise, called from _place after every
resolve): the FIRST real-surface placement after Free is picked (or a legacy pre-v22 pin is
upgraded) writes the resolved on-screen point back as the anchor point, so the panel's steppers
stop reading 0/0 the next time the user looks — no numeric change is possible before then, because
no surface exists to compute one from in the garage panel where Free is picked. Three gates, all
load-bearing: _sized (the first _place at _onReady still sees the engine's 256×256
size-timeout fallback surface — materialising against that bakes a wrong anchor point forever),
own-variant only (both bar hosts share ONE stored pair, and a live variant flip mid-battle can
briefly have both open), and the pair/frame re-read fresh rather than reused from _resolve's
locals (which were overwritten in place by the AUTO rewrite).
Ctrl+drag: adapter/battle_input.py samples Ctrl+LMB → battle_bridge._on_drag(phase, cursor)
(battle_bridge.py:354) → progress_view.drag(...) / efficiency_view.drag(...) →
BarHost.drag() (bar_window.py:467-...). v23: drag() refuses the WHOLE gesture outright
— checked at the very top, before any cursor read or window move, on EVERY phase — while
progress_bar_alignment() != PROGRESS_ALIGN_FREE. This is not a spatial gate (see
[[battle-bar-installdrag-has-no-spatial-gate-by-design]] for the JS-side one that predates and
is unrelated to this); it is an ALIGNMENT gate, and it exists precisely because MSA cannot make a
peer control's edit flip Alignment to Free for us (see moe-settings's gating vocabulary note) —
so the gesture is blocked instead of letting the bar visibly follow the cursor and then snap back
at gesture end. Absolute placement via cursor_top_left
(positioning.py), gain exactly 1 (see _space()'s derivation), ownership gated by
cursor_in_rect against the gesture's own window rect. window.move() still takes the
computed top-left, but what gets persisted (both the live per-move update and the gesture-end
write) is that top-left re-expressed as the Free anchor point via free_anchor_point — the grab
offset stays in top-left space throughout; only the value handed to
mod_settings.set_bar_position(x, y, persist=True) is converted, on every move (unpersisted) and
on phase "end" (persisted, and only if the gesture actually moved). Both bar VMs are
commands=0 — no setPosition reverse command; the
drag is entirely Python-owned (see moe-battle's Ctrl+drag section for the full mechanics,
shared verbatim by both bars).
Constants (domain/constants.py): PROGRESS_ANCHOR_Y_FRAC=0.865,
PROGRESS_ANCHOR_X_OFFSET=0, PROGRESS_ANCHOR_Y_SHIFT=-44 / _LARGE=-65,
PROGRESS_MM_GAP_BOTTOM=30, PROGRESS_MM_TRACK_X=105 / _LARGE=147 (pure derivation 107/149,
with a measured -2 hand-placement correction — see the constants' long comment on two
independent Ctrl+drags landing on the same corrected value across different surface
geometries; both the pure derivation and the correction's own validity predate the vertical
bars' V_PAD_X_REM growth to 70/52 below — a fresh in-game drag is owed); EFFICIENCY_ANCHOR_Y_FRAC=0.865,
EFFICIENCY_ANCHOR_Y_SHIFT=-50 / _LARGE=-77,
EFFICIENCY_MM_GAP_BOTTOM=28, EFFICIENCY_MM_TRACK_X=95 / _LARGE=137 (pure derivation, no
correction — only one, unconfirmed hand-drag exists for this bar, and it too predates the
growth below). Shared:
MM_GAP=8, MM_TICK_OVERHANG=3/_LARGE=5, MM_TRACK_Y=290/_LARGE=363,
VERTICAL_ANCHOR_Y_SHIFT=-90/_LARGE=-170 (identical for both bars — both vertical
compositions share the same backdrop geometry).
The _LARGE Y-shifts above were RE-DERIVED (rule 5, -55→-65, -63→-77, -113→-170) to pin
the composition's BOTTOM ink rather than the naive shift * SIZE_F algebraic identity, which pins
neither ink edge — see memory [[anchor-y-shift-large-pins-neither-ink-edge]]. These stay live:
rule 5's size-invariance still holds through anchor_minimap (Minimap/Fixed+vertical) and
free_top_left (Free, either orientation).
DELETED at v23 (the Fixed-alignment redesign, not merely unreachable): anchor_centred_ reduced's x_shift parameter, BarHost's x_shift_large constructor argument, and
PROGRESS_ANCHOR_X_SHIFT_LARGE / EFFICIENCY_ANCHOR_X_SHIFT_LARGE. This machinery existed only
to right-pin a VERTICAL bar resolving to the Damage Log anchor under Large (rule 5's X term for
that one combination); it is now structurally unreachable, not just unused — Alignment only ever
stores Fixed or Free (clamp_variant's ceiling is PROGRESS_ALIGN_FREE == 1), and Fixed always
resolves to Minimap when vertical (_resolve), which was already X-invariant by construction
(anchor_minimap subtracts from the full space using per-size TRACK offsets). There is no stored
value or UI path left that can select the old vertical+Damage-Log combination, so the constants,
the parameter and the constructor argument were removed rather than left dead. VERTICAL_ANCHOR_Y_ SHIFT / _LARGE (the −170 value) are KEPT even though placement no longer reads them for a
centred vertical anchor — the JS files still cite them as a wire-contract record and tests still
pin them against real geometry.
Settings (keys/getters — see moe-settings for the panel itself)
SETTINGS_VERSION=28. Master PROGRESS_BAR_KEY="progress_bar_enabled" (default False), getter
progress_bar_enabled() (mod_settings.py:516). Variant PROGRESS_VARIANT_KEY= "progress_bar_variant" (:548, 0=Efficiency/1=Moving Average). Size PROGRESS_SIZE_KEY= "progress_bar_size" (:559, 0=default/1=Large). Orientation PROGRESS_ORIENTATION_KEY= "progress_bar_orientation" (:640, 0=Horizontal/1=Vertical). Alignment PROGRESS_ALIGNMENT_KEY= "progress_bar_alignment" (:440, 0=Fixed/1=Free as of v23, collapsed from 0=Damage Log/1=Minimap/
2=Free — see moe-settings). Visibility children
PROGRESS_SHOW_EVENTS_KEY, PROGRESS_SHOW_ALT_KEY, PROGRESS_SHOW_ALWAYS_KEY (all default
True/True/False), folded by progress_show_events() (:524) and progress_alt_held(alt_held)
(:535 — "Always" IS a permanently-held Alt, no fourth code path). Transitions
PROGRESS_TRANSITIONS_KEY (master) + PROGRESS_TRANS_EVENTS_KEY + PROGRESS_TRANS_MANUAL_KEY
(all True) folded by progress_transitions_events()/progress_transitions_manual()
(:570-587). Hold slider PROGRESS_HOLD_SECONDS_KEY 1–30s default 5 →
progress_hold_seconds() — deliberately NOT master-folded (a duration, not a switch). Position
BAR_POS_X_KEY="progress_bar_pos_x" / BAR_POS_Y_KEY="progress_bar_pos_y" →
bar_pos_x()/bar_pos_y()/set_bar_position(). Every one of these is shared by both bars.
battle_bridge._window_gates() (:145-157) folds progress_bar_enabled() × progress_bar_variant()
into the two centre-bar entries — keep any new gate in lockstep there.
Tests
python -m pytest -q (repo root, Python 3.13). Progress/Efficiency-relevant:
tests/test_progress_bar_domain.py, tests/test_progress_bridge.py,
tests/test_progress_surface_mirror.py, tests/test_efficiency_axis.py,
tests/test_efficiency_bridge.py, tests/test_efficiency_surface_mirror.py,
tests/test_bar_window.py (shared BarHost, both bars), tests/test_battle_input.py,
tests/test_battle_bridge_settings.py, tests/test_mod_settings.py,
tests/test_view_models.py (derives properties= counts from source — the ONE place that
catches a slot append without the matching bump). Plus the Node checkers above.
Gotchas
Orientation flip is not live-stylable
The JS branches on vertical only at mount, so a live radio change must close+reopen the
window via battle_bridge.apply_settings() (battle_bridge.py:852-880, which diffs
_bar_orientation against the freshly-read setting), not just re-push the VM.
_extent() is a real window MOVE
Far-sentinel window.move(1<<20, 1<<20, ...) (bar_window.py:309-323) — memoize it, invalidate
only in _place(). BarHost.drag() must read window.position BEFORE calling _extent()
(bar_window.py:419-421) or a cold cache teleports the window to the sentinel first.
#moe-bar-box does not defeat the size-calculation timeout
The engine's "Size calculation timeout" fallback (256×256) runs LAST and wins even with the box
present, in-flow, and correctly sized — the JS's post-deadline SURFACE_REASSERT_MS re-assert
is the real fix, and the bar must stay hidden (surfaceSettled) until the surface lands or it
renders cropped ~142px too high.
VM properties= must be bumped with every appended field
ProgressVM.properties=16 (vertical at slot 15 was the last append); EfficiencyVM. properties=18 (vertical at slot 17). test_view_models.py derives the count from source and
does catch a mismatch — the push tests' fake VM ignores the declared count and cannot.
The two bars share #moe-bar-root and the .mp-* namespace
They avoid collision only because they are radio alternatives — never assume they compose or
can both be open (each host still keeps its OWN _active singleton so a live variant switch can
open one before the other closes).
Vertical surface must stay concentric with its track
anchor_centred_reduced has no x term, so widening one side of a vertical surface alone re-aims
the whole bar — mirrored in three places (both JS files' V_PAD_X_REM/shift derivations, one in
document rem, plus the shared VERTICAL_ANCHOR_Y_SHIFT constant).
The vertical Efficiency bar has no clip-invariance gate yet
The vertical Moving Average bar shipped a real clip on its bottom row once, caught only by
manually opening the tuner artifact and running its in-browser digit-count invariance check
(checkCaptionInvariance() in gen_bar_tuner_vertical.ps1, headless-SKIPped by
check_bar_vertical.js/test_progress_surface_mirror.py). The equivalent check exists inside
the Efficiency tuner's own selfCheck(), but nothing yet asserts a live clip bound the way the
Moving Average side does — treat a vertical Efficiency layout change as unverified until you
open the artifact and press Self-check.
Efficiency delta section
Beyond the VM/DOM/settings tables above (already Efficiency-inclusive), the Damage Efficiency
bar's own quirks: its bottom row has two icon-Y knobs in the tuner (eff_bar_tuner.html) —
icoyReq moves the three requirement-tick icons, icoyBm the r4/goalpost glyph, icoyCur the
current-damage top-row icon; these are tuner-only constants baked into the emitted CSS, not
exposed at runtime. band selects one of five CSS classes .mp-b-{w,g,t,v,au} (white/green/
teal/violet/gold — 0..4 requirements passed). barX is computed by
domain.battle_builder.efficiency_bar_x over the four equal-quarter stops
EFFICIENCY_BAR_STOPS=(0,25,50,75,100) (constants.py:39) — do not recompute the axis mapping
in JS. battleEpoch (slot 10) is the only field with no Progress equivalent: a monotonic
per-battle counter Python bumps before the first refresh() each battle
(battle_bridge.py:167-177), letting MoEEfficiency.js reset its own damage-delta latch on a
battle boundary without a dedicated VM reset command.