212 lines
9.8 KiB
GDScript
212 lines
9.8 KiB
GDScript
extends SceneTree
|
|
|
|
const InputIntentScript := preload("res://scenes/components/input_intent.gd")
|
|
|
|
var failures: Array[String] = []
|
|
var rejected: Array[StringName] = []
|
|
var started: Array[StringName] = []
|
|
|
|
|
|
func _init() -> void:
|
|
_run.call_deferred()
|
|
|
|
|
|
func _run() -> void:
|
|
var scene: PackedScene = load("res://scenes/characters/player.tscn")
|
|
var player := scene.instantiate()
|
|
root.add_child(player)
|
|
await process_frame
|
|
|
|
var controller := player.get_node_or_null("ActionController")
|
|
_expect_bool(controller != null, true, "Player should have ActionController")
|
|
if controller == null:
|
|
player.free()
|
|
_finish()
|
|
return
|
|
|
|
controller.connect("action_rejected", _on_action_rejected)
|
|
controller.connect("action_started", _on_action_started)
|
|
|
|
player.get_node("ComboWindow").record(&"S")
|
|
var miss: RefCounted = InputIntentScript.create(&"A", &"a", &"pressed", float(Time.get_ticks_msec()))
|
|
miss.judgement = {"label": "miss", "diff": 0.0}
|
|
controller.call("submit_intent", miss)
|
|
_expect_array(player.call("get_combo_slots"), [&"S", &"Ø"], "Miss intent should enter ComboWindow as an explicit placeholder")
|
|
_expect_string(str(rejected[rejected.size() - 1]), "miss", "Miss intent rejection reason")
|
|
await create_timer(0.4).timeout
|
|
await process_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"S", &"Ø"], "Miss intent should not clear ComboWindow by itself")
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
player.get_node("EnergyComponent").set_current(10)
|
|
controller.call("_reset_to_idle")
|
|
rejected.clear()
|
|
controller.call("submit_intent", _perfect_intent(&"A", &"a"))
|
|
var startup_miss: RefCounted = InputIntentScript.create(&"D", &"d", &"pressed", float(Time.get_ticks_msec()))
|
|
startup_miss.judgement = {"label": "miss", "diff": 0.0}
|
|
controller.call("submit_intent", startup_miss)
|
|
_expect_array(player.call("get_combo_slots"), [&"A", &"Ø"], "Miss during an action should enter ComboWindow immediately")
|
|
if rejected.is_empty():
|
|
failures.append("Miss during an action should be rejected immediately: no rejection emitted")
|
|
else:
|
|
_expect_string(str(rejected[rejected.size() - 1]), "miss", "Miss during an action should be rejected immediately")
|
|
await create_timer(0.4).timeout
|
|
await process_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"A", &"Ø"], "Miss during an action should not clear ComboWindow by itself")
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
|
|
started.clear()
|
|
player.get_node("EnergyComponent").set_current(0)
|
|
var first: RefCounted = InputIntentScript.create(&"A", &"a", &"pressed", float(Time.get_ticks_msec()))
|
|
first.judgement = {"label": "perfect", "diff": 0.0}
|
|
controller.call("submit_intent", first)
|
|
_expect_int(player.call("get_energy"), 0, "A reward should wait until active phase")
|
|
var second: RefCounted = InputIntentScript.create(&"A", &"a", &"pressed", float(Time.get_ticks_msec()))
|
|
second.judgement = {"label": "perfect", "diff": 0.0}
|
|
controller.call("submit_intent", second)
|
|
_expect_array(player.call("get_combo_slots"), [&"A"], "Input during startup should wait outside ComboWindow")
|
|
_expect_int(started.size(), 1, "Second input during startup should not start a second action immediately")
|
|
await create_timer(0.2).timeout
|
|
await physics_frame
|
|
_expect_int(player.call("get_energy"), 1, "A reward should apply once active phase begins")
|
|
await create_timer(0.48).timeout
|
|
await physics_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"A", &"A"], "Pending input should enter ComboWindow only when cancel window opens")
|
|
_expect_bool(started.size() >= 2, true, "Pending input should start after phase completes or cancel window opens")
|
|
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
player.get_node("EnergyComponent").set_current(10)
|
|
controller.call("_reset_to_idle")
|
|
rejected.clear()
|
|
started.clear()
|
|
controller.call("submit_intent", _perfect_intent(&"A", &"a"))
|
|
controller.call("submit_intent", _perfect_intent(&"D", &"d"))
|
|
controller.call("submit_intent", _perfect_intent(&"S", &"s"))
|
|
_expect_array(player.call("get_combo_slots"), [&"A"], "Rapid inputs should not advance ComboWindow at raw input speed")
|
|
_expect_bool(rejected.has(&"replaced"), true, "Rapid inputs should replace the single pending intent")
|
|
await create_timer(0.5).timeout
|
|
await physics_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"A", &"S"], "Only the latest pending intent should enter ComboWindow at cancel time")
|
|
_expect_int(started.size(), 2, "Replaced rapid inputs should start only the initial action and the latest pending fallback")
|
|
_expect_bool(started.has(&"skill_s"), true, "Latest pending S should fall back to normal S when A+S is unresolved")
|
|
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
player.get_node("EnergyComponent").set_current(10)
|
|
controller.call("_reset_to_idle")
|
|
started.clear()
|
|
controller.call("submit_intent", _perfect_intent(&"A", &"a"))
|
|
controller.call("submit_intent", _perfect_intent(&"SP", &"space"))
|
|
await create_timer(0.55).timeout
|
|
await physics_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"A", &"SP"], "Clear-window action should show its full input pattern")
|
|
await create_timer(0.2).timeout
|
|
await physics_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"A", &"SP"], "Clear-window action should not clear from a stale display timer before it finishes")
|
|
await create_timer(0.4).timeout
|
|
await physics_frame
|
|
_expect_array(player.call("get_combo_slots"), [], "Clear-window action should clear when its action finishes")
|
|
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
player.get_node("EnergyComponent").set_current(10)
|
|
player.get_node("ChargeComponent").cancel()
|
|
controller.call("_reset_to_idle")
|
|
started.clear()
|
|
player.call("_on_input_intent_created", _perfect_intent(&"D", &"d"))
|
|
player.call("_on_input_intent_created", _release_intent(&"D", &"d"))
|
|
player.call("_on_input_intent_created", _perfect_intent(&"D", &"d"))
|
|
player.call("_on_input_intent_created", _release_intent(&"D", &"d"))
|
|
await create_timer(0.55).timeout
|
|
await physics_frame
|
|
_expect_array(player.call("get_combo_slots"), [&"D", &"D"], "Released pending D should still be adjudicated into DD at cancel time")
|
|
await create_timer(0.8).timeout
|
|
await process_frame
|
|
_expect_bool(player.call("is_charge_active"), false, "Released pending D should not leave player stuck charging")
|
|
_expect_int(int(round(float(player.call("get_charge")))), 0, "Released pending D should not accumulate charge")
|
|
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
player.get_node("EnergyComponent").set_current(10)
|
|
controller.call("_reset_to_idle")
|
|
started.clear()
|
|
controller.call("submit_intent", _perfect_intent(&"S", &"s"))
|
|
controller.call("_reset_to_idle")
|
|
controller.call("submit_intent", _perfect_intent(&"SP", &"space"))
|
|
controller.call("_reset_to_idle")
|
|
controller.call("submit_intent", _perfect_intent(&"S", &"s"))
|
|
controller.call("_reset_to_idle")
|
|
controller.call("submit_intent", _perfect_intent(&"SP", &"space"))
|
|
_expect_bool(started.has(&"skill_s_projectile_1"), true, "First S+SP should start projectile")
|
|
_expect_bool(started.count(&"skill_s_projectile_1") >= 2, true, "Second S+SP should restart projectile chain")
|
|
|
|
player.get_node("ComboWindow").clear(&"test-reset")
|
|
player.get_node("EnergyComponent").set_current(10)
|
|
controller.call("_reset_to_idle")
|
|
started.clear()
|
|
rejected.clear()
|
|
player.get_node("ComboWindow").record(&"A")
|
|
player.get_node("ComboWindow").record(&"D")
|
|
controller.call("submit_intent", _perfect_intent(&"S", &"s"))
|
|
controller.call("_reset_to_idle")
|
|
controller.call("submit_intent", _perfect_intent(&"SP", &"space"))
|
|
_expect_bool(started.has(&"skill_s_projectile_1"), true, "S+SP should start projectile from the trailing two slots")
|
|
_expect_bool(not rejected.has(&"unresolved"), true, "Trailing S+SP should not be rejected as unresolved")
|
|
|
|
var context: Dictionary = controller.call("_resolver_context")
|
|
_expect_bool(context.has("state"), true, "Resolver context should include state")
|
|
_expect_bool(context.has("blade_chain_active"), true, "Resolver context should include blade_chain_active")
|
|
_expect_bool(context.has("counter_ready"), true, "Resolver context should include counter_ready")
|
|
_expect_bool(context.has("burst_action_id"), true, "Resolver context should include burst_action_id")
|
|
|
|
player.free()
|
|
_finish()
|
|
|
|
|
|
func _on_action_rejected(_intent, reason: StringName) -> void:
|
|
rejected.append(reason)
|
|
|
|
|
|
func _on_action_started(action: Resource, _intent) -> void:
|
|
started.append(StringName(str(action.get("id"))))
|
|
|
|
|
|
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_string(actual: String, expected: String, label: String) -> void:
|
|
if actual != expected:
|
|
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
|
|
|
|
|
func _expect_array(actual: Array, expected: Array, 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 _perfect_intent(symbol: StringName, rhythm_action: StringName) -> RefCounted:
|
|
var intent: RefCounted = InputIntentScript.create(symbol, rhythm_action, &"pressed", float(Time.get_ticks_msec()))
|
|
intent.judgement = {"label": "perfect", "diff": 0.0}
|
|
return intent
|
|
|
|
|
|
func _release_intent(symbol: StringName, rhythm_action: StringName) -> RefCounted:
|
|
var intent: RefCounted = InputIntentScript.create(symbol, rhythm_action, &"released", float(Time.get_ticks_msec()))
|
|
intent.judgement = {"label": "perfect", "diff": 0.0}
|
|
return intent
|
|
|
|
|
|
func _finish() -> void:
|
|
if failures.is_empty():
|
|
print("PASS action controller flow")
|
|
quit(0)
|
|
else:
|
|
for failure: String in failures:
|
|
push_error(failure)
|
|
quit(1)
|