149 lines
5.4 KiB
GDScript
149 lines
5.4 KiB
GDScript
extends SceneTree
|
|
|
|
## Game-flow state machine + boss-room entry rules (AnchorV1.0 §4.5 / §19.5).
|
|
|
|
var failures: Array[String] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
var flow: Node = load("res://autoload/game_flow_manager.gd").new()
|
|
flow.set("manage_scene", false)
|
|
root.add_child(flow)
|
|
await process_frame
|
|
|
|
# --- screens exist for every menu state ---
|
|
for state_name: StringName in [&"Title", &"DifficultySelect", &"Paused", &"VolumeSettings", &"ExitGameConfirm", &"ReturnTitleConfirm", &"Defeat", &"Victory", &"Result"]:
|
|
var screens: Dictionary = flow.get("_screens")
|
|
_expect_bool(screens.has(state_name), true, "screen for %s exists" % state_name)
|
|
var flow_ui := flow.get_node_or_null("FlowUI")
|
|
_expect_bool(flow_ui != null, true, "flow UI layer exists")
|
|
if flow_ui != null:
|
|
_expect_bool(flow_ui.get_node_or_null("PauseButton") == null, true, "HUD should not show a top-right pause button; pause is Esc-only")
|
|
|
|
# --- pause / resume drive the SceneTree pause + rhythm clock ---
|
|
flow.set("state", &"Gameplay_FrontArea")
|
|
var esc := InputEventAction.new()
|
|
esc.action = &"ui_cancel"
|
|
esc.pressed = true
|
|
flow.call("_unhandled_input", esc)
|
|
_expect_string(str(flow.get("state")), "Paused", "Esc enters Paused")
|
|
_expect_bool(root.get_tree().paused, true, "pause stops the SceneTree")
|
|
flow.call("_unhandled_input", esc)
|
|
_expect_string(str(flow.get("state")), "Gameplay_FrontArea", "Esc resumes gameplay")
|
|
_expect_bool(root.get_tree().paused, false, "resume releases the SceneTree")
|
|
|
|
# --- boss-room entry rules on a synthetic scene (§4.5) ---
|
|
var scene := _build_fake_scene()
|
|
root.add_child(scene)
|
|
current_scene = scene
|
|
await process_frame
|
|
var player := scene.get_node("Stage/ActorsContainer/Player")
|
|
var streak := player.get_node("StreakCounter")
|
|
streak.set("streak", 9)
|
|
var combo := player.get_node("ComboWindow")
|
|
combo.call("record", &"A")
|
|
combo.call("record", &"A")
|
|
var buff := player.get_node("AttackBuffComponent")
|
|
var container := player.get_node("EffectContainer")
|
|
for index: int in range(6):
|
|
container.call("add_effect", buff.get("buff_definition"), &"test")
|
|
_expect_int(int(buff.call("attack_buff_stacks")), 6, "test setup should reach 6 buff stacks")
|
|
|
|
flow.set("state", &"Gameplay_FrontArea")
|
|
flow.set("_boss_room_entered", false)
|
|
flow.call("enter_boss_room")
|
|
|
|
_expect_string(str(flow.get("state")), "Gameplay_BossRoom", "entering the boss room switches state")
|
|
_expect_int(int(streak.get("streak")), 0, "boss entry clears the combo counter")
|
|
_expect_int((combo.call("get_slots") as Array).size(), 0, "boss entry clears the four-slot window")
|
|
_expect_int(int(buff.call("attack_buff_stacks")), 3, "boss entry caps the attack buff at 3 stacks")
|
|
var boss := scene.get_node("Stage/ActorsContainer/Boss")
|
|
_expect_bool(bool(boss.get("combat_enabled")), true, "boss combat unlocks on entry")
|
|
_expect_bool(bool(boss.get("stationary")), false, "boss leaves training-dummy mode on entry")
|
|
|
|
# --- re-entering is a no-op ---
|
|
streak.set("streak", 5)
|
|
flow.call("enter_boss_room")
|
|
_expect_int(int(streak.get("streak")), 5, "boss entry rules apply only once")
|
|
|
|
# --- rank appears only on victory (§19.6) ---
|
|
flow.set("_victory", true)
|
|
flow.set("state", &"Result")
|
|
flow.call("_populate_result_screen")
|
|
var labels: Dictionary = flow.get("_result_labels")
|
|
_expect_bool((labels.get("rank") as Label).visible, true, "victory result shows the rank")
|
|
flow.set("_victory", false)
|
|
flow.call("_populate_result_screen")
|
|
_expect_bool((labels.get("rank") as Label).visible, false, "defeat result hides the rank")
|
|
|
|
current_scene = null
|
|
scene.free()
|
|
flow.free()
|
|
_finish()
|
|
|
|
|
|
func _build_fake_scene() -> Node:
|
|
var scene := Node.new()
|
|
scene.name = "Main"
|
|
var stage := Node2D.new()
|
|
stage.name = "Stage"
|
|
scene.add_child(stage)
|
|
var actors := Node2D.new()
|
|
actors.name = "ActorsContainer"
|
|
stage.add_child(actors)
|
|
|
|
var player := CharacterBody2D.new()
|
|
player.name = "Player"
|
|
actors.add_child(player)
|
|
var streak: Node = load("res://scenes/components/streak_counter.gd").new()
|
|
streak.name = "StreakCounter"
|
|
player.add_child(streak)
|
|
var combo: Node = load("res://scenes/components/combo_window.gd").new()
|
|
combo.name = "ComboWindow"
|
|
player.add_child(combo)
|
|
var container: Node = load("res://scenes/components/effect_container.gd").new()
|
|
container.name = "EffectContainer"
|
|
player.add_child(container)
|
|
var buff: Node = load("res://scenes/components/attack_buff_component.gd").new()
|
|
buff.name = "AttackBuffComponent"
|
|
player.add_child(buff)
|
|
|
|
var boss_script := GDScript.new()
|
|
boss_script.source_code = "extends Node2D\nvar combat_enabled := false\nvar stationary := true\n"
|
|
boss_script.reload()
|
|
var boss := Node2D.new()
|
|
boss.name = "Boss"
|
|
boss.set_script(boss_script)
|
|
actors.add_child(boss)
|
|
return scene
|
|
|
|
|
|
func _expect_bool(actual: bool, expected: bool, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _expect_int(actual: int, expected: int, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
|
|
|
|
|
func _expect_string(actual: String, expected: String, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _finish() -> void:
|
|
paused = false
|
|
if failures.is_empty():
|
|
print("PASS game flow")
|
|
quit(0)
|
|
else:
|
|
for failure: String in failures:
|
|
push_error(failure)
|
|
quit(1)
|