139 lines
9.2 KiB
GDScript
139 lines
9.2 KiB
GDScript
extends SceneTree
|
|
|
|
const PROP_LIBRARY := preload("res://scripts/props/prop_library.gd")
|
|
const STAGE_SPAWNER := preload("res://scripts/stage/stage_spawner.gd")
|
|
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
|
|
const STAGE_SELECTION := preload("res://scripts/stage/stage_selection.gd")
|
|
const THUMBNAIL_CACHE := preload("res://scripts/shared/thumbnails/thumbnail_cache.gd")
|
|
const PROP_FILE := preload("res://scripts/editors/prop/prop_file.gd")
|
|
const STAGE_SCENE := preload("res://scenes/stage/sandbox_stage.tscn")
|
|
const MISSING_PROP_PLACEHOLDER := preload("res://scripts/props/missing_prop_placeholder.gd")
|
|
|
|
const TEMP_PROP_PATH := "user://phase10_custom_integration.prop"
|
|
|
|
var failures: int = 0
|
|
var checks: int = 0
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("_run")
|
|
|
|
func _run() -> void:
|
|
_write_temp_prop()
|
|
var builtins: Array[Dictionary] = PROP_LIBRARY.get_entries()
|
|
_check(builtins.size() == 4, "built-in templates remain four entries")
|
|
var entries: Array[Dictionary] = PROP_LIBRARY.scan()
|
|
var scanned_briefcase: bool = false
|
|
for scanned_entry: Dictionary in entries:
|
|
if String(scanned_entry.get("path", "")) == "res://content/props/briefcase.prop":
|
|
scanned_briefcase = true
|
|
_check(entries.size() >= builtins.size() + 1 and scanned_briefcase, "scan returns built-ins and checked-in custom props")
|
|
var entry: Dictionary = PROP_LIBRARY.get_entry("res://content/props/briefcase.prop")
|
|
_check(not entry.is_empty() and bool(entry.get("is_custom", false)), "custom path resolves without selector scan")
|
|
_check((entry.get("shapes", []) as Array).size() > 1, "custom entry preserves the full shape stack")
|
|
var hull: PackedVector2Array = entry.get("collision_hull", PackedVector2Array())
|
|
_check(hull.size() >= 3, "custom entry has a valid collision hull")
|
|
var world: Node2D = Node2D.new()
|
|
root.add_child(world)
|
|
var spawner: StageSpawner = STAGE_SPAWNER.new(world)
|
|
var prop: PropBlock = spawner.spawn_prop_asset("res://content/props/briefcase.prop", Vector2(100, 50))
|
|
_check(prop != null and prop.has_custom_shapes(), "spawner creates layered custom PropBlock")
|
|
if prop != null:
|
|
var visuals: Node2D = prop.get_node_or_null(NodePath("Visuals")) as Node2D
|
|
_check(visuals != null and visuals.get_child_count() == (entry.get("shapes", []) as Array).size(), "each authored shape has one visual group")
|
|
var collision: CollisionPolygon2D = prop.get_node_or_null(NodePath("CollisionPolygon2D")) as CollisionPolygon2D
|
|
_check(collision != null and collision.polygon.size() >= 3 and not collision.disabled, "custom prop uses one enabled collision hull")
|
|
var aabb: Rect2 = STAGE_SELECTION.get_world_aabb(prop)
|
|
_check(aabb.size.x > 0.0 and aabb.size.y > 0.0, "selection bounds include custom visuals")
|
|
var cache: ThumbnailCache = THUMBNAIL_CACHE.new()
|
|
_check(cache.prop_file_png("res://content/props/briefcase.prop") != cache.prop_file_png("user://briefcase.prop"), "thumbnail keys include complete canonical path")
|
|
world.queue_free()
|
|
await process_frame
|
|
await _test_authored_visual_values()
|
|
await _test_missing_prop_round_trip()
|
|
await _test_custom_prop_lifecycle()
|
|
DirAccess.remove_absolute(TEMP_PROP_PATH)
|
|
print("RESULT: %d checks, %d failures" % [checks, failures])
|
|
quit(1 if failures > 0 else 0)
|
|
|
|
func _check(condition: bool, label: String) -> void:
|
|
checks += 1
|
|
if condition: print("PASS: " + label)
|
|
else:
|
|
failures += 1
|
|
print("FAIL: " + label)
|
|
|
|
|
|
func _write_temp_prop() -> void:
|
|
var shapes: Array[Dictionary] = [
|
|
{"shape_type": "polygon", "points": PackedVector2Array([Vector2(-30, -20), Vector2(30, -20), Vector2(30, 20), Vector2(-30, 20)]), "fill_color": "#123456FF", "outline_color": "#ABCDEF80", "outline_width": 3.5, "closed": true, "vertex_flags": PackedInt32Array([0, 0, 0, 0]), "reshaped": false},
|
|
{"shape_type": "line", "points": PackedVector2Array([Vector2(-45, 35), Vector2(45, 35)]), "fill_color": "#00000000", "outline_color": "#FEDCBAFF", "outline_width": 5.0, "closed": false, "vertex_flags": PackedInt32Array([0, 0]), "reshaped": false},
|
|
]
|
|
_check(PROP_FILE.save(TEMP_PROP_PATH, PROP_FILE.to_file_dict("Temporary Test Prop", PROP_BLOCK.MaterialPreset.METAL, shapes)), "temporary custom prop writes")
|
|
|
|
|
|
func _test_authored_visual_values() -> void:
|
|
var world: Node2D = Node2D.new()
|
|
root.add_child(world)
|
|
var spawner: StageSpawner = STAGE_SPAWNER.new(world)
|
|
var prop: PropBlock = spawner.spawn_prop_asset(TEMP_PROP_PATH, Vector2.ZERO)
|
|
_check(prop != null and prop.has_custom_shapes(), "temporary custom prop spawns as layered visual")
|
|
if prop != null:
|
|
var shape_zero: Node2D = prop.get_node_or_null(NodePath("Visuals/Shape_0")) as Node2D
|
|
var fill: Polygon2D = shape_zero.get_node_or_null(NodePath("Polygon2D")) as Polygon2D
|
|
var closed_outline: Line2D = shape_zero.get_node_or_null(NodePath("Line2D")) as Line2D
|
|
_check(fill != null and fill.color.is_equal_approx(Color("#123456FF")), "custom fill retains authored color")
|
|
_check(closed_outline != null and closed_outline.default_color.is_equal_approx(Color("#ABCDEF80")) and is_equal_approx(closed_outline.width, 3.5), "custom outline retains authored color and width")
|
|
_check(closed_outline != null and closed_outline.points.size() == 5 and closed_outline.points[0].is_equal_approx(Vector2(-30, -20)) and closed_outline.points[4].is_equal_approx(Vector2(-30, -20)), "closed shape outline repeats authored first point")
|
|
var line_outline: Line2D = prop.get_node_or_null(NodePath("Visuals/Shape_1/Line2D")) as Line2D
|
|
_check(line_outline != null and line_outline.points.size() == 2 and line_outline.points[0].is_equal_approx(Vector2(-45, 35)) and line_outline.points[1].is_equal_approx(Vector2(45, 35)) and line_outline.default_color.is_equal_approx(Color("#FEDCBAFF")), "open line retains authored points and outline")
|
|
world.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _test_missing_prop_round_trip() -> void:
|
|
var stage: Node2D = STAGE_SCENE.instantiate() as Node2D
|
|
root.add_child(stage)
|
|
await process_frame
|
|
var source: Dictionary = {"version": "1.0", "scene_name": "Missing Custom", "camera": {}, "metadata": {}, "objects": [{"file_id": "prop_1", "type": "prop", "asset_id": TEMP_PROP_PATH, "position": {"x": 42.0, "y": 75.0}, "rotation": 15.0, "scale": {"x": 1.2, "y": 0.8}, "material_preset": PROP_BLOCK.MaterialPreset.WOOD}], "event_rules": [{"id": 1, "trigger": {"type": "action_finished", "source_id": "prop_1", "target_id": null, "params": {}}, "actions": [{"type": "wait", "target_id": null, "params": {"duration": 0.1}}]}]}
|
|
DirAccess.remove_absolute(TEMP_PROP_PATH)
|
|
_check(stage._apply_scene_data(source, "user://phase10_missing.scene", false), "missing custom-prop scene loads")
|
|
var placeholder: Node2D = stage._world.get_child(0) as Node2D
|
|
_check(placeholder != null and placeholder.get_script() == MISSING_PROP_PLACEHOLDER, "missing custom prop becomes placeholder")
|
|
var preserved: Dictionary = stage._collect_scene_data()
|
|
var object_entry: Dictionary = (preserved.get("objects", []) as Array)[0]
|
|
_check(String(object_entry.get("asset_id", "")) == TEMP_PROP_PATH and (preserved.get("event_rules", []) as Array).size() == 1 and String(((preserved["event_rules"] as Array)[0]["trigger"] as Dictionary).get("source_id", "")) == "prop_1", "placeholder save preserves asset path and rule reference")
|
|
_write_temp_prop()
|
|
_check(stage._apply_scene_data(preserved, "user://phase10_missing.scene", false), "scene reloads after custom prop is restored")
|
|
var restored: PropBlock = stage._world.get_child(0) as PropBlock
|
|
_check(restored != null and restored.has_custom_shapes() and stage._event_rules.size() == 1 and int((stage._event_rules[0]["trigger"] as Dictionary).get("source", -1)) == restored.get_instance_id(), "restored custom prop replaces placeholder and resolves rule")
|
|
stage.queue_free()
|
|
await process_frame
|
|
|
|
|
|
func _test_custom_prop_lifecycle() -> void:
|
|
var stage: Node2D = STAGE_SCENE.instantiate() as Node2D
|
|
root.add_child(stage)
|
|
await process_frame
|
|
var prop: PropBlock = stage._spawner.spawn_prop_asset(TEMP_PROP_PATH, Vector2(160, -40))
|
|
prop.rotation = deg_to_rad(20.0)
|
|
prop.scale = Vector2(1.5, 1.5)
|
|
stage._finalize_placed_node(prop)
|
|
var authored_position: Vector2 = prop.global_position
|
|
var authored_rotation: float = prop.global_rotation
|
|
var authored_scale: Vector2 = prop.scale
|
|
_check(prop.freeze, "custom prop is frozen in EDIT")
|
|
stage.set_mode(stage.StageMode.PLAY)
|
|
_check(prop.freeze and prop.runtime_role == PROP_BLOCK.RuntimeRole.START_STILL, "new custom prop starts still in PLAY")
|
|
var play_scene: Dictionary = stage._collect_scene_data()
|
|
var play_entry: Dictionary = (play_scene.get("objects", []) as Array)[0]
|
|
var saved_scale: Vector2 = Vector2(float((play_entry.get("scale", {}) as Dictionary).get("x", 0.0)), float((play_entry.get("scale", {}) as Dictionary).get("y", 0.0)))
|
|
_check(saved_scale.is_equal_approx(authored_scale), "save during PLAY preserves authored prop scale")
|
|
prop.global_position += Vector2(90, 70)
|
|
prop.global_rotation += 0.4
|
|
prop.linear_velocity = Vector2(50, 20)
|
|
prop.angular_velocity = 2.0
|
|
stage.set_mode(stage.StageMode.EDIT)
|
|
_check(prop.freeze and prop.global_position.is_equal_approx(authored_position) and is_equal_approx(prop.global_rotation, authored_rotation) and prop.scale.is_equal_approx(authored_scale) and prop.linear_velocity.is_zero_approx() and is_zero_approx(prop.angular_velocity), "PLAY to EDIT restores authored pose, keeps scale, and clears velocity")
|
|
stage.queue_free()
|
|
await process_frame
|