diff --git a/SCENARIO_EDITOR_PLAN.md b/SCENARIO_EDITOR_PLAN.md index f50281b7..439026bd 100644 --- a/SCENARIO_EDITOR_PLAN.md +++ b/SCENARIO_EDITOR_PLAN.md @@ -132,11 +132,14 @@ - `apps/scenario-editor/` (from Stage 3) ### Requirements (Task Checklist) -- [ ] **4.1** Add objective editor for predicate DSL (`metric`, `all`, `any`, `not`). -- [ ] **4.2** Add event/action editor for declarative scripts (`atTick`, `everyTicks`, actions union). -- [ ] **4.3** Add behavior profile assignment UI with closed profile validation. -- [ ] **4.4** Add authoring-time semantic validation (unknown metric, invalid op/type combos, empty `all/any`, etc.). -- [ ] **4.5** Integrate edited scripts/objectives into export pipeline. +- [x] **4.1** Add objective editor for predicate DSL (`metric`, `all`, `any`, `not`). +- [x] **4.2** Add event/action editor for declarative scripts (`atTick`, `everyTicks`, actions union). +- [x] **4.3** Add behavior profile assignment UI with closed profile validation. +- [x] **4.4** Add authoring-time semantic validation (unknown metric, invalid op/type combos, empty `all/any`, etc.). +- [x] **4.5** Integrate edited scripts/objectives into export pipeline. + +Stage 4.3 repair note: behavior profile assignment now uses closed-key selection UI with explicit invalid-key diagnostics tied to the runtime profile registry. +Stage 4.4 repair note: semantic validators now reject predicate/script kind-field mismatches (invalid type combos) in addition to unknown metrics/operators and empty composite nodes. ### Good Tests - UI-driven serialization tests from form model -> valid script/objective JSON. diff --git a/apps/scenario-editor/package.json b/apps/scenario-editor/package.json index fca6be19..bb4924be 100644 --- a/apps/scenario-editor/package.json +++ b/apps/scenario-editor/package.json @@ -14,6 +14,7 @@ "dependencies": { "@city/classicyui": "workspace:*", "@city/scenario-core": "workspace:*", + "@city/scenario-runtime": "workspace:*", "@tanstack/react-router": "1.157.16", "react": "19.2.4", "react-dom": "19.2.4" diff --git a/apps/scenario-editor/src/routes/__root.tsx b/apps/scenario-editor/src/routes/__root.tsx index f68508bc..343164c4 100644 --- a/apps/scenario-editor/src/routes/__root.tsx +++ b/apps/scenario-editor/src/routes/__root.tsx @@ -16,7 +16,7 @@ function RootRouteLayout() { return (
-
Scenario Editor MVP
+
Scenario Editor
@@ -76,5 +76,14 @@ function getScenarioEditorWorkbenchViewLabel(view: ScenarioEditorWorkbenchView): if (view === 'map') { return 'Map'; } + if (view === 'objective') { + return 'Objective'; + } + if (view === 'script') { + return 'Scripts'; + } + if (view === 'behavior') { + return 'Behavior'; + } return 'Export'; } diff --git a/apps/scenario-editor/src/routes/index.tsx b/apps/scenario-editor/src/routes/index.tsx index c4cb9864..5cae27f6 100644 --- a/apps/scenario-editor/src/routes/index.tsx +++ b/apps/scenario-editor/src/routes/index.tsx @@ -10,6 +10,11 @@ import { useState, } from 'react'; +import { + getScenarioEditorBehaviorValidationIssue, + isScenarioEditorBehaviorProfileKey, + SCENARIO_EDITOR_BEHAVIOR_PROFILE_KEYS, +} from '../state/editor-behavior.ts'; import { buildScenarioEditorStrictExport, getScenarioEditorExportFileName, @@ -26,6 +31,37 @@ import { readScenarioEditorMapTileWord, type ScenarioEditorMapPoint, } from '../state/editor-map.ts'; +import { + appendScenarioObjectiveChildPredicate, + coerceScenarioObjectivePredicateKind, + getScenarioEditorObjectiveValidationIssues, + removeScenarioObjectiveChildPredicate, + replaceScenarioObjectiveChildPredicate, + replaceScenarioObjectiveNotChildPredicate, + SCENARIO_EDITOR_OBJECTIVE_COMPARISONS, + SCENARIO_EDITOR_OBJECTIVE_METRIC_KEYS, + SCENARIO_EDITOR_OBJECTIVE_PREDICATE_KINDS, + type ScenarioEditorObjectivePredicate, +} from '../state/editor-objective.ts'; +import { + appendScenarioEditorScriptAction, + appendScenarioEditorScriptEvent, + coerceScenarioEditorScriptActionKind, + coerceScenarioEditorScriptTriggerKind, + getScenarioEditorScriptTriggerKind, + getScenarioEditorScriptValidationIssues, + removeScenarioEditorScriptAction, + removeScenarioEditorScriptEvent, + replaceScenarioEditorAtTickTrigger, + replaceScenarioEditorEveryTicksTrigger, + replaceScenarioEditorScriptAction, + replaceScenarioEditorScriptEvent, + replaceScenarioEditorSendMessageId, + SCENARIO_EDITOR_SCRIPT_ACTION_KINDS, + SCENARIO_EDITOR_SCRIPT_TRIGGER_KINDS, + type ScenarioEditorScriptAction, + type ScenarioEditorScriptEvent, +} from '../state/editor-script.ts'; import { getScenarioEditorMetadataValidationIssues, parseScenarioEditorTagsInput, @@ -57,10 +93,12 @@ export const Route = createFileRoute('/')({ }); /** - * Stage 3 workbench route with metadata editing and manual map editing MVP sections. - * Parity note: metadata `startYear`/`startFunds` map to `LoadScenario` fields in - * `ref/micropolis/src/sim/s_fileio.c`; map edits mirror `Map[x][y]` writes from - * `SimCmdTile`/`SimCmdFill` in `ref/micropolis/src/sim/w_sim.c`. + * Stage 4 workbench route with metadata/map editing plus objective and script authoring. + * Parity note: objective metric leaves map to `DoScenarioScore` checks in + * `ref/micropolis/src/sim/s_msg.c`, while logical composition forms are declarative + * runtime extensions from `packages/scenario-runtime`; script events map to + * `ScenarioDisaster` trigger/action domains in `ref/micropolis/src/sim/s_disast.c`; behavior + * profile assignment maps closed `DoShipSprite` variants from `ref/micropolis/src/sim/w_sprite.c`. */ function ScenarioEditorHomeRoute() { const { activeView } = useScenarioEditorState(); @@ -71,6 +109,15 @@ function ScenarioEditorHomeRoute() { if (activeView === 'map') { return ; } + if (activeView === 'objective') { + return ; + } + if (activeView === 'script') { + return ; + } + if (activeView === 'behavior') { + return ; + } return ; } @@ -382,6 +429,619 @@ function ScenarioMapEditorCard() { ); } +/** + * Objective authoring card for Stage 4.1 predicate DSL editing. + * Parity note: metric leaves mirror `DoScenarioScore` checks in + * `ref/micropolis/src/sim/s_msg.c`; logical nodes (`all`/`any`/`not`) are + * declarative extensions supported by `packages/scenario-runtime`. + */ +function ScenarioObjectiveEditorCard() { + const { objective, isDirty } = useScenarioEditorState(); + const dispatch = useScenarioEditorDispatch(); + const validationIssues = useMemo( + () => getScenarioEditorObjectiveValidationIssues(objective), + [objective], + ); + const objectiveJson = useMemo( + () => JSON.stringify(objective.enabled ? objective.predicate : null, null, 2), + [objective.enabled, objective.predicate], + ); + + return ( +
+

Scenario Objective

+

+ Author objective predicates using the Stage 4 DSL. Metric comparisons track classic + `DoScenarioScore` fields, while `all`/`any`/`not` allow composed checks. +

+ + + + {objective.enabled ? ( + { + dispatch({ type: 'replace-objective-predicate', predicate }); + }} + predicate={objective.predicate} + /> + ) : ( +

Objective checks are disabled for this draft.

+ )} + +
+
Dirty State
+
{isDirty ? 'dirty' : 'clean'}
+
Objective Enabled
+
{objective.enabled ? 'yes' : 'no'}
+
Root Predicate
+
{objective.enabled ? objective.predicate.kind : 'none'}
+
Validation
+
+ {!objective.enabled + ? 'disabled' + : validationIssues.length === 0 + ? 'valid' + : `invalid (${validationIssues.length} issue${ + validationIssues.length === 1 ? '' : 's' + })`} +
+
+ + {objective.enabled && validationIssues.length > 0 ? ( +
+

Objective Semantic Issues

+
    + {validationIssues.map((issue, index) => ( +
  • + {issue.path}: {issue.message} +
  • + ))} +
+
+ ) : null} + +
+

Objective Predicate JSON

+