Phase 95: Calibration & Scenario Editor Depth¶
Status: Complete Block: 10 (UI Depth & Engine Exposure) Tests: 24 new frontend tests (396 total vitest) Files: 3 new + 7 modified frontend files, 3 new test files + 1 modified test file
What Was Built¶
95a: Per-Side Calibration Panel¶
- New collapsible "Per-Side Overrides" section in CalibrationSliders
- Blue/Red tab buttons (useState toggle), side names derived from config.sides
- 4 per-side sliders: cohesion, force_ratio_modifier, hit_probability_modifier, target_size_modifier
- New
SET_SIDE_CALIBRATIONaction type → writescalibration_overrides.side_overrides.{side}.{field} - 3-level immutable spread in reducer for nested state update
95b: Expanded Morale & Rout Cascade Sliders¶
- Morale group expanded from 1 to 5 sliders: morale_degrade_rate_modifier + morale_base_degrade_rate, morale_casualty_weight, morale_force_ratio_weight, morale_check_interval
- New "Rout Cascade" slider group: rout_cascade_radius_m, rout_cascade_base_chance
- All use existing
SET_CALIBRATIONaction — backend_MORALE_KEY_MAPhandles routing to nested MoraleCalibration
95c: Doctrine & Commander Pickers¶
- DoctrinePicker.tsx (new): Per-side
<select>dropdowns populated fromGET /meta/schoolsviauseSchools()hook. "None" option, description text, OODA multiplier and risk tolerance badges. - CommanderPicker.tsx (new): Per-side
<select>dropdowns fromGET /meta/commandersviauseCommanders()hook. "None" option, trait preview card with key-value grid. SET_SCHOOLaction → writesschool_config.{side}_school, auto-enablesschool_configif absentSET_COMMANDERaction → writescommander_config.side_defaults.{side}, auto-enablescommander_configif absent- API client functions
fetchSchools(),fetchCommanders()added toapi/meta.ts - TanStack Query hooks
useSchools(),useCommanders()added tohooks/useMeta.ts SchoolInfo,CommanderInfoTypeScript interfaces added totypes/api.ts
95d: Victory Weights Editor¶
- VictoryWeightsEditor.tsx (new): 3 sliders for force_ratio (default 1.0), morale_ratio (default 0.0), casualty_exchange (default 0.0)
- Normalized percentage display:
Math.round(value / total * 100)%next to each slider - Warning message when all weights are zero
SET_VICTORY_WEIGHTaction → writescalibration_overrides.victory_weights.{key}- Keys match engine's
VictoryEvaluator.evaluate_force_advantage()actual usage
Wiring¶
- All 3 new components imported and placed in ScenarioEditorPage.tsx
- DoctrinePicker + CommanderPicker between ForceEditor and ConfigToggles
- VictoryWeightsEditor after CalibrationSliders
commander_config: {}added to CONFIG_DEFAULTS in useScenarioEditor.ts
Design Decisions¶
- Dedicated action types over overloading SET_CALIBRATION — SET_SIDE_CALIBRATION needs 3-level nesting, SET_SCHOOL/SET_COMMANDER write to non-calibration config paths. Type safety over DRY.
- school_config uses blue_school/red_school — matches existing YAML format (
school_config: { blue_school: maneuverist }). - commander_config uses side_defaults.{side} — matches YAML format (
commander_config: { side_defaults: { blue: joint_commander } }). - Auto-enable on select — selecting a school auto-creates
school_configvia CONFIG_DEFAULTS pattern. Clearing does NOT auto-disable. - Victory weight keys match engine — spec said
casualties/morale/territorybut engine usesforce_ratio/morale_ratio/casualty_exchange. Implementation follows engine, spec corrected. - morale_check_interval default = 1 — matches engine's
MoraleCalibration.check_intervaldefault, not the spec's suggested 15.
Deviations from Plan¶
- Era-aware commander filtering (95c) — not implemented.
CommanderInfohas noerafield; commanders are era-agnostic by schema design. This was a spec issue, not a gap. - Stacked bar visualization (95d) — replaced with inline percentage display. The
(60%)annotation next to each slider is cleaner for 3 values than a separate bar chart component. - Victory weight keys — spec said
casualties/morale/territory, implementation usesforce_ratio/morale_ratio/casualty_exchange(matching engine). Spec corrected.
Files Changed¶
New Files¶
frontend/src/pages/editor/DoctrinePicker.tsxfrontend/src/pages/editor/CommanderPicker.tsxfrontend/src/pages/editor/VictoryWeightsEditor.tsxfrontend/src/__tests__/pages/editor/DoctrinePicker.test.tsxfrontend/src/__tests__/pages/editor/CommanderPicker.test.tsxfrontend/src/__tests__/pages/editor/VictoryWeightsEditor.test.tsx
Modified Files¶
frontend/src/types/api.ts— SchoolInfo, CommanderInfo interfacesfrontend/src/types/editor.ts— 4 new EditorAction typesfrontend/src/hooks/useScenarioEditor.ts— 4 reducer cases, commander_config defaultfrontend/src/api/meta.ts— fetchSchools, fetchCommandersfrontend/src/hooks/useMeta.ts— useSchools, useCommanders hooksfrontend/src/pages/editor/CalibrationSliders.tsx— per-side section, expanded morale, rout cascadefrontend/src/pages/editor/ScenarioEditorPage.tsx— wire 3 new componentsfrontend/src/__tests__/pages/CalibrationSliders.test.tsx— 12 new reducer + component tests
Postmortem¶
- Scope: On target. All 4 sub-phases delivered. Two spec items descoped (era filtering, stacked bar) — both were spec issues, not implementation gaps.
- Quality: High. Zero TODOs, zero dead code, full type safety, 24 new tests.
- Integration: Fully wired. Every new component imported, every action handled, every API function consumed.
- Deficits: None. All features work as intended.