29 KiB
Phase 5 — Sandbox Scene File Features
Status: implemented and verified in Godot/headless Phase 5 coverage.
1. Goal and boundaries
Phase 5 gives the standalone Sandbox Stage Builder durable, versioned scene files. It adds New, Open, Open Recent, Save, Save As, Rename Scene, dirty-state prompts, an autosave-on-Play recovery path, and tolerant handling of missing .stk assets.
This plan targets res://scenes/sandbox_stage.tscn, which is run separately with F6. It does not change the stickman editor's existing .stk Save/Load flow or the project's main scene.
Current architecture facts that constrain the design:
SandboxStagebuilds its entire UI inscripts/sandbox_stage.gd; the.tscncontains onlySandboxStage/Camera2DandSandboxStage/World.- There is no existing sandbox File menu or Clear command. Phase 5 creates the menu; it does not reuse a Clear flow.
- All authored objects are direct children of
World.NavigationRegion2D, gizmos, overlays, ragdoll bodies, and collision proxies are not scene-file objects. _authoredis the source of the edit pose restored after Play. Saving must use that authored transform rather than a prop's or rig's transient physics transform. Currently_on_transform_committed()refreshes_authoredonly forRigidBody2D; Phase 5 must move_save_object_state(node)outside that type branch so transformed terrain, areas, and stickmen are also authoritative.- Runtime rules store
Object.get_instance_id()integers intrigger.source,trigger.target, and rule-actiontarget. Those IDs cannot be written as persistent references. - Queue and rule dictionaries contain
Vector2values. They require an explicit JSON conversion; raw runtime dictionaries are not the on-disk contract. TerrainBlock.spawn_idrecords its source template.PropBlockdoes not currently record its prop-library ID, andStickmanRigdoes not record its source.stkpath. Phase 5 must add that provenance at spawn time.
2. Files and classes
| File | Planned responsibility |
|---|---|
scripts/stage_scene_io.gd (new class_name StageSceneIO, extends RefCounted) |
Own the v1.0 JSON contract, JSON-safe action/rule conversion, validation, migration dispatch, and failure-safe disk reads/writes. It does not mutate the live tree. |
scripts/sandbox_stage.gd |
Own file-menu/dialog flow, dirty state, pending destructive actions, live-tree collection/application, runtime-ID mapping, recents, autosave lifecycle, title/status updates, and one final navigation rebuild. |
scripts/stage_spawner.gd |
Record asset provenance and expose load-safe spawn methods whose positions are actual object origins, not placement-cursor coordinates. |
scripts/prop_block.gd |
Add var spawn_id: String = "". |
scripts/stickman_rig.gd |
Add var source_stk_path: String = "". This is provenance only and does not change runtime rig behavior. |
scripts/missing_stickman_placeholder.gd (new MissingStickmanPlaceholder, extends Node2D) |
Draw and expose selection bounds for a missing-stickman marker while retaining the original stickman file entry. |
tests/test_phase5_file_features.gd (new) |
Headless codec, round-trip, mapping, dirty-state, missing-asset, and autosave lifecycle coverage. |
No authored node changes are required in scenes/sandbox_stage.tscn; the new controls remain code-built to match the current stage.
3. Versioned .scene contract
The extension is .scene. The content is UTF-8, tab-indented JSON. v1.0 uses only JSON primitives, arrays, and dictionaries.
3.1 Root schema
{
"version": "1.0",
"scene_name": "Playground",
"camera": {
"position": { "x": 0.0, "y": -400.0 },
"zoom": 0.5
},
"objects": [],
"event_rules": [],
"metadata": {
"created_unix": 1789420000,
"modified_unix": 1789420300,
"autosave": false,
"source_path": ""
}
}
versionis required and must equal a supported version after migration.scene_nameis a non-empty string after trimming; invalid or empty input becomes"Untitled".camerais optional. Missing/invalid values use position(0, -400)and uniform zoom0.5; loaded zoom is clamped to the stage'smin_zoom/max_zoom.objectsandevent_rulesare required arrays. Unknown root and entry keys are ignored and are not preserved on the next save.created_unixis retained across manual saves.modified_unixis refreshed for each written snapshot. Both are UTC Unix seconds.- Manual files use
autosave: falseand an emptysource_path. An autosave usesautosave: trueand stores the manual scene path, if any, insource_path.
All positions are stage-world coordinates. All rotations are degrees in the file and radians at runtime. All vectors use { "x": number, "y": number }.
3.2 Object IDs and ordering
Each entry has a required, non-empty file_id string. IDs must be unique across the whole objects array; a duplicate entry is rejected before anything is spawned or inserted into the reference map. On each save, objects are walked in current World child order and assigned readable IDs with per-type counters: terrain_1, prop_1, stickman_1, area_1. IDs are stable within that snapshot, not permanent node identities.
The encoder simultaneously builds runtime instance ID -> file_id. The loader spawns objects in saved array order and builds file_id -> runtime instance ID. Navigation does not require terrain-first spawning, so preserving file order avoids an unnecessary World-order change; navigation is rebuilt once after all objects and rules are applied.
3.3 Object entries
Terrain:
{
"file_id": "terrain_1",
"type": "terrain",
"asset_id": "ground",
"position": { "x": 0.0, "y": 0.0 },
"rotation": 0.0,
"scale": { "x": 1.0, "y": 1.0 }
}
Prop:
{
"file_id": "prop_1",
"type": "prop",
"asset_id": "crate",
"position": { "x": 100.0, "y": -40.0 },
"rotation": 12.0,
"scale": { "x": 1.0, "y": 1.0 },
"material_preset": "WOOD"
}
Stickman:
{
"file_id": "stickman_1",
"type": "stickman",
"stk_path": "res://stickmen/test.stk",
"position": { "x": 0.0, "y": -385.0 },
"rotation": 0.0,
"facing_profile": "FORWARD",
"action_queue": [
{ "type": "walk_to", "target": { "x": 300.0, "y": 0.0 } }
]
}
Trigger area:
{
"file_id": "area_1",
"type": "area",
"position": { "x": 500.0, "y": 0.0 },
"rotation": 0.0,
"scale": { "x": 1.0, "y": 1.0 },
"size": { "x": 160.0, "y": 80.0 }
}
Rules:
- Terrain
asset_idcomes fromTerrainBlock.spawn_id. - Prop
asset_idcomes from the newPropBlock.spawn_id.material_presetis saved as the stable enum name"NONE","WOOD","RUBBER","CARDBOARD", or"METAL"rather than its reorderable integer, even though current library templates also provide it. stk_pathis the exact normalized source used to spawn the rig: keepres://paths project-relative and keep external paths absolute. Scene files do not embed.stkdata in Phase 5.- Stickman
position,rotation, andfacing_profilecome from_authoredwhen available, not the live Play pose. Root scale is intentionally not stored for stickmen becauseStickmanRig.set_facing_profile(LEFT)derives the root X mirror. Reapplying a serialized mirror would double-flip the rig. - Terrain, prop, and area scale are saved. The present gizmo does not edit scale, but transformed areas already exist in tests and must round-trip correctly.
- Only known stage object classes are serialized. Ghosts, overlays, ragdoll bodies, navigation nodes, collision proxies, and unknown World children are skipped with a warning.
3.4 Queue-action schema
Queue actions preserve their existing flat runtime shape, with vectors converted to JSON dictionaries:
walk_to -> { "type": "walk_to", "target": {x, y} }
speak -> { "type": "speak", "text": String, "duration": float, optional "stop_current": bool }
wait -> { "type": "wait", "duration": float }
ragdoll -> { "type": "ragdoll" }
recover -> { "type": "recover" }
Order is preserved. StageSceneIO validates types against ActionRegistry; malformed or unknown actions are skipped with warnings instead of reaching the runner.
3.5 Event-rule schema and reference boundary
The disk rule deliberately differs from the live rule at reference fields:
{
"id": 4,
"trigger": {
"type": "entered_area",
"source_id": "stickman_1",
"target_id": "area_1",
"params": {}
},
"actions": [
{
"type": "speak",
"target_id": "stickman_2",
"params": { "text": "Hello", "duration": 2.0 }
}
]
}
source_id/target_idare file-ID strings. A runtime-1wildcard is encoded as JSONnulland decoded back to-1.- Trigger params are:
waypoint_pos: {x,y}forarrived_at_waypoint,action_type: Stringforaction_finished, and{}for the other current trigger types. - Rule-action params keep the existing nested runtime shape;
walk_to.params.targetis converted to/from{x,y}. - On load, a rule is installed only when every non-null object reference resolves to a compatible live object. One unresolved reference skips the complete rule and contributes to the final load warning.
- Runtime rules remain unchanged: after decoding they again use integer instance IDs under
trigger.source,trigger.target, and actiontarget. Existing event matching, panels, and editors therefore need no data-model rewrite. - Loaded numeric rule IDs are retained when unique and non-negative. Duplicate/invalid IDs are reassigned in array order.
_next_rule_idbecomesmax(loaded IDs) + 1.
This conversion boundary replaces the original assumption that the runtime rule dictionary can be serialized unchanged.
3.6 Validation and migration
StageSceneIO validates the complete root before the live stage is cleared. A missing file, I/O failure, invalid JSON, non-dictionary root, absent/unsupported version, or non-array objects/event_rules fails the load and leaves the current stage untouched.
Individual invalid object/action/rule entries are warn-and-skip. Every decoded numeric field must be finite (is_finite()); vectors require finite numeric x and y, rotations/durations/zoom reject NaN or infinity, and area size must be positive. Unknown keys are dropped. v1.0 is the first version, so _migrate_to_current(data: Dictionary) -> Dictionary has no transformations yet, but version dispatch must exist and reject newer unsupported versions cleanly.
4. StageSceneIO API contracts
class_name StageSceneIO
extends RefCounted
const FILE_VERSION: String = "1.0"
static func read_file(path: String) -> Dictionary
# { "ok": bool, "data": Dictionary, "error": String, "warnings": Array[String] }
static func write_file_safe(path: String, data: Dictionary) -> Dictionary
# { "ok": bool, "error": String }
static func encode_queue(actions: Array[Dictionary]) -> Dictionary
# { "actions": Array[Dictionary], "warnings": Array[String] }
static func decode_queue(entries: Array) -> Dictionary
# { "actions": Array[Dictionary], "warnings": Array[String] }
static func encode_rules(rules: Array[Dictionary], runtime_to_file: Dictionary) -> Dictionary
# { "rules": Array[Dictionary], "warnings": Array[String] }
static func decode_rules(entries: Array, file_to_runtime: Dictionary, unavailable_ids: Dictionary) -> Dictionary
# { "rules": Array[Dictionary], "warnings": Array[String], "skipped": int }
static func vector_to_data(value: Vector2) -> Dictionary
static func is_vector_data(value: Variant) -> bool
static func data_to_vector(value: Variant, fallback: Vector2) -> Vector2
write_file_safe uses sibling <target>.tmp and <target>.bak files because Godot's DirAccess.rename_absolute(temp, target) is not a guaranteed overwrite operation when target already exists on Windows:
- Resolve and verify the absolute target/temp/backup paths share the same parent directory.
- Write, flush, and close the complete temporary file.
- If a target exists, remove only a stale verified sibling backup, then rename the target to the backup.
- Rename the temporary file to the target.
- If step 4 fails, rename the backup back to the target and report failure; never report success until the new target exists.
- On success, remove the backup. On startup/read, if the target is absent but its backup exists, restore the backup before reading; if both target and backup exist, keep the target and remove the stale backup. Stale temporary files are safe to remove.
Each same-directory rename is atomic at the filesystem level; the backup and rollback protocol protects the prior destination across the multi-step replacement and makes an interrupted swap recoverable. No recursive filesystem operation is involved.
5. Spawn and provenance contracts
StageSpawner.spawn(id, cursor_position) remains the palette API, including the stickman's STICKMAN_FOOT_OFFSET. Add load-safe APIs that do not apply a second placement offset:
func spawn_terrain_asset(asset_id: String, object_position: Vector2) -> TerrainBlock
func spawn_prop_asset(asset_id: String, object_position: Vector2) -> PropBlock
func spawn_stickman_asset(stk_path: String, root_position: Vector2) -> StickmanRig
func spawn_area(object_position: Vector2) -> TriggerArea
The existing palette helpers delegate to these methods. On success:
- terrain sets
TerrainBlock.spawn_id = asset_id; - prop sets
PropBlock.spawn_id = asset_id; - stickman sets
StickmanRig.source_stk_path = normalized stk_path.
Unknown terrain/prop IDs return null plus a warning. A missing or malformed .stk returns null and is handled by the stage as §11 describes. Load APIs must not mutate selected_prop_id or selected_stickman_path.
6. Sandbox stage state and internal APIs
Add:
enum PendingFileAction { NONE, NEW_SCENE, OPEN_DIALOG, OPEN_PATH, QUIT }
const AUTOSAVE_PATH: String = "user://sandbox_autosave.scene"
const RECENT_SCENE_LIMIT: int = 5
const DEFAULT_SCENE_NAME: String = "Untitled"
const DEFAULT_CAMERA_POSITION: Vector2 = Vector2(0.0, -400.0)
const DEFAULT_CAMERA_ZOOM: float = 0.5
var _current_scene_path: String = ""
var _scene_name: String = DEFAULT_SCENE_NAME
var _scene_created_unix: int = 0
var _dirty: bool = false
var _suppress_dirty: bool = false
var _recent_scenes: Array[String] = []
var _pending_file_action: PendingFileAction = PendingFileAction.NONE
var _pending_open_path: String = ""
Primary stage methods:
func _mark_scene_dirty() -> void
func _set_scene_dirty(value: bool) -> void
func _collect_scene_data(is_autosave: bool = false) -> Dictionary
func _save_scene_to_path(path: String, is_autosave: bool = false) -> bool
func _load_scene_from_path(path: String, is_recovery: bool = false) -> bool
func _apply_scene_data(data: Dictionary, source_path: String, is_recovery: bool) -> bool
func _reset_scene_content() -> void
func _request_pending_file_action(action: PendingFileAction, path: String = "") -> void
func _continue_pending_file_action() -> void
func _update_scene_chrome() -> void
func _update_recent_menu() -> void
_suppress_dirty is set with a scoped save/restore around load/New operations. It prevents spawn wiring and queue population from marking the new document dirty. It must be restored on every return path.
7. File menu, dialogs, and layout
Add a MenuButton as the first control in the existing code-built top-bar HBoxContainer. Give new dynamic controls explicit names so tests and future code can resolve them:
SandboxStage/UI (CanvasLayer)
├── TopBar (PanelContainer)
│ └── Content (HBoxContainer)
│ └── FileMenu (MenuButton)
├── SceneOpenDialog (FileDialog)
├── SceneSaveDialog (FileDialog)
├── RenameSceneDialog (ConfirmationDialog)
│ └── SceneNameEdit (LineEdit)
├── MissingAssetsDialog (AcceptDialog)
├── UnsavedChangesDialog (ConfirmationDialog)
└── AutosaveRecoveryDialog (ConfirmationDialog)
The existing asset BrowseDialog remains dedicated to .stk. The existing generic _confirm_dialog remains dedicated to delete/clear confirmations; file flow uses separate dialogs and callbacks.
File menu items:
| Item | Contract |
|---|---|
| New | Dirty-check, then reset content/name/path/camera. |
| Open… | Dirty-check, then show SceneOpenDialog. |
| Open Recent | Submenu, most recent first; missing paths are pruned when rebuilt. |
| Save | Save to _current_scene_path, or open Save As when empty. |
| Save As… | Open SceneSaveDialog; append .scene if the selected path has another/no extension. |
| Rename Scene… | Opens a wide ConfirmationDialog with Cancel, pre-fills SceneNameEdit, focuses it, and selects all text. The LineEdit uses the placeholder Scene name, which is visible only while the field is empty. A changed, non-empty confirmed name marks dirty. |
There is no separate Clear item: the live sandbox has no existing Clear command, and New already supplies the destructive blank-stage behavior.
The Open and Save dialogs use ACCESS_FILESYSTEM and filter *.scene ; Stickman Scene Files. Keyboard accelerators may be added for Ctrl+N/O/S and Ctrl+Shift+S, but are not required for Phase 5 acceptance.
_update_scene_chrome() sets:
- window title:
"%s%s — %s — Stickman Studio" % [_scene_name, " *" if _dirty else "", scene_file]throughget_window().title, wherescene_fileis the current.scenebasename orUnsaved; - status prefix:
"%s | Objects: ...", retaining the existing selection, pending-action, rule-hint, toast, and cursor behavior.
8. Dirty-state and unsaved-changes flow
Dirty becomes true on:
- successful object placement, terrain paint commit, transform commit, or deletion;
- any rig
queue_changedsignal; - rule create/update/delete/clear/reorder;
- a committed scene rename.
Camera pan/zoom, selection/hover, grid/snap settings, mode changes, Play physics, and opening/canceling an editor do not dirty the file.
Replace the current rig queue connection to StageDirectorVisuals.mark_dirty with a stage handler that calls both mark_dirty() and _mark_scene_dirty().
Unsaved protection applies before New, Open, Open Recent, and window close. UnsavedChangesDialog exposes Save, Discard, and Cancel:
- Save performs Save or Save As. The pending action continues only after a successful write. Canceling Save As or a write failure cancels the pending action.
- Discard hides the unsaved-changes dialog and continues the pending action without writing. New therefore clears the stage and closes the prompt in the same operation.
- Cancel/window-close clears the pending action and changes nothing.
Only one pending action exists at a time. For close interception, set get_tree().auto_accept_quit = false in _ready() and handle NOTIFICATION_WM_CLOSE_REQUEST. A clean close, successful save-then-close, or explicit discard-then-close retires the autosave before get_tree().quit().
Dirty is cleared only by successful manual Save/Save As, successful ordinary load, and New. Autosave never changes _current_scene_path, recents, timestamps held for the manual document, or _dirty.
9. New and load transaction
_reset_scene_content() first returns to Edit semantics so queues/ragdolls/physics are stopped, then closes transient popups and capture flows, clears selection and authored physics targets, removes all direct World children, and clears:
_authored,_event_rules,_next_rule_id;_area_overlap,_collision_pairs, terrain occupancy, director visuals;- selection/hover/gizmo targets and placement ghosts.
New additionally sets Untitled, empty current path, a new creation timestamp, the default camera, clean state, then rebuilds the grid/navigation/status once.
Ordinary load is transactional at the document boundary:
- Read, parse, root-validate, and migrate before touching the live stage.
- If root validation fails, show one error and keep the current document unchanged.
- Reset the live content under
_suppress_dirty. - Spawn valid objects in saved array order. Apply transform/properties before calling the existing
_finalize_placed_node()wiring. - Build
file_id -> runtime instance ID; track missing-stickman IDs separately. - Decode/install only rules whose references resolve.
- Restore camera, name, creation timestamp, authored transforms,
_next_rule_id, grid occupancy, and director visuals. Clear any queued_nav_dirtyrequest, then rebuild navigation exactly once. - Ordinary load adopts the normalized source path, clears dirty, adds it to recents, and updates title/status.
Object-level warnings are summarized in one toast after load; details also go to push_warning. A partially valid document therefore loads without turning one bad entry into total data loss.
10. Recent files and settings merge
Extend the existing user://sandbox_settings.json rather than creating another settings file:
{
"version": "1.0",
"grid_size": 15.0,
"snap_to_grid": false,
"show_grid": true,
"recent_scenes": ["C:/Scenes/playground.scene"]
}
_load_settings() type-checks the list, normalizes/deduplicates paths, removes missing files, and caps it at five. _save_settings() writes both existing grid fields and recent scenes so one settings change cannot erase the other feature's state.
A successful manual open or save moves that path to the front. Autosave is never added. The submenu rebuild removes newly missing paths and displays one disabled (No recent scenes) row when empty. If a recent file disappears between menu rebuild and click, show File not found: <path>, remove it, save settings, and leave the current document untouched.
11. Missing or corrupt .stk files
Add MissingStickmanPlaceholder with:
var original_entry: Dictionary = {}
var missing_stk_path: String = ""
func configure(entry: Dictionary) -> void
func get_area_rect() -> Rect2
It draws a red rectangle labeled Missing asset - <filename> at local origin. get_area_rect() makes it selectable by the current duck-typed stage bounds path. It is a normal World child for movement, rotation, and deletion, but it has no rig/action/event behavior.
When a stickman asset is absent or fails JSON parsing:
- Log the detailed failure.
- Spawn a placeholder at the saved authored transform.
- Store a deep copy of the original stickman entry, including its
stk_path, facing, and action queue. - Mark its
file_idunavailable when decoding rules, so every rule that refers to that missing rig is skipped as a whole. - Show one end-of-load toast with missing-stickman and skipped-rule counts.
- Show one
MissingAssetsDialoglisting each distinct missing asset filename and saved path, with a note that red markers identify their scene positions.
Saving a placeholder serializes it back as a type: "stickman" entry using original_entry, updated with the placeholder's current file ID and authored transform. Thus the missing path and queue survive and can recover automatically if the .stk later returns. The placeholder is not serialized as a new fifth object type. Deleting the placeholder and saving removes that stickman entry.
12. Autosave-on-Play and recovery lifecycle
Before set_mode(StageMode.PLAY) changes mode or unfreezes physics, write the authored scene snapshot to user://sandbox_autosave.scene. Failure shows a toast and logs a warning but does not block Play.
Autosave write rules:
- use the same v1.0 schema and failure-safe writer;
- set
metadata.autosave = true,metadata.source_path = _current_scene_path, and a freshmodified_unix; - do not clear dirty, update recents, adopt the autosave path, or change the manual document's creation/modified bookkeeping.
On _ready(), after UI construction and settings load, inspect the autosave. Offer recovery only when it is valid and either has no source path, its source is missing, or its modified_unix is newer than the source file's filesystem modification time. A stale/invalid autosave is removed with a warning.
- Recover applies the snapshot without adding the autosave path to recents. It restores
metadata.source_pathas_current_scene_pathonly when non-empty, and sets_dirty = trueso the recovered snapshot must be manually saved. Keep the autosave until that manual save/New/Open/discard/clean shutdown retires it, protecting a second crash before save. - Decline deletes the autosave and keeps the new blank stage.
- A successful manual save retires the autosave.
- A normal confirmed shutdown retires it. A crash/forced process stop leaves it behind, which is what makes the next-launch prompt meaningful.
This lifecycle prevents a normal Play followed by an orderly exit from prompting on every next launch.
13. Signals and theme impact
No new public gameplay signal is required. Existing stage signals keep their current payloads and behavior. Loading should not emit object_placed once per reconstructed entry; it applies a document snapshot under _suppress_dirty and refreshes dependents once at the end.
The file controls use the existing stage font and popup-theme helpers. No new sandbox_theme.json key is required. Dialog buttons use Godot's standard dialog theme.
14. Verification strategy
Create tests/test_phase5_file_features.gd and run it headlessly with the project's established test form:
& "C:\Godot4\Godot_v4.7.1-stable_win64_console.exe" --headless --path "C:\Godot4\stickman" --script "res://tests/test_phase5_file_features.gd"
Required automated coverage:
- Vector/action/rule codecs emit only JSON-safe types and round-trip every current action/trigger type.
- Runtime instance IDs encode to file IDs and decode to new live instance IDs, including wildcard
null <-> -1. - Terrain/prop/stickman/area provenance, authored position/rotation, non-rig scale, area size, named material/facing enums, queues, rule order, and World order survive save/load.
- A LEFT-facing rig loads with one mirror only; its derived root scale is not serialized.
- Save while/after Play uses
_authored, not displaced physics state. - Parse/root-validation failure does not clear the existing World.
- Empty/duplicate file IDs, unknown object IDs, non-finite numbers/vectors, and malformed entries skip safely; navigation is rebuilt once.
- Missing/corrupt
.stkcreates a selectable red labeled placeholder, shows a load-error dialog listing the missing asset(s), skips dependent rules, and re-save preserves the original stickman entry and queue. - Place/transform/delete, queue edits, rule edits/reorder, and rename dirty the scene; camera/grid/selection/mode do not. Load-time reconstruction remains clean.
- Rename focuses and selects all text, exposes Cancel, and uses an empty-field-only placeholder hint. Save-via-Save-As continuation and Save/Discard/Cancel pending-action paths behave correctly, including canceled/failed saves and a closed prompt after Discard.
- Recents deduplicate, reorder, cap at five, prune missing paths, and never include autosave.
- Autosave does not clear dirty or adopt a path; stale autosaves are pruned, recovery is dirty, decline/manual save/orderly quit retire the file.
- Safe-write replacement, rollback, and interrupted-backup recovery leave the prior destination recoverable and report failures.
After the focused suite passes, run every existing script under tests/ using its documented headless command; report suite names/results rather than relying on the stale aggregate assertion counts in older plans.
Manual Godot Editor verification:
- exercise File menu and all dialogs, including title
scene name * — scene-file.scene — Stickman Studio, rename focus/select-all/placeholder behavior, cancel paths, and keyboard/window close; - create all four object types, actions, cross-object rules, transforms, save, close, reload, and Play;
- rename/move a referenced
.stk, verify the load-error dialog lists its filename/path and the scene shows a redMissing asset - <filename>rectangle, save, restore the.stk, and reload; - force-stop after Play to verify recovery, then exit normally after Play to verify no false recovery prompt.
15. Approval gate: decisions to accept before implementation
The architect recommends approving these as a set:
- Keep
.sceneas the user-facing JSON extension and v1.0 as the first file version. - Use per-save readable file IDs and a strict runtime-ID/file-ID conversion boundary; never persist Godot instance IDs.
- Add typed provenance fields to
PropBlockandStickmanRig, plus load-safeStageSpawnerAPIs, instead of relying on mutable palette selections or ad-hoc node metadata. - Omit a redundant Clear command; New is the blank-stage destructive action.
- Preserve missing stickmen as stickman entries through a selectable placeholder, but drop complete rules that reference an unavailable rig.
- Keep one autosave snapshot, write it on Play, retain it only across unclean exits/recovery, and mark recovered work dirty.
- Drop unknown keys on re-save and reject newer unsupported versions; do not attempt forward-compatible preservation in v1.0.
- Preserve saved World order and rebuild navigation once after load rather than grouping terrain first.
16. Out of scope
- Undo/redo
- Scene thumbnails/library/browser/templates
- Periodic or rotating autosave history
- Bundling/embedding
.stkcontents into.scene - Relinking a missing
.stkfrom the placeholder UI - Cloud sharing/collaboration
- Video/GIF export
Implementation and focused headless validation are complete; the user confirmed the feature is verified and working, so this plan records the final behavior for handoff.