209 lines
8.1 KiB
GDScript
209 lines
8.1 KiB
GDScript
extends SceneTree
|
|
|
|
const MainScene = preload("res://main.tscn")
|
|
const State = preload("res://p9_3/core_loop_state.gd")
|
|
const VIEWPORT_SIZE := Vector2i(844, 390)
|
|
|
|
const PATHS := {
|
|
"Wayrunner": {"tick": 99, "hearthguard_hp": 22, "heals": 0, "coin": 22},
|
|
"Kindhand": {"tick": 133, "hearthguard_hp": 84, "heals": 11, "coin": 21},
|
|
}
|
|
|
|
var failures: Array[String] = []
|
|
var game: Control
|
|
var experience: Control
|
|
var touch_count := 0
|
|
var capture_root := ""
|
|
|
|
|
|
func _initialize() -> void:
|
|
call_deferred("run_test")
|
|
|
|
|
|
func run_test() -> void:
|
|
capture_root = OS.get_environment("ABG_P9_4_CAPTURE_ROOT")
|
|
root.content_scale_mode = Window.CONTENT_SCALE_MODE_CANVAS_ITEMS
|
|
root.content_scale_aspect = Window.CONTENT_SCALE_ASPECT_EXPAND
|
|
root.content_scale_size = VIEWPORT_SIZE
|
|
root.size = VIEWPORT_SIZE
|
|
for recruit_id in ["Wayrunner", "Kindhand"]:
|
|
await run_path(recruit_id)
|
|
await destroy_game()
|
|
if failures.is_empty():
|
|
print("ABG_P9_4_SECOND_CYCLE_RUNTIME_OK paths=2 touch=%d fallback=0 direct=0 ticks=99,133 hearthguard_hp=22,84 wayrunner=true healing=11 coin=22,21 company=3/10 cards=0" % touch_count)
|
|
quit(0)
|
|
else:
|
|
for failure in failures:
|
|
push_error(failure)
|
|
quit(1)
|
|
|
|
|
|
func run_path(recruit_id: String) -> void:
|
|
var expected: Dictionary = PATHS[recruit_id]
|
|
game = MainScene.instantiate()
|
|
game.auto_boot = false
|
|
game.audio_enabled = false
|
|
root.add_child(game)
|
|
await settle(8)
|
|
game.start_p93_experience()
|
|
await settle(8)
|
|
experience = game.p93_experience
|
|
prepare_returned_hall()
|
|
experience.returned_from_battle = true
|
|
experience._render_current()
|
|
await settle(8)
|
|
|
|
expect(find_control(experience, "P94IncomingThreat") != null, "%s sees Crosswind before input" % recruit_id)
|
|
expect(find_control(experience, "P93OfferTarget_%s" % recruit_id) != null, "%s body is a direct target" % recruit_id)
|
|
await capture(recruit_id, "01-returned-threat")
|
|
await tap("P93OfferTarget_%s" % recruit_id)
|
|
expect(experience.service.state.focused_offer == recruit_id, "%s first touch focuses" % recruit_id)
|
|
expect(find_control(experience, "P93OfferDetail") != null, "%s exposes role detail" % recruit_id)
|
|
await capture(recruit_id, "02-focused")
|
|
await tap("P93OfferTarget_%s" % recruit_id)
|
|
expect(experience.service.state.phase == State.Phase.ENCOUNTER, "%s second touch hires" % recruit_id)
|
|
expect(experience.service.state.company == ["Hearthguard", "Farstring", recruit_id], "%s joins the 3/10 line" % recruit_id)
|
|
|
|
await tap("P93EncounterTarget_crosswind")
|
|
expect(experience.service.state.selected_encounter == "crosswind", "%s selects Crosswind" % recruit_id)
|
|
await tap("P93EncounterTarget_crosswind")
|
|
expect(experience.service.state.phase == State.Phase.BATTLE, "%s starts the second battle" % recruit_id)
|
|
await tap("P93SpeedButton")
|
|
|
|
var role_seen := false
|
|
var role_captured := false
|
|
var role_cue_seen := false
|
|
var guard := 0
|
|
while experience.service.state.phase == State.Phase.BATTLE and guard < 1600:
|
|
for event in experience.service.battle.state.event_log:
|
|
if String(event.get("kind", "")) != "member_action" or String(event.get("source_member_id", "")) != recruit_id:
|
|
continue
|
|
if recruit_id == "Kindhand" and String(event.get("qualifier_en", "")) != "healing":
|
|
continue
|
|
role_seen = true
|
|
role_cue_seen = role_cue_seen or find_control(experience, "P94RoleCue_%s" % recruit_id) != null
|
|
if role_seen and not role_captured:
|
|
await settle(10)
|
|
role_cue_seen = role_cue_seen or find_control(experience, "P94RoleCue_%s" % recruit_id) != null
|
|
await capture(recruit_id, "03-role-action")
|
|
role_captured = true
|
|
await process_frame
|
|
guard += 1
|
|
expect(role_seen, "%s performs the required role action" % recruit_id)
|
|
expect(role_cue_seen, "%s exposes a visible role cue" % recruit_id)
|
|
expect(experience.service.state.phase == State.Phase.RESULT and experience.service.state.last_outcome == "victory", "%s reaches victory" % recruit_id)
|
|
expect(experience.service.battle.state.tick == int(expected.tick), "%s preserves the exact terminal tick" % recruit_id)
|
|
expect(int(experience.service.battle.state.member_hp.Hearthguard) == int(expected.hearthguard_hp), "%s preserves the exact Hearthguard consequence" % recruit_id)
|
|
expect(count_heals(recruit_id) == int(expected.heals), "%s preserves the exact heal count" % recruit_id)
|
|
experience._process(0.8)
|
|
await settle(55)
|
|
expect(find_control(experience, "P93BattleResult") != null, "%s result holds on the field" % recruit_id)
|
|
await capture(recruit_id, "04-result")
|
|
|
|
await tap("P93ReturnHallButton")
|
|
expect(experience.service.state.phase == State.Phase.HALL, "%s returns to Hall" % recruit_id)
|
|
expect(experience.service.state.coin == int(expected.coin), "%s preserves exact Coin" % recruit_id)
|
|
expect(experience.service.state.company == ["Hearthguard", "Farstring", recruit_id], "%s preserves the selected Company" % recruit_id)
|
|
expect(find_control(experience, "P93Company_%s" % recruit_id) != null, "%s is visible in the returned line" % recruit_id)
|
|
expect(count_named_fragment(experience, "Card") == 0, "%s path adds no cards" % recruit_id)
|
|
verify_control_bounds(experience, Rect2(Vector2.ZERO, Vector2(VIEWPORT_SIZE)))
|
|
await capture(recruit_id, "05-returned-company")
|
|
|
|
|
|
func prepare_returned_hall() -> void:
|
|
var service = experience.service
|
|
service.enter_hall()
|
|
service.focus_offer("Farstring")
|
|
service.hire("Farstring")
|
|
service.select_encounter("open_road")
|
|
service.start_battle()
|
|
while service.state.phase == State.Phase.BATTLE:
|
|
service.advance_battle_ticks(1)
|
|
service.claim_and_return()
|
|
|
|
|
|
func count_heals(recruit_id: String) -> int:
|
|
var result := 0
|
|
for event in experience.service.battle.state.event_log:
|
|
if String(event.get("kind", "")) == "member_action" and String(event.get("source_member_id", "")) == recruit_id and String(event.get("qualifier_en", "")) == "healing":
|
|
result += 1
|
|
return result
|
|
|
|
|
|
func tap(node_name: String) -> void:
|
|
var control := find_control(experience, node_name)
|
|
if control == null or not control.is_visible_in_tree() or (control is BaseButton and control.disabled):
|
|
failures.append("unavailable parsed-touch target %s" % node_name)
|
|
return
|
|
var position := root.get_final_transform() * control.get_global_rect().get_center()
|
|
var down := InputEventScreenTouch.new()
|
|
down.index = 0
|
|
down.position = position
|
|
down.pressed = true
|
|
Input.parse_input_event(down)
|
|
await process_frame
|
|
var up := InputEventScreenTouch.new()
|
|
up.index = 0
|
|
up.position = position
|
|
up.pressed = false
|
|
Input.parse_input_event(up)
|
|
touch_count += 1
|
|
await settle(6)
|
|
|
|
|
|
func capture(path_id: String, name: String) -> void:
|
|
if capture_root.is_empty():
|
|
return
|
|
var path := capture_root.path_join(path_id.to_lower())
|
|
DirAccess.make_dir_recursive_absolute(path)
|
|
await settle(3)
|
|
await RenderingServer.frame_post_draw
|
|
var error := root.get_texture().get_image().save_png(path.path_join(name + ".png"))
|
|
expect(error == OK, "capture %s/%s writes" % [path_id, name])
|
|
|
|
|
|
func verify_control_bounds(node: Node, bounds: Rect2) -> void:
|
|
if node is BaseButton and node.is_visible_in_tree():
|
|
var rect: Rect2 = node.get_global_rect()
|
|
if rect.position.x < -0.5 or rect.position.y < -0.5 or rect.end.x > bounds.end.x + 0.5 or rect.end.y > bounds.end.y + 0.5:
|
|
failures.append("control %s outside viewport: %s" % [node.name, rect])
|
|
if rect.size.x < 44.0 or rect.size.y < 44.0:
|
|
failures.append("control %s below 44px: %s" % [node.name, rect.size])
|
|
for child in node.get_children():
|
|
verify_control_bounds(child, bounds)
|
|
|
|
|
|
func count_named_fragment(node: Node, fragment: String) -> int:
|
|
var result := 1 if fragment in String(node.name) else 0
|
|
for child in node.get_children():
|
|
result += count_named_fragment(child, fragment)
|
|
return result
|
|
|
|
|
|
func find_control(node: Node, node_name: String) -> Control:
|
|
if node.name == node_name and node is Control and not node.is_queued_for_deletion():
|
|
return node
|
|
for child in node.get_children():
|
|
var found := find_control(child, node_name)
|
|
if found != null:
|
|
return found
|
|
return null
|
|
|
|
|
|
func destroy_game() -> void:
|
|
if is_instance_valid(game):
|
|
game.queue_free()
|
|
await settle(90)
|
|
game = null
|
|
experience = null
|
|
|
|
|
|
func settle(frames: int) -> void:
|
|
for _frame in frames:
|
|
await process_frame
|
|
|
|
|
|
func expect(condition: bool, message: String) -> void:
|
|
if not condition:
|
|
failures.append("P9.4 second-cycle runtime: %s" % message)
|