feat(stage): complete phase 20b action plans
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
extends SceneTree
|
||||
|
||||
const STAGE_ACTION_PLAN := preload("res://scripts/stage/stage_action_plan.gd")
|
||||
const STAGE_ACTION_IDENTITY := preload("res://scripts/stage/stage_action_identity.gd")
|
||||
const STAGE_SCENE_IO := preload("res://scripts/stage/stage_scene_io.gd")
|
||||
const ACTION_REGISTRY := preload("res://scripts/stage/action_registry.gd")
|
||||
const ACTION_RUNNER := preload("res://scripts/characters/stickman/stickman_action_runner.gd")
|
||||
const STICKMAN_RIG_SCENE := preload("res://scenes/characters/stickman/stickman_rig.tscn")
|
||||
const STICKMAN_RIG := preload("res://scripts/characters/stickman/stickman_rig.gd")
|
||||
const STICKMAN_LOCOMOTION := preload("res://scripts/characters/stickman/stickman_locomotion.gd")
|
||||
const PROP_BLOCK := preload("res://scripts/props/prop_block.gd")
|
||||
const TARGET_CAPTURE := preload("res://scripts/editors/stage/stage_target_capture_session.gd")
|
||||
const STAGE_PLACEMENT_OVERLAY := preload("res://scripts/stage/stage_placement_overlay.gd")
|
||||
const STAGE_DIRECTOR_VISUALS := preload("res://scripts/stage/stage_director_visuals.gd")
|
||||
|
||||
class FakeLocomotion extends "res://scripts/characters/stickman/stickman_locomotion.gd":
|
||||
var walking: bool = false
|
||||
var current_target: Vector2 = Vector2.ZERO
|
||||
var retargets: Array[Vector2] = []
|
||||
var styles: Array[String] = []
|
||||
|
||||
func project_target_to_navigation(target: Vector2) -> Vector2:
|
||||
return target
|
||||
|
||||
func move_to(target: Vector2, style: String = "walk", _speed_override: float = -1.0) -> bool:
|
||||
current_target = target
|
||||
styles.append(style)
|
||||
walking = true
|
||||
return true
|
||||
|
||||
func retarget_walk(target: Vector2) -> bool:
|
||||
current_target = target
|
||||
retargets.append(target)
|
||||
walking = true
|
||||
return true
|
||||
|
||||
func is_walking() -> bool:
|
||||
return walking
|
||||
|
||||
func cancel_walk(_restore_idle_pose: bool = true) -> void:
|
||||
walking = false
|
||||
|
||||
func consume_walk_failed() -> bool:
|
||||
return false
|
||||
|
||||
func consume_walk_completed() -> bool:
|
||||
return false
|
||||
|
||||
|
||||
var _checks: int = 0
|
||||
var _failures: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
_test_plan_helpers()
|
||||
_test_v18_migration()
|
||||
_test_v19_validation_and_round_trip()
|
||||
await _test_group_runtime_barrier()
|
||||
await _test_group_recover_say_runtime_barrier()
|
||||
await _test_moving_prop_pursuit()
|
||||
print("PHASE 20B ACTION PLAN: %d checks, %d failures" % [_checks, _failures])
|
||||
quit(1 if _failures > 0 else 0)
|
||||
|
||||
|
||||
func _check(condition: bool, message: String) -> void:
|
||||
_checks += 1
|
||||
if condition:
|
||||
print("PASS: " + message)
|
||||
else:
|
||||
_failures += 1
|
||||
push_error("FAIL: " + message)
|
||||
|
||||
|
||||
func _test_plan_helpers() -> void:
|
||||
var placement_overlay: StagePlacementOverlay = STAGE_PLACEMENT_OVERLAY.new() as StagePlacementOverlay
|
||||
placement_overlay.set_action_trajectory(Vector2.ZERO, Vector2(100.0, 80.0), true)
|
||||
_check(placement_overlay.action_show_trajectory and not placement_overlay.action_grounded_trajectory, "ordinary trajectories keep their authored direction")
|
||||
placement_overlay.set_grounded_action_trajectory(Vector2.ZERO, Vector2(100.0, 80.0), true)
|
||||
_check(placement_overlay.action_show_trajectory and placement_overlay.action_grounded_trajectory, "walk and run previews can keep a horizontal ground route")
|
||||
placement_overlay.set_action_marker(Vector2(40.0, 55.0))
|
||||
_check(placement_overlay.action_visible and not placement_overlay.action_show_trajectory and not placement_overlay.action_grounded_trajectory and placement_overlay.action_target.is_equal_approx(Vector2(40.0, 55.0)), "selection previews keep a cursor marker without a movement trajectory")
|
||||
placement_overlay.free()
|
||||
var director_visuals: StageDirectorVisuals = STAGE_DIRECTOR_VISUALS.new() as StageDirectorVisuals
|
||||
director_visuals.set_plan_candidate(null, "candidate", {}, false, Vector2.INF, "run")
|
||||
_check(director_visuals._plan_candidate_movement_label == "Run", "candidate route label follows the selected Run style")
|
||||
director_visuals.free()
|
||||
var place_capture: Dictionary = TARGET_CAPTURE.normalize_plan_destination(Vector2(33.0, 44.0))
|
||||
_check(String(place_capture.get("kind", "")) == "place" and (place_capture.get("position", Vector2.INF) as Vector2).is_equal_approx(Vector2(33.0, 44.0)), "PLAN_PLACE capture Vector2 normalizes to an explicit free-place destination")
|
||||
var prop_capture: Dictionary = {"kind": "prop", "target_prop_id": "stage-prop-1", "approach_offset": Vector2(12.0, -8.0)}
|
||||
_check(TARGET_CAPTURE.normalize_plan_destination(prop_capture) == prop_capture, "exact-prop capture dictionaries preserve the stable target ID and world offset")
|
||||
_check(TARGET_CAPTURE.normalize_plan_destination(null).is_empty(), "unsupported capture payloads do not author a destination")
|
||||
var move: Dictionary = {"action_id": "move-1", "type": "move_to", "style": "walk", "destination": {"kind": "place", "position": Vector2(120.0, 420.0)}}
|
||||
var say: Dictionary = {"action_id": "say-1", "type": "speak", "text": "Ready", "duration": 1.0}
|
||||
var wait_action: Dictionary = {"action_id": "wait-1", "type": "wait", "duration": 0.25}
|
||||
var together: Array[Dictionary] = [{"actions": [move, say]}]
|
||||
_check(STAGE_ACTION_PLAN.validate_steps(together).is_empty(), "Move plus Say is a valid together step")
|
||||
_check(not STAGE_ACTION_PLAN.validate_steps([{"actions": [move, wait_action]}]).is_empty(), "Move plus Wait is rejected as an authored together step")
|
||||
_check(STAGE_ACTION_PLAN.can_add_together({"actions": [wait_action]}, "speak"), "Wait step offers Say as a compatible together action")
|
||||
for action_type: String in ACTION_REGISTRY.types():
|
||||
if action_type == "speak":
|
||||
continue
|
||||
var primary_action: Dictionary = {"action_id": "primary-%s" % action_type, "type": action_type}
|
||||
_check(STAGE_ACTION_PLAN.can_add_together({"actions": [say]}, action_type), "%s step offers Say as a compatible together action" % ACTION_REGISTRY.label(action_type))
|
||||
_check(STAGE_ACTION_PLAN.can_add_together({"actions": [primary_action]}, "speak"), "Say can join %s" % ACTION_REGISTRY.label(action_type))
|
||||
_check(STAGE_ACTION_PLAN.validate_steps([{"actions": [primary_action, say]}]).is_empty(), "%s plus Say is a valid together step" % ACTION_REGISTRY.label(action_type))
|
||||
_check(not STAGE_ACTION_PLAN.can_add_together({"actions": [say]}, "speak"), "a step cannot contain two Say actions")
|
||||
_check(not STAGE_ACTION_PLAN.can_add_together({"actions": [move, say]}, "wait"), "full step rejects a third together action")
|
||||
var reordered: Array[Dictionary] = STAGE_ACTION_PLAN.move_step([{"actions": [move]}, {"actions": [wait_action]}], 0, 1)
|
||||
_check(String(reordered[1]["actions"][0]["action_id"]) == "move-1", "step reorder preserves action identity")
|
||||
var removed: Array[Dictionary] = STAGE_ACTION_PLAN.remove_action(together, "say-1")
|
||||
_check(removed.size() == 1 and (removed[0]["actions"] as Array).size() == 1, "deleting one grouped action preserves the remaining step")
|
||||
var identity: StageActionIdentity = STAGE_ACTION_IDENTITY.new() as StageActionIdentity
|
||||
var world: Node2D = Node2D.new()
|
||||
var rig: StickmanRig = STICKMAN_RIG.new() as StickmanRig
|
||||
world.add_child(rig)
|
||||
rig.set_action_plan(together)
|
||||
var first_id: String = identity.allocate(world)
|
||||
_check(not first_id.is_empty() and first_id not in ["move-1", "say-1"], "scene-scoped allocator avoids IDs already present on a live actor")
|
||||
world.free()
|
||||
|
||||
|
||||
func _test_v18_migration() -> void:
|
||||
var original: Dictionary = _legacy_v18_document([
|
||||
{"type": "speak", "text": "Go!", "duration": 0.4, "keep_acting": true},
|
||||
{"type": "move_to", "target": {"x": 130.0, "y": 420.0}, "style": "run"},
|
||||
{"type": "speak", "text": "Wait", "duration": 0.4, "keep_acting": true},
|
||||
{"type": "wait", "duration": 0.2},
|
||||
{"type": "speak", "text": "Legacy", "duration": 0.4, "keep_acting": true},
|
||||
{"type": "ragdoll"},
|
||||
])
|
||||
var migrated: Dictionary = STAGE_SCENE_IO._migrate_to_current(original)
|
||||
var repeated: Dictionary = STAGE_SCENE_IO._migrate_to_current(original.duplicate(true))
|
||||
var objects: Array = migrated.get("objects", [])
|
||||
var actor: Dictionary = objects[0] as Dictionary
|
||||
var plan: Array = actor.get("action_plan", [])
|
||||
_check(String(migrated.get("version", "")) == "1.9", "v1.8 migrates to v1.9")
|
||||
_check(not actor.has("action_queue"), "v1.8 migration removes the legacy authoritative queue")
|
||||
_check(plan.size() == 4 and (plan[0]["actions"] as Array).size() == 2, "keep_acting Say pairs with its following Move")
|
||||
_check(bool((plan[0]["actions"] as Array)[0].get("legacy_nonblocking", false)), "paired legacy Say retains nonblocking compatibility")
|
||||
var moved_action: Dictionary = (plan[0]["actions"] as Array)[1] as Dictionary
|
||||
var moved_destination: Dictionary = moved_action.get("destination", {})
|
||||
_check(String(moved_destination.get("kind", "")) == "place" and moved_destination.get("position", {}) == {"x": 130.0, "y": 420.0}, "legacy flat Move migrates to an explicit place target")
|
||||
_check((plan[1]["actions"] as Array).size() == 2 and String(((plan[1]["actions"] as Array)[1] as Dictionary).get("type", "")) == "wait", "keep_acting Say pairs with its following Wait")
|
||||
_check(bool(((plan[2]["actions"] as Array)[0] as Dictionary).get("legacy_nonblocking", false)), "unpaired legacy Say retains its explicit compatibility flag")
|
||||
_check(JSON.stringify(plan) == JSON.stringify((repeated["objects"] as Array)[0]["action_plan"]), "legacy action IDs are deterministic across repeated migration")
|
||||
_check(String(migrated.get("sky_id", "")) == "sky_sunset" and String(migrated.get("background_kind", "")) == "plain", "v1.8 migration keeps its authored background values")
|
||||
var warning_source: Dictionary = _legacy_v18_document([{"type": "not_a_real_action"}])
|
||||
var warning_migration: Dictionary = STAGE_SCENE_IO._migrate_to_current(warning_source)
|
||||
_check(not (warning_migration.get("_migration_warnings", []) as Array).is_empty(), "malformed legacy actions keep skip-and-warn migration behavior")
|
||||
|
||||
|
||||
func _test_v19_validation_and_round_trip() -> void:
|
||||
var canonical_plan: Array[Dictionary] = [
|
||||
{"actions": [
|
||||
{"action_id": "stable-move", "type": "move_to", "style": "run", "destination": {"kind": "prop", "target_prop_id": "prop-blue-door", "approach_offset": Vector2(-42.0, 0.0), "last_known_approach": Vector2(310.0, 420.0)}},
|
||||
{"action_id": "stable-say", "type": "speak", "text": "Let's go!", "duration": 1.2},
|
||||
]},
|
||||
{"actions": [{"action_id": "stable-place", "type": "move_to", "style": "walk", "destination": {"kind": "place", "position": Vector2(510.0, 420.0)}}]},
|
||||
]
|
||||
var encoded_result: Dictionary = STAGE_SCENE_IO.encode_plan(canonical_plan)
|
||||
var encoded_steps: Array = encoded_result.get("steps", [])
|
||||
_check((encoded_result.get("warnings", []) as Array).is_empty(), "canonical plans encode without warnings")
|
||||
var doc: Dictionary = _current_document([_stickman_entry("actor-a", "stage-actor-a", "Milo", encoded_steps)])
|
||||
_check(bool(STAGE_SCENE_IO._validate_root(doc).get("ok", false)), "valid v1.9 grouped Move+Say and place actions validate")
|
||||
var current_move: Dictionary = (((doc["objects"] as Array)[0] as Dictionary)["action_plan"] as Array)[0]["actions"][0]
|
||||
var current_destination: Dictionary = current_move.get("destination", {})
|
||||
_check(String(current_destination.get("target_prop_id", "")) == "prop-blue-door", "prop destination persists the exact stage object ID")
|
||||
_check(current_destination.get("approach_offset", {}) == {"x": -42.0, "y": 0.0}, "prop destination persists its world-aligned offset")
|
||||
var bad_duplicate: Dictionary = doc.duplicate(true)
|
||||
var duplicated_actor: Dictionary = _stickman_entry("actor-b", "stage-actor-b", "Ava", encoded_steps.duplicate(true))
|
||||
(bad_duplicate["objects"] as Array).append(duplicated_actor)
|
||||
_check(not bool(STAGE_SCENE_IO._validate_root(bad_duplicate).get("ok", true)), "duplicate stable action IDs across actors are rejected")
|
||||
var bad_queue: Dictionary = doc.duplicate(true)
|
||||
((bad_queue["objects"] as Array)[0] as Dictionary)["action_queue"] = []
|
||||
_check(not bool(STAGE_SCENE_IO._validate_root(bad_queue).get("ok", true)), "v1.9 rejects a second authoritative action_queue")
|
||||
var bad_group: Dictionary = doc.duplicate(true)
|
||||
var bad_first_action: Dictionary = ((((bad_group["objects"] as Array)[0] as Dictionary)["action_plan"] as Array)[0]["actions"] as Array)[0]
|
||||
bad_first_action["type"] = "ragdoll"
|
||||
bad_first_action.erase("style")
|
||||
bad_first_action.erase("destination")
|
||||
var bad_second_action: Dictionary = ((((bad_group["objects"] as Array)[0] as Dictionary)["action_plan"] as Array)[0]["actions"] as Array)[1]
|
||||
bad_second_action["type"] = "wait"
|
||||
bad_second_action["duration"] = 0.5
|
||||
bad_second_action.erase("text")
|
||||
_check(not bool(STAGE_SCENE_IO._validate_root(bad_group).get("ok", true)), "v1.9 rejects incompatible grouped actions")
|
||||
var bad_destination: Dictionary = doc.duplicate(true)
|
||||
var bad_move: Dictionary = ((((bad_destination["objects"] as Array)[0] as Dictionary)["action_plan"] as Array)[0]["actions"] as Array)[0]
|
||||
(bad_move["destination"] as Dictionary).erase("target_prop_id")
|
||||
_check(not bool(STAGE_SCENE_IO._validate_root(bad_destination).get("ok", true)), "v1.9 rejects a prop destination with a missing exact ID")
|
||||
var save_path: String = ProjectSettings.globalize_path("res://tests/.phase20b_plan_round_trip.scene")
|
||||
var write_result: Dictionary = STAGE_SCENE_IO.write_file_safe(save_path, doc)
|
||||
var read_result: Dictionary = STAGE_SCENE_IO.read_file(save_path)
|
||||
_check(bool(write_result.get("ok", false)) and bool(read_result.get("ok", false)), "v1.9 plan document writes and reads through the production IO path")
|
||||
var read_actor: Dictionary = (((read_result.get("data", {}) as Dictionary).get("objects", []) as Array)[0] as Dictionary)
|
||||
var decoded: Dictionary = STAGE_SCENE_IO.decode_plan(read_actor.get("action_plan", []))
|
||||
var decoded_steps: Array = decoded.get("steps", [])
|
||||
var decoded_prop: Dictionary = (((decoded_steps[0]["actions"] as Array)[0] as Dictionary).get("destination", {}) as Dictionary)
|
||||
var decoded_place: Dictionary = (((decoded_steps[1]["actions"] as Array)[0] as Dictionary).get("destination", {}) as Dictionary)
|
||||
_check(String(((decoded_steps[0]["actions"] as Array)[0] as Dictionary).get("action_id", "")) == "stable-move", "round-trip preserves stable action IDs")
|
||||
_check(decoded_prop.get("approach_offset", Vector2.ZERO) == Vector2(-42.0, 0.0), "round-trip decodes the prop offset as a Vector2")
|
||||
_check(String(decoded_place.get("kind", "")) == "place" and decoded_place.get("position", Vector2.ZERO) == Vector2(510.0, 420.0), "nearby free-place intent remains an explicit place after round-trip")
|
||||
DirAccess.remove_absolute(save_path)
|
||||
DirAccess.remove_absolute(save_path + ".bak")
|
||||
|
||||
|
||||
func _test_group_runtime_barrier() -> void:
|
||||
var rig: StickmanRig = STICKMAN_RIG_SCENE.instantiate() as StickmanRig
|
||||
root.add_child(rig)
|
||||
await process_frame
|
||||
var runner: StickmanActionRunner = rig.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
var started: Array[Dictionary] = []
|
||||
rig.planned_action_started.connect(func(action_id: String, _action: Dictionary, step_index: int) -> void:
|
||||
started.append({"id": action_id, "step": step_index, "frame": Engine.get_physics_frames()})
|
||||
)
|
||||
var grouped: Array[Dictionary] = [
|
||||
{"actions": [
|
||||
{"action_id": "runtime-wait", "type": "wait", "duration": 0.05},
|
||||
{"action_id": "runtime-say", "type": "speak", "text": "Together", "duration": 0.3, "legacy_fixed": true},
|
||||
]},
|
||||
{"actions": [{"action_id": "runtime-after", "type": "wait", "duration": 0.05}]},
|
||||
]
|
||||
_check(rig.set_action_plan(grouped), "runtime fixture accepts a grouped plan")
|
||||
rig.start_queue()
|
||||
runner.tick(0.01, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 2 and String(started[0]["id"]) == "runtime-wait" and String(started[1]["id"]) == "runtime-say", "group actions emit start together in plan order")
|
||||
_check(int(started[0]["frame"]) == int(started[1]["frame"]), "grouped actions start on the same runtime tick")
|
||||
runner.tick(0.06, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 2, "next step waits after primary Wait ends while grouped Say remains active")
|
||||
runner.pause_simulation()
|
||||
runner.tick(2.0, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 2, "paused runtime freezes the Say barrier")
|
||||
runner.resume_simulation()
|
||||
runner.tick(0.31, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 2, "completed Say releases the barrier before the next runner tick")
|
||||
runner.tick(0.01, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 3 and String(started[2]["id"]) == "runtime-after", "next step starts only after every grouped action ends")
|
||||
rig.stop_queue()
|
||||
rig.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _test_group_recover_say_runtime_barrier() -> void:
|
||||
var rig: StickmanRig = STICKMAN_RIG_SCENE.instantiate() as StickmanRig
|
||||
root.add_child(rig)
|
||||
await process_frame
|
||||
var runner: StickmanActionRunner = rig.get_node("RigComponents/ActionRunner") as StickmanActionRunner
|
||||
var started: Array[String] = []
|
||||
rig.planned_action_started.connect(func(action_id: String, _action: Dictionary, _step_index: int) -> void:
|
||||
started.append(action_id)
|
||||
)
|
||||
var grouped: Array[Dictionary] = [
|
||||
{"actions": [
|
||||
{"action_id": "runtime-recover", "type": "recover"},
|
||||
{"action_id": "runtime-recover-say", "type": "speak", "text": "I am okay!", "duration": 0.3, "legacy_fixed": true},
|
||||
]},
|
||||
{"actions": [{"action_id": "runtime-after-recover", "type": "wait", "duration": 0.05}]},
|
||||
]
|
||||
_check(rig.set_action_plan(grouped), "runtime fixture accepts Recover plus Say")
|
||||
rig.start_queue()
|
||||
runner.tick(0.01, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started == ["runtime-recover", "runtime-recover-say"], "Recover and Say start on the same runtime tick")
|
||||
runner.tick(0.01, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 2, "next step waits after Recover ends while its grouped Say remains active")
|
||||
runner.tick(0.31, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 2, "Recover plus Say completes its speech barrier before advancing")
|
||||
runner.tick(0.01, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(started.size() == 3 and started[2] == "runtime-after-recover", "the following step starts after Recover and Say both end")
|
||||
rig.stop_queue()
|
||||
rig.queue_free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _test_moving_prop_pursuit() -> void:
|
||||
var world: Node2D = Node2D.new()
|
||||
root.add_child(world)
|
||||
var rig: StickmanRig = STICKMAN_RIG_SCENE.instantiate() as StickmanRig
|
||||
world.add_child(rig)
|
||||
var prop: PropBlock = PROP_BLOCK.new() as PropBlock
|
||||
prop.stage_object_id = "prop-fetch-target"
|
||||
prop.global_position = Vector2(120.0, 420.0)
|
||||
world.add_child(prop)
|
||||
var fake_locomotion: FakeLocomotion = FakeLocomotion.new() as FakeLocomotion
|
||||
var runner: StickmanActionRunner = ACTION_RUNNER.new() as StickmanActionRunner
|
||||
root.add_child(runner)
|
||||
await process_frame
|
||||
runner.configure(rig, fake_locomotion)
|
||||
var outcome: Array[StringName] = []
|
||||
runner.planned_action_ended.connect(func(action_id: String, result: StringName, _step: int) -> void:
|
||||
if action_id == "fetch-move":
|
||||
outcome.append(result)
|
||||
)
|
||||
var move_plan: Array[Dictionary] = [{"actions": [{
|
||||
"action_id": "fetch-move",
|
||||
"type": "move_to",
|
||||
"style": "run",
|
||||
"destination": {"kind": "prop", "target_prop_id": prop.stage_object_id, "approach_offset": Vector2(-30.0, 0.0), "last_known_approach": Vector2(90.0, 420.0)},
|
||||
}]}]
|
||||
_check(runner.set_action_plan(move_plan), "pursuit fixture accepts an exact prop Move")
|
||||
runner.start_queue()
|
||||
runner.tick(0.01, int(StickmanRig.RigState.ANIMATED), false)
|
||||
_check(runner.is_prop_pursuit_active() and fake_locomotion.current_target == Vector2(90.0, 420.0), "prop Move starts toward its world-aligned approach offset")
|
||||
prop.global_position += Vector2(24.0, 0.0)
|
||||
runner.pre_movement(0.11)
|
||||
_check(fake_locomotion.retargets.size() == 1 and fake_locomotion.retargets[0] == Vector2(114.0, 420.0), "moving prop retargets after its shift using the fixed world offset")
|
||||
runner.pause_simulation()
|
||||
prop.global_position += Vector2(30.0, 0.0)
|
||||
runner.pre_movement(0.5)
|
||||
_check(fake_locomotion.retargets.size() == 1, "paused pursuit does not retarget or spend its time budget")
|
||||
runner.resume_simulation()
|
||||
runner.pre_movement(0.11)
|
||||
_check(fake_locomotion.retargets.size() == 2 and fake_locomotion.retargets[1] == Vector2(144.0, 420.0), "pursuit resumes and retargets from the prop's translated position")
|
||||
prop.queue_free()
|
||||
await process_frame
|
||||
runner.pre_movement(0.11)
|
||||
_check(outcome == [&"failed"], "vanished prop ends the active Move as failure, not arrival")
|
||||
runner.stop_queue()
|
||||
world.queue_free()
|
||||
runner.queue_free()
|
||||
fake_locomotion.free()
|
||||
await process_frame
|
||||
|
||||
|
||||
func _legacy_v18_document(queue: Array) -> Dictionary:
|
||||
var wall: Dictionary = {
|
||||
"file_id": "wall-1",
|
||||
"type": "wall_panel",
|
||||
"position": {"x": 0.0, "y": 0.0},
|
||||
"rotation": 0.0,
|
||||
"size": {"x": 320.0, "y": 400.0},
|
||||
"finish_id": "finish_wall_paint",
|
||||
"color": "#123456",
|
||||
}
|
||||
var actor: Dictionary = _stickman_entry("actor-a", "stage-actor-a", "Milo", [])
|
||||
actor["action_queue"] = queue.duplicate(true)
|
||||
return {
|
||||
"version": "1.8",
|
||||
"scene_name": "Legacy Plan",
|
||||
"sky_id": "sky_sunset",
|
||||
"background_kind": "plain",
|
||||
"plain_color": "#F5EFE0",
|
||||
"camera": {"position": {"x": 0.0, "y": 0.0}, "zoom": 0.5},
|
||||
"objects": [actor, wall],
|
||||
"event_rules": [],
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
|
||||
func _current_document(objects: Array) -> Dictionary:
|
||||
return {
|
||||
"version": STAGE_SCENE_IO.FILE_VERSION,
|
||||
"scene_name": "Plan Round Trip",
|
||||
"sky_id": "sky_neutral",
|
||||
"background_kind": "sky",
|
||||
"plain_color": "#F5EFE0",
|
||||
"camera": {"position": {"x": 0.0, "y": 0.0}, "zoom": 0.5},
|
||||
"objects": objects,
|
||||
"event_rules": [],
|
||||
"metadata": {},
|
||||
}
|
||||
|
||||
|
||||
func _stickman_entry(file_id: String, stage_id: String, stage_name: String, action_plan: Array) -> Dictionary:
|
||||
return {
|
||||
"file_id": file_id,
|
||||
"type": "stickman",
|
||||
"stage_object_id": stage_id,
|
||||
"stage_name": stage_name,
|
||||
"stk_path": "res://content/stickmen/basic.stk",
|
||||
"position": {"x": 0.0, "y": 0.0},
|
||||
"rotation": 0.0,
|
||||
"action_plan": action_plan.duplicate(true),
|
||||
}
|
||||
Reference in New Issue
Block a user