Add missing stickman placeholder and stage scene I/O functionality

- Implemented `MissingStickmanPlaceholder` class to visually indicate missing stickman assets in the scene.
- Created `StageSceneIO` class for handling JSON serialization and deserialization of stage documents, including encoding and decoding of actions and rules.
- Added tests for vector handling, rule encoding/decoding, safe file writing, root validation, and handling of missing assets.
- Introduced a new stickman asset file `bad.stk` for testing purposes.
- Enhanced test coverage for phase 5 file features, ensuring robust handling of scene data and missing assets.
This commit is contained in:
2026-09-14 15:45:32 -04:00
parent 3a1d522ec9
commit a7c58705e1
15 changed files with 3247 additions and 26 deletions
+48 -23
View File
@@ -127,18 +127,60 @@ func spawn(id: String, world_position: Vector2) -> Node2D:
var pos := world_position + offset
match String(entry.get("kind", "")):
"terrain":
return _spawn_terrain(entry, pos)
return spawn_terrain_asset(id, pos)
"prop":
return _spawn_prop(entry, pos)
return spawn_prop_asset(selected_prop_id, pos)
"stickman":
return _spawn_stickman(pos)
return spawn_stickman_asset(selected_stickman_path, pos)
"area":
return _spawn_area(pos)
return spawn_area(pos)
_:
push_warning("StageSpawner: unknown spawn kind '%s'." % entry.get("kind", ""))
return null
## Load-safe factories take real object origins and never apply palette offsets.
func spawn_terrain_asset(asset_id: String, object_position: Vector2) -> TerrainBlock:
var entry := _find_entry(asset_id)
if entry.is_empty() or String(entry.get("kind", "")) != "terrain":
push_warning("StageSpawner: unknown terrain '%s'." % asset_id)
return null
return _spawn_terrain(entry, object_position)
func spawn_prop_asset(asset_id: String, object_position: Vector2) -> PropBlock:
var template: Dictionary = PROP_LIBRARY.get_entry(asset_id)
if template.is_empty():
push_warning("StageSpawner: unknown prop '%s'." % asset_id)
return null
var prop: PropBlock = PROP_UTILS.spawn_prop(_world, object_position, template["payload"], int(template["material_preset"]), Vector2.ZERO)
prop.spawn_id = asset_id
return prop
func spawn_stickman_asset(stk_path: String, root_position: Vector2) -> StickmanRig:
var normalized := stk_path
var data: Dictionary = _stickman_cache.get(normalized, {})
if data.is_empty():
data = STICKMAN_FACTORY.load_stk(normalized)
_stickman_cache[normalized] = data
if data.is_empty():
push_warning("StageSpawner: no stickman data for '%s'." % normalized)
return null
var rig: StickmanRig = STICKMAN_FACTORY.spawn_from_data(data)
if rig == null:
push_warning("StageSpawner: failed to spawn stickman.")
return null
rig.position = root_position
rig.source_stk_path = normalized
_world.add_child(rig)
return rig
func spawn_area(object_position: Vector2) -> TriggerArea:
return _spawn_area(object_position) as TriggerArea
## World-space AABB for a spawnable node: union of the Polygon2D child's world
## points; for a stickman rig, union of its mounted `Body/*` shape geometry; a
## point rect otherwise.
@@ -257,11 +299,7 @@ func _spawn_terrain(entry: Dictionary, world_position: Vector2) -> TerrainBlock:
func _spawn_prop(entry: Dictionary, world_position: Vector2) -> PropBlock:
var t: Dictionary = PROP_LIBRARY.get_entry(selected_prop_id)
if t.is_empty():
push_warning("StageSpawner: unknown selected prop '%s'." % selected_prop_id)
return null
return PROP_UTILS.spawn_prop(_world, world_position, t["payload"], int(t["material_preset"]), Vector2.ZERO)
return spawn_prop_asset(String(entry.get("id", selected_prop_id)), world_position)
func _spawn_area(world_position: Vector2) -> Node2D:
@@ -273,20 +311,7 @@ func _spawn_area(world_position: Vector2) -> Node2D:
func _spawn_stickman(world_position: Vector2) -> StickmanRig:
var data: Dictionary = _stickman_cache.get(selected_stickman_path, {})
if data.is_empty():
data = STICKMAN_FACTORY.load_stk(selected_stickman_path)
_stickman_cache[selected_stickman_path] = data
if data.is_empty():
push_warning("StageSpawner: no stickman data for '%s'." % selected_stickman_path)
return null
var rig: StickmanRig = STICKMAN_FACTORY.spawn_from_data(data)
if rig == null:
push_warning("StageSpawner: failed to spawn stickman.")
return null
rig.position = world_position
_world.add_child(rig)
return rig
return spawn_stickman_asset(selected_stickman_path, world_position)
static func _points_center(pts: PackedVector2Array) -> Vector2: