Files
stickman/tests/test_phase17b_use_spots.gd
T
ryan 59f1817063 Add StageBackgroundView and related tests for plain background functionality
- Implemented StageBackgroundView class for managing sky and plain backgrounds in the stage.
- Added functionality to set background type and color, bind camera, and apply themes.
- Created test_phase19_plain_background.gd to validate plain background behavior and migration.
- Developed test_phase19_starter_chooser_layout.gd to ensure proper layout and functionality of the starter chooser.
- Included visual capture tests for UI elements and interactions.
2026-10-22 16:18:25 -04:00

191 lines
10 KiB
GDScript

extends SceneTree
const PROP_USE_SPOT_DATA := preload("res://scripts/props/prop_use_spot_data.gd")
const PROP_BLOCK_SCRIPT := preload("res://scripts/props/prop_block.gd")
const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
const STAGE_RULE_ENGINE := preload("res://scripts/stage/stage_rule_engine.gd")
const PROP_INTERACTOR_SCRIPT := preload("res://scripts/characters/stickman/stickman_prop_interactor.gd")
const STICKMAN_RIG_SCENE := preload("res://scenes/characters/stickman/stickman_rig.tscn")
var _checks: int = 0
var _failures: int = 0
var _last_use_failure: String = ""
func _initialize() -> void:
call_deferred("_run")
func _run() -> void:
_test_migration_and_schema()
_test_spot_round_trip()
_test_action_and_trigger_references()
_test_rule_matching()
await _test_coordinates_and_empty_hands()
print("----------------------------------------")
if _failures == 0:
print("RESULT: ALL PASSED (%d checks)" % _checks)
quit(0)
else:
print("RESULT: %d FAILURE(S) out of %d checks" % [_failures, _checks])
quit(1)
func _expect(condition: bool, message: String) -> void:
_checks += 1
if condition:
print("PASS: " + message)
else:
_failures += 1
push_error("Phase 17b check failed: " + message)
func _test_migration_and_schema() -> void:
for version: String in ["1.0", "1.1", "1.2"]:
var legacy: Dictionary = {"version": version, "objects": [{"file_id": "prop_a", "type": "prop"}]}
var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(legacy)
var entry: Dictionary = migrated["objects"][0]
_expect(String(migrated["version"]) == STAGE_SCENE_IO.FILE_VERSION and entry.get("use_spots") == [] and int(entry.get("next_use_spot_number", 0)) == 1, "%s prop receives current-format empty Use spot defaults" % version)
var valid_root: Dictionary = _valid_root()
_expect(bool(STAGE_SCENE_IO._validate_root(valid_root).get("ok", false)), "valid Use spot data passes root validation")
var duplicate: Dictionary = _valid_root()
duplicate["objects"][0]["use_spots"] = [
{"id": "spot_1", "number": 1, "name": "A", "position": {"x": 2.0, "y": 3.0}},
{"id": "spot_1", "number": 1, "name": "B", "position": {"x": 4.0, "y": 5.0}},
]
duplicate["objects"][0]["next_use_spot_number"] = 2
_expect(not bool(STAGE_SCENE_IO._validate_root(duplicate).get("ok", false)), "duplicate Use spot IDs and numbers are rejected")
var nonfinite: Dictionary = _valid_root()
nonfinite["objects"][0]["use_spots"][0]["position"] = {"x": INF, "y": 0.0}
_expect(not bool(STAGE_SCENE_IO._validate_root(nonfinite).get("ok", false)), "nonfinite Use spot coordinates are rejected")
var bad_counter: Dictionary = _valid_root()
bad_counter["objects"][0]["next_use_spot_number"] = 1
_expect(not bool(STAGE_SCENE_IO._validate_root(bad_counter).get("ok", false)), "Use spot counter must be greater than stored numbers")
func _test_spot_round_trip() -> void:
var spots: Array[Dictionary] = [{"id": "spot_2", "number": 2, "name": "Button", "position": Vector2(24.5, -18.25)}]
var encoded: Array[Dictionary] = PROP_USE_SPOT_DATA.encode(spots)
var decoded: Array[Dictionary] = PROP_USE_SPOT_DATA.decode(encoded)
_expect(encoded[0]["position"] == {"x": 24.5, "y": -18.25}, "spot serializer writes local point coordinates")
_expect(decoded[0]["position"] == Vector2(24.5, -18.25), "spot decoder restores nonzero vector coordinates")
_expect(String(decoded[0]["id"]) == "spot_2" and int(decoded[0]["number"]) == 2, "spot serializer preserves stable identity")
func _test_action_and_trigger_references() -> void:
var action: Dictionary = {"type": "use_prop", "target_prop_id": "prop_missing", "target_spot_id": "spot_9"}
var encoded_queue: Dictionary = STAGE_SCENE_IO.encode_queue([action])
var decoded_queue: Dictionary = STAGE_SCENE_IO.decode_queue(encoded_queue["actions"])
var queue_action: Dictionary = decoded_queue["actions"][0]
_expect(queue_action == action, "Use queue action preserves missing stable prop and spot IDs for repair")
var flat_action: Dictionary = action.duplicate(true)
var consequence: Dictionary = ACTION_REGISTRY.to_rule_action(flat_action, 42)
var rule: Dictionary = {
"id": 3,
"trigger": {"type": "prop_used", "source": 42, "target": -1, "params": {"target_prop_id": "prop_missing", "spot_scope": "specific", "target_spot_id": "spot_9"}},
"actions": [consequence],
}
var encoded_rules: Dictionary = STAGE_SCENE_IO.encode_rules([rule], {42: "actor_a"})
var encoded_trigger: Dictionary = encoded_rules["rules"][0]["trigger"]
_expect(encoded_trigger["source_id"] == "actor_a" and encoded_trigger["target_id"] == null, "Used prop saves actor source and null target")
var decoded_rules: Dictionary = STAGE_SCENE_IO.decode_rules(encoded_rules["rules"], {"actor_a": 42}, {})
var decoded_trigger: Dictionary = decoded_rules["rules"][0]["trigger"]
_expect(String(decoded_trigger["params"]["target_prop_id"]) == "prop_missing" and String(decoded_trigger["params"]["target_spot_id"]) == "spot_9", "Used prop round-trip preserves repairable stable target IDs")
var invalid_target: Dictionary = rule.duplicate(true)
invalid_target["trigger"]["target"] = 88
_expect(STAGE_SCENE_IO.encode_rules([invalid_target], {42: "actor_a"})["rules"].is_empty(), "Used prop in-memory target must be exactly -1")
var saved_bad_target: Dictionary = encoded_rules["rules"][0].duplicate(true)
saved_bad_target["trigger"]["target_id"] = "other_prop"
_expect(STAGE_SCENE_IO.decode_rules([saved_bad_target], {"actor_a": 42, "other_prop": 99}, {}).get("rules", []).is_empty(), "Used prop saved target_id must be null")
var any_with_spot: Dictionary = rule.duplicate(true)
any_with_spot["trigger"]["params"] = {"target_prop_id": "prop_missing", "spot_scope": "any", "target_spot_id": "spot_9"}
_expect(STAGE_SCENE_IO.encode_rules([any_with_spot], {42: "actor_a"})["rules"].is_empty(), "Any Used prop trigger omits a specific spot ID")
func _test_rule_matching() -> void:
var actor: Node2D = Node2D.new()
var prop: PropBlock = PROP_BLOCK_SCRIPT.new()
actor.name = "UseActor"
prop.stage_object_id = "prop_live"
root.add_child(actor)
root.add_child(prop)
var engine: StageRuleEngine = STAGE_RULE_ENGINE.new()
var base: Dictionary = {"type": "prop_used", "source": actor.get_instance_id(), "target": -1, "params": {"target_prop_id": "prop_live", "spot_scope": "specific", "target_spot_id": "spot_2"}}
var rule: Dictionary = {"id": 1, "trigger": base, "actions": []}
var event: Dictionary = {"type": "prop_used", "source": actor, "target": prop, "spot_id": "spot_2"}
_expect(engine.matches_rule(rule, event), "specific Used prop rule matches exact live prop and spot")
event["spot_id"] = "spot_1"
_expect(not engine.matches_rule(rule, event), "specific Used prop rule rejects a different spot")
var any_rule: Dictionary = rule.duplicate(true)
any_rule["trigger"]["params"] = {"target_prop_id": "prop_live", "spot_scope": "any"}
event["spot_id"] = "spot_1"
_expect(engine.matches_rule(any_rule, event), "Any Used prop rule matches another spot on the same prop")
any_rule["trigger"]["params"]["target_prop_id"] = "prop_missing"
_expect(not engine.matches_rule(any_rule, event), "Used prop wildcard target does not match a different prop")
actor.queue_free()
prop.queue_free()
func _test_coordinates_and_empty_hands() -> void:
var prop: PropBlock = PROP_BLOCK_SCRIPT.new()
prop.shape_type = PropBlock.ShapeType.POLYGON
prop.polygon_points = PackedVector2Array([Vector2(-40.0, -30.0), Vector2(40.0, -30.0), Vector2(40.0, 30.0), Vector2(-40.0, 30.0)])
prop.position = Vector2(180.0, 90.0)
prop.rotation = deg_to_rad(27.0)
prop.scale = Vector2(-1.5, 1.25)
root.add_child(prop)
await process_frame
var spot_id: String = prop.add_use_spot(Vector2(12.0, -8.0))
var expected_world: Vector2 = prop.global_transform * Vector2(12.0, -8.0)
_expect(spot_id == "spot_1" and prop.use_spot_world_position(spot_id).is_equal_approx(expected_world), "spot follows prop position, rotation, and signed EDIT scale")
var inverse_point: Vector2 = prop.use_spot_local_from_world(expected_world)
_expect(inverse_point.is_equal_approx(Vector2(12.0, -8.0)), "world authoring coordinate converts back to prop-local space")
prop.begin_play_instance_scale()
var play_expected: Vector2 = prop.global_transform * (Vector2(12.0, -8.0) * Vector2(-1.5, 1.25))
_expect(prop.use_spot_world_position(spot_id).is_equal_approx(play_expected), "signed authored scale is baked into PLAY spot position")
prop.end_play_instance_scale()
_expect(prop.scale.is_equal_approx(Vector2(-1.5, 1.25)), "STOP restores authored prop scale")
var rig: StickmanRig = STICKMAN_RIG_SCENE.instantiate() as StickmanRig
root.add_child(rig)
await process_frame
var interactor: Node = rig.get_node("RigComponents/PropInteractor")
var held: PropBlock = PROP_BLOCK_SCRIPT.new()
root.add_child(held)
interactor.set("_held_prop", held)
prop.apply_runtime_role(PropBlock.RuntimeRole.START_PHYSICS)
prop.linear_velocity = Vector2(31.0, -4.0)
var start_freeze: bool = prop.freeze
var start_layer: int = prop.collision_layer
var start_mask: int = prop.collision_mask
interactor.connect("use_failed", Callable(self, "_on_use_failed"))
var started: bool = bool(interactor.call("start_action", {"type": "use_prop", "target_prop_id": prop.stage_object_id, "target_spot_id": spot_id}))
_expect(not started and _last_use_failure == "hands_full", "Use rejects an actor with full hands")
_expect(prop.runtime_role == PropBlock.RuntimeRole.START_PHYSICS and prop.freeze == start_freeze and prop.collision_layer == start_layer and prop.collision_mask == start_mask, "failed Use leaves target physics profile unchanged")
_expect(prop.linear_velocity.is_equal_approx(Vector2(31.0, -4.0)), "failed Use does not alter target velocity")
held.queue_free()
rig.queue_free()
prop.queue_free()
await process_frame
func _on_use_failed(reason_code: String, _prop: PropBlock, _spot_id: String) -> void:
_last_use_failure = reason_code
func _valid_root() -> Dictionary:
return {
"version": STAGE_SCENE_IO.FILE_VERSION,
"sky_id": "sky_neutral",
"background_kind": "sky",
"plain_color": STAGE_SCENE_IO.DEFAULT_PLAIN_COLOR,
"objects": [{
"file_id": "prop_file",
"type": "prop",
"stage_object_id": "prop_live",
"use_spots": [{"id": "spot_1", "number": 1, "name": "Button", "position": {"x": 24.0, "y": -18.0}}],
"next_use_spot_number": 2,
}],
}