503 lines
18 KiB
GDScript
503 lines
18 KiB
GDScript
extends Node
|
|
|
|
const APP_SCENE := preload("res://src/scenes/App/App.tscn")
|
|
|
|
var _dance_animation_starts := 0
|
|
var _animation_starts: Array[StringName] = []
|
|
|
|
|
|
func _ready() -> void:
|
|
var app := APP_SCENE.instantiate()
|
|
add_child(app)
|
|
for _frame in range(8):
|
|
await get_tree().process_frame
|
|
|
|
var pet_host := get_node_or_null("App/InteractivePetHost")
|
|
if pet_host == null or not pet_host.HasPet:
|
|
_fail("theme pet was not activated")
|
|
return
|
|
pet_host.SetReducedEffects(true)
|
|
|
|
var start_anchor: Vector2 = pet_host.PetAnchor
|
|
var press_position := start_anchor + Vector2(0.0, -30.0)
|
|
var release_position := press_position + Vector2(110.0, -170.0)
|
|
|
|
var press := InputEventScreenTouch.new()
|
|
press.index = 0
|
|
press.position = press_position
|
|
press.pressed = true
|
|
get_viewport().push_input(press, true)
|
|
await get_tree().process_frame
|
|
print("PET_SMOKE_AFTER_PRESS state=%s anchor=%s press=%s" % [pet_host.MotionState, pet_host.PetAnchor, press_position])
|
|
if _current_pet_animation(pet_host) != "hanging":
|
|
_fail("dragging did not use the theme hanging animation")
|
|
return
|
|
|
|
var drag := InputEventScreenDrag.new()
|
|
drag.index = 0
|
|
drag.position = release_position
|
|
drag.relative = release_position - press_position
|
|
get_viewport().push_input(drag, true)
|
|
await get_tree().process_frame
|
|
print("PET_SMOKE_AFTER_DRAG state=%s anchor=%s release=%s" % [pet_host.MotionState, pet_host.PetAnchor, release_position])
|
|
|
|
var dragged_anchor: Vector2 = pet_host.PetAnchor
|
|
if dragged_anchor.distance_to(start_anchor) < 50.0:
|
|
_fail("single-finger drag did not move the pet")
|
|
return
|
|
|
|
var release := InputEventScreenTouch.new()
|
|
release.index = 0
|
|
release.position = release_position
|
|
release.pressed = false
|
|
get_viewport().push_input(release, true)
|
|
await get_tree().process_frame
|
|
var drop_animation := _current_pet_animation(pet_host)
|
|
print("PET_SMOKE_AFTER_RELEASE state=%s animation=%s" % [pet_host.MotionState, drop_animation])
|
|
if pet_host.MotionState == "Dropping" and drop_animation != "falling":
|
|
_fail("falling after drag did not use the theme falling animation (actual: %s)" % drop_animation)
|
|
return
|
|
|
|
var touch_landed := false
|
|
var saw_touch_landing := false
|
|
for _frame in range(6000):
|
|
await get_tree().process_frame
|
|
if pet_host.MotionState == "Landing":
|
|
saw_touch_landing = true
|
|
if _current_pet_animation(pet_host) != "landing":
|
|
_fail("landing state did not use the theme landing animation")
|
|
return
|
|
if pet_host.MotionState == "Sitting":
|
|
touch_landed = true
|
|
break
|
|
|
|
if not touch_landed:
|
|
_fail("pet did not land on a declared surface after touch drag")
|
|
return
|
|
if not saw_touch_landing:
|
|
_fail("pet skipped the landing animation after touch drag")
|
|
return
|
|
var post_landing_animation := _current_pet_animation(pet_host)
|
|
if post_landing_animation != "idle" and post_landing_animation != "sitting":
|
|
_fail("pet used an unexpected rest animation after landing: %s" % post_landing_animation)
|
|
return
|
|
|
|
var mouse_start: Vector2 = pet_host.PetAnchor
|
|
var mouse_press_position := mouse_start + Vector2(0.0, -30.0)
|
|
var mouse_release_position := mouse_press_position + Vector2(-70.0, -120.0)
|
|
var mouse_press := InputEventMouseButton.new()
|
|
mouse_press.button_index = MOUSE_BUTTON_LEFT
|
|
mouse_press.position = mouse_press_position
|
|
mouse_press.pressed = true
|
|
get_viewport().push_input(mouse_press, true)
|
|
await get_tree().process_frame
|
|
|
|
var mouse_motion := InputEventMouseMotion.new()
|
|
mouse_motion.position = mouse_release_position
|
|
mouse_motion.relative = mouse_release_position - mouse_press_position
|
|
get_viewport().push_input(mouse_motion, true)
|
|
await get_tree().process_frame
|
|
if pet_host.PetAnchor.distance_to(mouse_start) < 50.0:
|
|
_fail("held left mouse drag did not move the pet")
|
|
return
|
|
|
|
var mouse_release := InputEventMouseButton.new()
|
|
mouse_release.button_index = MOUSE_BUTTON_LEFT
|
|
mouse_release.position = mouse_release_position
|
|
mouse_release.pressed = false
|
|
get_viewport().push_input(mouse_release, true)
|
|
|
|
var mouse_landed := false
|
|
for _frame in range(240):
|
|
await get_tree().process_frame
|
|
if pet_host.MotionState == "Sitting":
|
|
mouse_landed = true
|
|
break
|
|
|
|
if not mouse_landed:
|
|
_fail("pet did not land on a declared surface after mouse drag")
|
|
return
|
|
if pet_host.CurrentSurfaceId != "floor":
|
|
if not pet_host.RequestMoveToSurface("floor"):
|
|
_fail("host could not return the pet to the Home floor before climb verification")
|
|
return
|
|
if not await _wait_for_surface(pet_host, "floor", 900):
|
|
_fail("pet did not return to the Home floor before climb verification")
|
|
return
|
|
|
|
var pet := _pet_instance(pet_host)
|
|
if pet == null:
|
|
_fail("pet instance is unavailable for climb action verification")
|
|
return
|
|
var animation_started_callback := Callable(self, "_on_pet_animation_started")
|
|
if not pet.is_connected("pet_animation_started", animation_started_callback):
|
|
pet.connect("pet_animation_started", animation_started_callback)
|
|
_animation_starts.clear()
|
|
|
|
if not pet_host.RequestMoveToSurface("advertisement_window"):
|
|
_fail("host rejected a connected theme-owned window surface")
|
|
return
|
|
|
|
var saw_climb := false
|
|
var saw_hanging_during_climb := false
|
|
var saw_climb_action := false
|
|
var climb_action_anchor := Vector2.ZERO
|
|
for _frame in range(900):
|
|
await get_tree().process_frame
|
|
saw_climb = saw_climb or pet_host.MotionState.begins_with("Climb")
|
|
if pet_host.MotionState.begins_with("Climb") and _current_pet_animation(pet_host) == "hanging":
|
|
saw_hanging_during_climb = true
|
|
if pet_host.MotionState.begins_with("ClimbAction"):
|
|
if not saw_climb_action:
|
|
saw_climb_action = true
|
|
climb_action_anchor = pet_host.PetAnchor
|
|
elif pet_host.PetAnchor.distance_to(climb_action_anchor) > 0.1:
|
|
_fail("pet moved while the climbing action sequence was playing")
|
|
return
|
|
if pet_host.MotionState == "Sitting" and pet_host.CurrentSurfaceId == "advertisement_window":
|
|
if not saw_climb:
|
|
_fail("pet reached an elevated window without entering a climb state")
|
|
return
|
|
if saw_hanging_during_climb:
|
|
_fail("hanging animation played while completing a climb")
|
|
return
|
|
if not saw_climb_action:
|
|
_fail("pet completed the climb without the climbing action sequence")
|
|
return
|
|
var expected_climb_action: Array[StringName] = [
|
|
&"climbing_action_start",
|
|
&"climbing_action",
|
|
&"climbing_action_stop",
|
|
]
|
|
if not _contains_animation_subsequence(_animation_starts, expected_climb_action):
|
|
_fail("unexpected climbing action sequence: %s" % [_animation_starts])
|
|
return
|
|
var post_walk_animation := _current_pet_animation(pet_host)
|
|
if post_walk_animation != "idle" and post_walk_animation != "sitting":
|
|
_fail("post-walk rest used an unexpected animation: %s" % post_walk_animation)
|
|
return
|
|
if not await _verify_screen_integration(pet_host):
|
|
return
|
|
print("INTERACTIVE_PET_SMOKE_OK surface=%s anchor=%s" % [
|
|
pet_host.CurrentSurfaceId,
|
|
pet_host.PetAnchor,
|
|
])
|
|
get_tree().quit(0)
|
|
return
|
|
|
|
_fail("pet did not climb to and sit on the declared window surface")
|
|
|
|
|
|
func _verify_screen_integration(pet_host: Node) -> bool:
|
|
var screen_manager := get_node_or_null("/root/ScreenManager")
|
|
if screen_manager == null:
|
|
_fail("screen manager autoload is unavailable")
|
|
return false
|
|
|
|
var elevated_anchor: Vector2 = pet_host.PetAnchor
|
|
if not screen_manager.Show(1, false): # CashPayment
|
|
_fail("could not switch from Home to CashPayment")
|
|
return false
|
|
await get_tree().process_frame
|
|
if not pet_host.visible:
|
|
_fail("pet was hidden on CashPayment")
|
|
return false
|
|
if pet_host.MotionState != "Dropping" or _current_pet_animation(pet_host) != "falling":
|
|
_fail("pet did not start falling toward the bottom after a screen change")
|
|
return false
|
|
|
|
if not await _wait_for_landing_then_sitting(pet_host, 360):
|
|
_fail("pet did not land after switching from Home to CashPayment")
|
|
return false
|
|
if pet_host.PetAnchor.y <= elevated_anchor.y:
|
|
_fail("pet did not land at the bottom after leaving an elevated Home surface")
|
|
return false
|
|
|
|
# Prove that the CashPayment topology is active by completing a climb onto its button.
|
|
if not pet_host.RequestMoveToSurface("dispense_button"):
|
|
_fail("CashPayment topology rejected its dispense button surface")
|
|
return false
|
|
if not await _wait_for_surface(pet_host, "dispense_button", 900):
|
|
_fail("pet did not walk and climb onto the CashPayment dispense button")
|
|
return false
|
|
|
|
# Switching from an elevated surface must preserve the old anchor and drop to
|
|
# the default floor declared by the next scene.
|
|
var cash_button_anchor: Vector2 = pet_host.PetAnchor
|
|
if not screen_manager.Show(2, false): # CardAmount
|
|
_fail("could not switch from CashPayment to CardAmount")
|
|
return false
|
|
var anchor_after_switch: Vector2 = pet_host.PetAnchor
|
|
if anchor_after_switch.distance_to(cash_button_anchor) > 0.1:
|
|
_fail("screen change did not preserve the pet anchor before falling")
|
|
return false
|
|
await get_tree().process_frame
|
|
if not pet_host.visible:
|
|
_fail("pet was hidden on CardAmount")
|
|
return false
|
|
if pet_host.MotionState != "Dropping" or _current_pet_animation(pet_host) != "falling":
|
|
_fail("pet did not fall from the old CashPayment position on CardAmount")
|
|
return false
|
|
if not await _wait_for_sitting(pet_host, 300):
|
|
_fail("pet did not land on the CardAmount floor")
|
|
return false
|
|
if not pet_host.RequestMoveToSurface("confirm_button"):
|
|
_fail("CardAmount topology rejected its confirm button surface")
|
|
return false
|
|
|
|
# A unique reachable target on every remaining screen proves that the host
|
|
# switched to that scene's theme-owned topology rather than reusing Home.
|
|
var screen_targets := [
|
|
[3, "keypad_action_row", "CardCustomAmount"],
|
|
[4, "cancel_button", "CardTerminal"],
|
|
[5, "home_button", "Success"],
|
|
[6, "home_button", "Error"],
|
|
[7, "cancel_button", "ServiceAccess"],
|
|
[0, "advertisement_window", "Home"],
|
|
]
|
|
for entry in screen_targets:
|
|
var screen_id: int = entry[0]
|
|
var surface_id: String = entry[1]
|
|
var screen_name: String = entry[2]
|
|
if not screen_manager.Show(screen_id, false):
|
|
_fail("could not switch to %s" % screen_name)
|
|
return false
|
|
await get_tree().process_frame
|
|
if not pet_host.visible:
|
|
_fail("pet was hidden on %s" % screen_name)
|
|
return false
|
|
if not pet_host.RequestMoveToSurface(surface_id):
|
|
_fail("%s topology rejected its %s surface" % [screen_name, surface_id])
|
|
return false
|
|
|
|
# Preserve an elevated position while Settings hides the pet, then fall on the next screen.
|
|
var press_position: Vector2 = pet_host.PetAnchor + Vector2(0.0, -30.0)
|
|
var held_position: Vector2 = press_position + Vector2(0.0, -180.0)
|
|
var press := InputEventScreenTouch.new()
|
|
press.index = 0
|
|
press.position = press_position
|
|
press.pressed = true
|
|
get_viewport().push_input(press, true)
|
|
await get_tree().process_frame
|
|
var drag := InputEventScreenDrag.new()
|
|
drag.index = 0
|
|
drag.position = held_position
|
|
drag.relative = held_position - press_position
|
|
get_viewport().push_input(drag, true)
|
|
await get_tree().process_frame
|
|
var position_before_settings: Vector2 = pet_host.PetAnchor
|
|
|
|
if not screen_manager.Show(8, false): # Settings
|
|
_fail("could not switch to Settings")
|
|
return false
|
|
await get_tree().process_frame
|
|
if pet_host.visible:
|
|
_fail("pet remained visible on Settings")
|
|
return false
|
|
if not pet_host.PetAnchor.is_equal_approx(position_before_settings):
|
|
_fail("Settings changed the pet position while hiding it")
|
|
return false
|
|
|
|
if not screen_manager.Show(6, false): # Error
|
|
_fail("could not leave Settings for Error")
|
|
return false
|
|
await get_tree().process_frame
|
|
if not pet_host.visible or pet_host.MotionState != "Dropping":
|
|
_fail("pet did not reappear falling from its saved position after Settings")
|
|
return false
|
|
if _current_pet_animation(pet_host) != "falling":
|
|
_fail("pet did not use falling after leaving Settings")
|
|
return false
|
|
|
|
if not await _wait_for_sitting(pet_host, 300):
|
|
_fail("pet did not reach the bottom after leaving Settings")
|
|
return false
|
|
|
|
# A dispense celebration requested while the pet is falling must wait for
|
|
# landing and then restart the mapped dance exactly four times.
|
|
if not pet_host.RequestMoveToSurface("home_button"):
|
|
_fail("Error topology rejected its home button before celebration test")
|
|
return false
|
|
if not await _wait_for_surface(pet_host, "home_button", 900):
|
|
_fail("pet did not climb onto the Error home button")
|
|
return false
|
|
|
|
var pet := _pet_instance(pet_host)
|
|
if pet == null:
|
|
_fail("pet instance is unavailable for celebration verification")
|
|
return false
|
|
var animation_started_callback := Callable(self, "_on_pet_animation_started")
|
|
if not pet.is_connected("pet_animation_started", animation_started_callback):
|
|
pet.connect("pet_animation_started", animation_started_callback)
|
|
_dance_animation_starts = 0
|
|
|
|
if not screen_manager.Show(5, false): # Success
|
|
_fail("could not switch from Error to Success for celebration test")
|
|
return false
|
|
if not pet_host.RequestDispenseCelebration(4):
|
|
_fail("host rejected the four-cycle dispense celebration")
|
|
return false
|
|
if pet_host.MotionState != "Dropping":
|
|
_fail("dispense celebration did not wait for the screen-change drop")
|
|
return false
|
|
|
|
var saw_landing_before_celebration := false
|
|
var saw_celebration := false
|
|
for _frame in range(6000):
|
|
await get_tree().process_frame
|
|
if pet_host.MotionState == "Landing":
|
|
saw_landing_before_celebration = true
|
|
if _current_pet_animation(pet_host) != "landing":
|
|
_fail("queued celebration landing state used the wrong animation")
|
|
return false
|
|
if pet_host.MotionState == "Celebrating" and not saw_landing_before_celebration:
|
|
_fail("dispense celebration started before the landing animation completed")
|
|
return false
|
|
saw_celebration = saw_celebration or pet_host.MotionState == "Celebrating"
|
|
if saw_celebration and pet_host.MotionState == "Sitting":
|
|
if not saw_landing_before_celebration:
|
|
_fail("dispense celebration skipped the landing animation")
|
|
return false
|
|
if _dance_animation_starts != 4:
|
|
_fail("dispense celebration started %s dance cycles instead of 4" % _dance_animation_starts)
|
|
return false
|
|
return await _verify_composite_interaction(pet_host)
|
|
_fail("four-cycle dispense celebration did not finish")
|
|
return false
|
|
|
|
|
|
func _wait_for_sitting(pet_host: Node, max_frames: int) -> bool:
|
|
for _frame in range(maxi(max_frames, 1200)):
|
|
await get_tree().process_frame
|
|
if pet_host.MotionState == "Sitting":
|
|
return true
|
|
return false
|
|
|
|
|
|
func _wait_for_landing_then_sitting(pet_host: Node, max_frames: int) -> bool:
|
|
var saw_landing := false
|
|
for _frame in range(maxi(max_frames, 1200)):
|
|
await get_tree().process_frame
|
|
if pet_host.MotionState == "Landing":
|
|
saw_landing = true
|
|
if _current_pet_animation(pet_host) != "landing":
|
|
return false
|
|
if pet_host.MotionState == "Sitting":
|
|
var post_landing_animation := _current_pet_animation(pet_host)
|
|
return saw_landing and (
|
|
post_landing_animation == "idle" or post_landing_animation == "sitting"
|
|
)
|
|
return false
|
|
|
|
|
|
func _verify_composite_interaction(pet_host: Node) -> bool:
|
|
if pet_host.MotionState != "Sitting" and not await _wait_for_sitting(pet_host, 300):
|
|
_fail("pet did not reach an initial surface before the composite action")
|
|
return false
|
|
pet_host.SetReducedEffects(false)
|
|
|
|
var pet := _pet_instance(pet_host)
|
|
if pet == null:
|
|
_fail("pet instance is unavailable for composite action verification")
|
|
return false
|
|
var animation_started_callback := Callable(self, "_on_pet_animation_started")
|
|
if not pet.is_connected("pet_animation_started", animation_started_callback):
|
|
pet.connect("pet_animation_started", animation_started_callback)
|
|
if not bool(pet.call("play_pet_animation", &"walking_left")):
|
|
_fail("could not prepare the left-facing composite action branch")
|
|
return false
|
|
await get_tree().process_frame
|
|
|
|
_animation_starts.clear()
|
|
pet_host.RequestInteraction(1)
|
|
if pet_host.MotionState != "ThemeAction" or not pet_host.HasActiveCompositeAction:
|
|
_fail("host did not delegate interaction to the theme composite action")
|
|
return false
|
|
|
|
var visual_root := pet.get_node_or_null("VisualRoot") as Node2D
|
|
var saw_double_scale := false
|
|
var saw_vertical_wrap := false
|
|
var previous_anchor: Vector2 = pet_host.PetAnchor
|
|
for _frame in range(2400):
|
|
await get_tree().process_frame
|
|
if visual_root != null and visual_root.scale.x >= 1.95:
|
|
saw_double_scale = true
|
|
var current_anchor: Vector2 = pet_host.PetAnchor
|
|
if previous_anchor.y - current_anchor.y > 500.0:
|
|
saw_vertical_wrap = true
|
|
previous_anchor = current_anchor
|
|
if pet_host.MotionState == "Sitting" and not pet_host.HasActiveCompositeAction:
|
|
break
|
|
|
|
if pet_host.MotionState != "Sitting" or pet_host.HasActiveCompositeAction:
|
|
_fail("theme composite action did not return control to the host")
|
|
return false
|
|
if not saw_double_scale:
|
|
_fail("come_closer did not enlarge the theme-owned visual to 2x")
|
|
return false
|
|
if not saw_vertical_wrap:
|
|
_fail("falling did not cross the bottom and wrap to the top")
|
|
return false
|
|
if pet_host.CurrentSurfaceId.is_empty() or pet_host.PetAnchor.y >= 200.0:
|
|
_fail("composite action did not land on the highest declared support")
|
|
return false
|
|
|
|
var expected: Array[StringName] = [
|
|
&"turn_left",
|
|
&"come_closer",
|
|
&"action",
|
|
&"falling",
|
|
&"landing",
|
|
&"idle",
|
|
]
|
|
if not _contains_animation_subsequence(_animation_starts, expected):
|
|
_fail("unexpected composite animation sequence: %s" % [_animation_starts])
|
|
return false
|
|
return true
|
|
|
|
|
|
func _contains_animation_subsequence(
|
|
actual: Array[StringName],
|
|
expected: Array[StringName]
|
|
) -> bool:
|
|
var expected_index := 0
|
|
for animation_name in actual:
|
|
if expected_index < expected.size() and animation_name == expected[expected_index]:
|
|
expected_index += 1
|
|
return expected_index == expected.size()
|
|
|
|
|
|
func _wait_for_surface(pet_host: Node, surface_id: String, max_frames: int) -> bool:
|
|
for _frame in range(max_frames):
|
|
await get_tree().process_frame
|
|
if pet_host.MotionState == "Sitting" and pet_host.CurrentSurfaceId == surface_id:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _current_pet_animation(pet_host: Node) -> String:
|
|
var pet := _pet_instance(pet_host)
|
|
if pet == null:
|
|
return ""
|
|
if not pet.has_method("get_current_pet_animation"):
|
|
return ""
|
|
return String(pet.call("get_current_pet_animation"))
|
|
|
|
|
|
func _pet_instance(pet_host: Node) -> Node:
|
|
var pet_stage := pet_host.get_node_or_null("PetStage")
|
|
if pet_stage == null or pet_stage.get_child_count() != 1:
|
|
return null
|
|
return pet_stage.get_child(0)
|
|
|
|
|
|
func _on_pet_animation_started(animation_name: StringName) -> void:
|
|
_animation_starts.append(animation_name)
|
|
if animation_name == &"dancing":
|
|
_dance_animation_starts += 1
|
|
|
|
|
|
func _fail(message: String) -> void:
|
|
push_error("INTERACTIVE_PET_SMOKE_FAILED: %s" % message)
|
|
get_tree().quit(1)
|