feat: Integrate custom prop library and sandbox functionality

- Added support for custom props in the sandbox, including loading, saving, and rendering.
- Implemented `PropLibrary` for managing custom prop entries and scanning for `.prop` files.
- Introduced `PropVisuals` for shared visual and collision representation of custom props.
- Enhanced `StageSpawner` to handle spawning of custom props with layered visuals and collision hulls.
- Created `MissingPropPlaceholder` to manage missing custom props during scene reloads.
- Updated thumbnail generation to support custom props with unique keys based on file paths.
- Added tests for custom prop lifecycle, authored visual values, and missing prop handling.
- Refactored existing code to accommodate new prop functionalities and ensure backward compatibility.
This commit is contained in:
2026-09-21 23:13:35 -04:00
parent 00abacc7b2
commit 97f3730413
19 changed files with 581 additions and 23 deletions
+17 -2
View File
@@ -165,8 +165,17 @@ func spawn_prop_asset(asset_id: String, object_position: Vector2) -> PropBlock:
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
var prop: PropBlock
if bool(template.get("is_custom", false)):
prop = PROP_BLOCK.new()
prop.name = "PropBlock"
prop.position = object_position
prop.material_preset = int(template["material_preset"])
_world.add_child(prop)
prop.set_custom_shapes(template.get("shapes", []) as Array[Dictionary], template.get("collision_hull", PackedVector2Array()) as PackedVector2Array)
else:
prop = PROP_UTILS.spawn_prop(_world, object_position, template["payload"], int(template["material_preset"]), Vector2.ZERO)
prop.spawn_id = String(template.get("id", asset_id))
return prop
@@ -202,6 +211,12 @@ static func get_world_aabb(node: Node2D) -> Rect2:
# Duck-typed TriggerArea: expose its centered local rect through the transform.
if node.has_method("get_area_rect"):
return node.global_transform * (node.call("get_area_rect") as Rect2)
var visuals := node.get_node_or_null(NodePath("Visuals")) as Node2D
if visuals != null:
var visual_acc := { "min_x": INF, "min_y": INF, "max_x": -INF, "max_y": -INF }
_collect_visual_points(visuals, visual_acc)
if visual_acc["min_x"] <= visual_acc["max_x"]:
return Rect2(Vector2(visual_acc["min_x"], visual_acc["min_y"]), Vector2(visual_acc["max_x"] - visual_acc["min_x"], visual_acc["max_y"] - visual_acc["min_y"]))
var poly := node.get_node_or_null(NodePath("Polygon2D")) as Polygon2D
if poly != null and not poly.polygon.is_empty():
var rect := Rect2(node.to_global(poly.polygon[0]), Vector2.ZERO)