Initial project sync
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
extends SceneTree
|
||||
|
||||
var failures: Array[String] = []
|
||||
var started: Array[StringName] = []
|
||||
var judged: Array[Dictionary] = []
|
||||
|
||||
|
||||
func _init() -> void:
|
||||
_run.call_deferred()
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var fixture := await _controller_fixture()
|
||||
if fixture.is_empty():
|
||||
_finish()
|
||||
return
|
||||
var controller: Node = fixture["controller"]
|
||||
var combo: Node = fixture["combo"]
|
||||
var state: Node = fixture["state"]
|
||||
controller.connect("action_started", _on_action_started)
|
||||
_event_bus().connect("judgement_made", _on_judgement_made)
|
||||
|
||||
controller.call("submit_intent", _intent(&"A", &"a", &"pressed", "perfect", 20))
|
||||
_expect(started.has(&"ground_attack_left_1"), "A after_tap should execute the normal A action first")
|
||||
_expect_array(combo.call("get_slots"), [&"A"], "A after_tap should enter ComboWindow")
|
||||
controller.call("_tick_charge_hold", 0.2)
|
||||
controller.call("_reset_to_idle", false)
|
||||
controller.call("_tick_charge_hold", 0.0)
|
||||
_expect_equal(state.call("build_context").get("action_phase"), &"Charging", "A after_tap should enter Charging after hold threshold")
|
||||
controller.call("_tick_charge_hold", 0.5)
|
||||
controller.call("submit_intent", _intent(&"A", &"a", &"released", "perfect", 21))
|
||||
_expect(started.has(&"jian_yu_left_lv2"), "A release should select a left blade rain action by time spent charging")
|
||||
|
||||
controller.call("_reset_to_idle")
|
||||
combo.call("clear", &"test-reset")
|
||||
started.clear()
|
||||
judged.clear()
|
||||
controller.call("submit_intent", _intent(&"A", &"a", &"pressed", "perfect", 30))
|
||||
controller.call("_tick_charge_hold", 0.2)
|
||||
controller.call("_reset_to_idle", false)
|
||||
controller.call("_tick_charge_hold", 0.0)
|
||||
_expect_equal(state.call("build_context").get("action_phase"), &"Charging", "Held A should be charging before SP arrives")
|
||||
controller.call("submit_intent", _intent(&"SP", &"space", &"pressed", "perfect", 31))
|
||||
_expect(started.has(&"dash_slash_left"), "SP during a held-A charge should break the charge into the [A][sp] dash")
|
||||
_expect(not started.has(&"jian_yu_left_lv1"), "SP during a held-A charge must not cast blade rain")
|
||||
controller.call("submit_intent", _intent(&"A", &"a", &"released", "perfect", 32))
|
||||
_expect(not started.has(&"jian_yu_left_lv1"), "Releasing A after the dash derive must not cast blade rain")
|
||||
|
||||
controller.call("_reset_to_idle")
|
||||
combo.call("clear", &"test-reset")
|
||||
started.clear()
|
||||
judged.clear()
|
||||
controller.call("submit_intent", _intent(&"S", &"s", &"pressed", "perfect", 22))
|
||||
_expect_array(combo.call("get_slots"), [&"S"], "S should enter ComboWindow as a normal down action")
|
||||
_expect(started.has(&"block_start"), "S should start block_start")
|
||||
_expect_int(int(judged[judged.size() - 1].get("beat")), 22, "S should emit judgement feedback using its timestamp")
|
||||
|
||||
controller.call("_reset_to_idle")
|
||||
combo.call("clear", &"test-reset")
|
||||
started.clear()
|
||||
judged.clear()
|
||||
controller.call("submit_intent", _intent(&"S", &"s", &"pressed", "perfect", 24))
|
||||
controller.call("submit_intent", _intent(&"S", &"s", &"released", "perfect", 24))
|
||||
_expect(started.has(&"block_start"), "S release should not add a second action")
|
||||
_expect_array(combo.call("get_slots"), [&"S"], "S release should not add another ComboWindow entry")
|
||||
_expect_int(judged.size(), 1, "S release should not be judged as a second press")
|
||||
controller.call("_tick_charge_hold", 1.0)
|
||||
controller.call("_reset_to_idle", false)
|
||||
controller.call("_tick_charge_hold", 1.0)
|
||||
_expect_equal(state.call("build_context").get("action_phase"), &"Neutral", "Released S should cancel charge tracking instead of entering Charging later")
|
||||
|
||||
controller.call("_reset_to_idle")
|
||||
started.clear()
|
||||
judged.clear()
|
||||
controller.call("submit_intent", _intent(&"S", &"s", &"pressed", "perfect", 25))
|
||||
_expect(started.has(&"block_start"), "Second timed S should trigger block_start again")
|
||||
_expect_array(combo.call("get_slots"), [&"S", &"S"], "Repeated S should remain visible as two timed block inputs")
|
||||
_expect_int(int(judged[judged.size() - 1].get("beat")), 25, "Repeated S should judge the second press")
|
||||
|
||||
controller.call("_reset_to_idle")
|
||||
combo.call("clear", &"test-reset")
|
||||
combo.call("record", &"S")
|
||||
combo.call("record", &"SP")
|
||||
started.clear()
|
||||
judged.clear()
|
||||
controller.call("submit_intent", _intent(&"S", &"s", &"pressed", "perfect", 26))
|
||||
_expect(started.has(&"block_start"), "S should remain a plain block input after earlier S/SP slots")
|
||||
_expect_array(combo.call("get_slots"), [&"S", &"SP", &"S"], "S should append normally without blade-chain clearing")
|
||||
|
||||
controller.call("_reset_to_idle")
|
||||
combo.call("clear", &"test-reset")
|
||||
started.clear()
|
||||
judged.clear()
|
||||
controller.call("submit_intent", _intent(&"S", &"s", &"pressed", "perfect", 27))
|
||||
controller.call("_tick_charge_hold", 1.0)
|
||||
controller.call("_reset_to_idle", false)
|
||||
controller.call("_tick_charge_hold", 0.0)
|
||||
_expect_equal(state.call("build_context").get("action_phase"), &"Charging", "Held S should enter Charging only while the key remains held")
|
||||
controller.call("_tick_charge_hold", 1.0)
|
||||
controller.call("submit_intent", _intent(&"SP", &"space", &"pressed", "perfect", 28))
|
||||
_expect(started.has(&"zhan_bo_lv3"), "Held S then SP should release blade wave by time spent charging")
|
||||
|
||||
fixture["root"].free()
|
||||
_finish()
|
||||
|
||||
|
||||
func _controller_fixture() -> Dictionary:
|
||||
var combo_script: Script = load("res://scenes/components/combo_window.gd")
|
||||
var resolver_script: Script = load("res://scenes/combat/action_resolver.gd")
|
||||
var state_script: Script = load("res://scenes/components/state_machine.gd")
|
||||
var energy_script: Script = load("res://scenes/components/energy_component.gd")
|
||||
var controller_script: Script = load("res://scenes/components/action_controller.gd")
|
||||
if combo_script == null or resolver_script == null or state_script == null or energy_script == null or controller_script == null:
|
||||
failures.append("Phase 3 component scripts should load")
|
||||
return {}
|
||||
var fixture_root := Node.new()
|
||||
root.add_child(fixture_root)
|
||||
var combo: Node = combo_script.new()
|
||||
combo.name = "ComboWindow"
|
||||
fixture_root.add_child(combo)
|
||||
var resolver: Node = resolver_script.new()
|
||||
resolver.name = "ActionResolver"
|
||||
fixture_root.add_child(resolver)
|
||||
var state: Node = state_script.new()
|
||||
state.name = "StateMachine"
|
||||
fixture_root.add_child(state)
|
||||
var energy: Node = energy_script.new()
|
||||
energy.name = "EnergyComponent"
|
||||
fixture_root.add_child(energy)
|
||||
var controller: Node = controller_script.new()
|
||||
controller.name = "ActionController"
|
||||
controller.set("combo_window_path", NodePath("../ComboWindow"))
|
||||
controller.set("action_resolver_path", NodePath("../ActionResolver"))
|
||||
controller.set("state_machine_path", NodePath("../StateMachine"))
|
||||
fixture_root.add_child(controller)
|
||||
await process_frame
|
||||
energy.call("set_values", 99, 99)
|
||||
return {
|
||||
"root": fixture_root,
|
||||
"combo": combo,
|
||||
"controller": controller,
|
||||
"state": state,
|
||||
}
|
||||
|
||||
|
||||
func _intent(symbol: StringName, rhythm_action: StringName, event_type: StringName, label: String, beat: int) -> RefCounted:
|
||||
var intent_script: Script = load("res://scenes/components/input_intent.gd")
|
||||
var intent: RefCounted = intent_script.call("create", symbol, rhythm_action, event_type, float(Time.get_ticks_msec()))
|
||||
intent.set("judgement", {
|
||||
"label": label,
|
||||
"diff": 0.0,
|
||||
"abs_diff": 0.0,
|
||||
"nearest_beat": beat,
|
||||
})
|
||||
return intent
|
||||
|
||||
|
||||
func _on_action_started(action: Resource, _intent) -> void:
|
||||
started.append(StringName(str(action.get("id"))))
|
||||
|
||||
|
||||
func _on_judgement_made(quality: StringName, offset_ms: float, beat_index: int) -> void:
|
||||
judged.append({"quality": quality, "offset_ms": offset_ms, "beat": beat_index})
|
||||
|
||||
|
||||
func _event_bus() -> Node:
|
||||
var bus := root.get_node_or_null("EventBus")
|
||||
if bus == null:
|
||||
bus = load("res://autoload/event_bus.gd").new()
|
||||
bus.name = "EventBus"
|
||||
root.add_child(bus)
|
||||
return bus
|
||||
|
||||
|
||||
func _expect(condition: bool, label: String) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
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_array(actual: Array, expected: Array, label: String) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_equal(actual: Variant, expected: Variant, label: String) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %s, got %s" % [label, str(expected), str(actual)])
|
||||
|
||||
|
||||
func _finish() -> void:
|
||||
if failures.is_empty():
|
||||
print("PASS charge hold gate")
|
||||
quit(0)
|
||||
else:
|
||||
for failure: String in failures:
|
||||
push_error(failure)
|
||||
quit(1)
|
||||
Reference in New Issue
Block a user