Files
stickman/tests/test_phase10_prop_integration.gd
T

135 lines
8.9 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()
_check(entries.size() == 8, "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(not prop.freeze, "custom prop unfreezes 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