Add Use Spot Overlay and Target Capture Bar functionality
- Introduced `StageUseTargetCaptureBar` class for managing user interactions with props and their use spots. - Implemented UI elements including buttons for changing props and canceling actions. - Added `PropUseSpotData` class for validation and serialization of prop use spots. - Created tests for validating use spot data migration, serialization, and rule matching. - Ensured proper handling of edge cases in use spot data, including unique IDs and finite positions.
This commit is contained in:
@@ -76,7 +76,7 @@ func _test_legacy_migration() -> void:
|
||||
var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(legacy)
|
||||
var prop_entry: Dictionary = migrated["objects"][1]
|
||||
var roles: Dictionary = prop_entry["roles"]
|
||||
_expect(migrated["version"] == "1.2", "old stage version migrates to 1.2")
|
||||
_expect(migrated["version"] == "1.3", "old stage version migrates to 1.3")
|
||||
_expect(prop_entry["stage_object_id"] == "prop_a", "1.0 prop IDs migrate from file IDs")
|
||||
_expect(String(roles["start_motion"]) == PROP_STAGE_ROLES.START_PHYSICS, "legacy prop keeps dynamic start behavior")
|
||||
_expect(bool(roles["blocks_stickmen"]) and bool(roles["legacy_draw_order"]), "legacy prop keeps collisions and draw order")
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
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"]) == "1.3" and entry.get("use_spots") == [] and int(entry.get("next_use_spot_number", 0)) == 1, "%s prop receives v1.3 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": "1.3",
|
||||
"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,
|
||||
}],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://ckpf6m2c4knov
|
||||
Reference in New Issue
Block a user