extends SceneTree const NetPlay = preload("res://net_play.gd") # test_combo_continuous_flow.gd # 验证 40250 连击状态机:长按空格、连点空格与目标追击时的连击链(1->2->3->4)顺畅推进, # 绝无因外层 _attack_cd 吞键或卡死在第 1 段的现象。 class FakeClient extends Node: var ents := {} var attacks := [] var moves := [] var _in_game := true signal damage(vid: int, amount: int, flag: int) signal target_info(vid: int, hp_pct: int) signal entity_despawned(vid: int) signal entity_dead(vid: int) signal combo_changed(enabled: bool) signal observer_mode_changed(enabled: bool) signal entity_main_set(vid: int) signal entity_moved(vid: int, x: int, y: int, rot: float, dur: int) signal points_changed() signal vitals_changed() func is_in_game() -> bool: return _in_game func get_entity(vid: int) -> Dictionary: return ents.get(vid, {}) func get_entities() -> Array: return ents.values() func attack(skill: int, vid: int) -> bool: attacks.append([skill, vid]) return true func set_target(_vid: int) -> void: pass func move(func_type: int, arg: int, rot: float, x: int, y: int) -> void: moves.append([func_type, arg, rot, x, y]) class FakePlayerController extends Node: var player: Node3D = null var active := true var frozen := false var locked := false var moving_skill := false var pickables: Array[Node3D] = [] signal target_selected(node: Node3D) signal moved(is_moving: bool) signal anim_state(s: String) func can_move() -> bool: return active and not frozen and not locked func is_going() -> bool: return false func stop() -> void: pass func walk_to(_pos: Vector3) -> void: pass class FakePlayerView extends Node3D: var states: Array[String] = [] var attack_motions: Array = [] var anim: FakeAnim = FakeAnim.new() var attack_md := {} signal motion_bound(state: String) func play_attack_motion(mode: int, index: int, speed: float) -> void: attack_motions.append([mode, index, speed]) states.append("attack") if not attack_md.is_empty(): anim.md = attack_md func set_anim_state(s: String) -> void: states.append(s) func is_in_hit_reaction() -> bool: return false class FakeAnim: var md := {} func get_motion_data() -> Dictionary: return md var _fail := 0 func _ck(cond: bool, msg: String) -> void: if cond: print(" [PASS] " + msg) else: _fail += 1 printerr(" [FAIL] " + msg) func _init() -> void: print("=== Running test_combo_continuous_flow ===") await _run_tests() if _fail == 0: print("\n=== ALL COMBO TESTS PASSED (0 failures) ===\n") quit(0) else: printerr("\n=== %d COMBO TEST(S) FAILED ===\n" % _fail) quit(1) func _run_tests() -> void: var fc := FakeClient.new() fc.ents[1000] = {"vid": 1000, "kind": 0, "name": "Hero", "dead": false, "in_safe": false} fc.ents[2000] = {"vid": 2000, "kind": 2, "name": "WildDog", "dead": false, "in_safe": false} var pcn := Node3D.new() get_root().add_child(pcn) pcn.position = Vector3.ZERO var pc := FakePlayerController.new() pc.player = pcn var pv := FakePlayerView.new() get_root().add_child(pv) var np: Node = NetPlay.new() get_root().add_child(np) np.setup(fc, pc, null, null) np.player_view = pv np._main_vid = 1000 # 注册 4 段单手剑 Combo 段表 (motions: 14, 15, 16, 17) var K: int = (np.MOTION_MODE_ONEHAND_SWORD << 16) | 0 np._combo_tables = {0: {K: PackedInt32Array([14, 15, 16, 17])}} np.combo_class = 0 np.combo_motion_mode = np.MOTION_MODE_ONEHAND_SWORD # 配置带有 ComboInputData 的 motion 属性 (典型单手剑连击:长 0.8s, 输入窗 0.15~0.5s, 0.25s 切下一段) pv.attack_md = { "has_combo_input": true, "duration": 0.8, "pre_input_time": 0.15, "direct_input_time": 0.25, "input_limit_time": 0.50, "next_combo": 0.25, } pv.anim.md = pv.attack_md var _unused_md := { "has_combo_input": true, "duration": 0.8, "pre_input_time": 0.15, "direct_input_time": 0.25, "input_limit_time": 0.50, "next_combo": 0.25, } await process_frame print("\n--- Test 1: Space Key Hold Down Continuous 4-Hit Combo Chain ---") # 玩家长按空格键 np.set_attack_key(true) # 帧 1:发起第 1 段 (段号 14) np._process(0.016) _ck(np._combo_index == 1, "Frame 1: Space hold triggers Combo 1 (got index %d)" % np._combo_index) _ck(pv.attack_motions.size() >= 1 and pv.attack_motions[-1][1] == 14, "Combo 1 plays motion 14") # 时间推进到 0.10s (在 pre_input_time 0.15 之前):保持在段 1,且未挂起预输入 np._local_time = np._motion_start_t + 0.10 np._process(0.016) _ck(np._combo_index == 1 and not np._is_pre_input, "t=0.10s (<0.15s): still in combo 1, no pre-input") # 时间推进到 0.20s (在 [0.15s, 0.25s] 预输入窗内):因为空格仍按着,成功挂起预输入 np._local_time = np._motion_start_t + 0.20 np._process(0.016) _ck(np._combo_index == 1 and np._is_pre_input, "t=0.20s in [0.15, 0.25]: pre-input successfully latched") # 时间推进到 0.26s (>0.25s NextComboTime):ComboProcess 到点,自动推进到第 2 段 (段号 15)! np._local_time = np._motion_start_t + 0.26 np._process(0.016) _ck(np._combo_index == 2 and not np._is_pre_input, "t=0.26s (>0.25s): ComboProcess advances to Combo 2 (index %d)" % np._combo_index) _ck(pv.attack_motions.size() >= 2 and pv.attack_motions[-1][1] == 15, "Combo 2 plays motion 15") # 时间推进到段 2 的 0.20s 预输入窗 np._local_time = np._motion_start_t + 0.20 np._process(0.016) _ck(np._combo_index == 2 and np._is_pre_input, "Combo 2 t=0.20s: pre-input latched for Combo 3") # 时间推进到段 2 的 0.26s -> 自动切入第 3 段 (段号 16)! np._local_time = np._motion_start_t + 0.26 np._process(0.016) _ck(np._combo_index == 3 and not np._is_pre_input, "Combo 3 triggered (index %d, motion 16)" % np._combo_index) # 时间推进到段 3 的 0.20s -> 预输入 np._local_time = np._motion_start_t + 0.20 np._process(0.016) _ck(np._combo_index == 3 and np._is_pre_input, "Combo 3 t=0.20s: pre-input latched for Combo 4") # 时间推进到段 3 的 0.26s -> 自动切入最后一段 (段 4,段号 17)! np._local_time = np._motion_start_t + 0.26 np._process(0.016) _ck(np._combo_index == 4 and not np._is_pre_input, "Combo 4 triggered (index %d, motion 17, last segment)" % np._combo_index) # 段 4 播放完毕 (0.8s),回到 wait,重置 combo 索引 np._local_time = np._motion_start_t + 0.85 pv.anim.md = {"has_combo_input": false, "duration": 2.0} pv.motion_bound.emit("wait") _ck(np._combo_index == 0, "Motion finished back to wait -> combo_index resets to 0") # 下一帧因为空格一直按住,立即无缝开启下一轮 Combo 1 np._process(0.016) _ck(np._combo_index == 1, "Next frame: Space still held down seamlessly restarts Combo 1") # 松开空格 np.set_attack_key(false) print("\n--- Test 2: Rapid Space Key Tapping ---") # 模拟连续按空格(每次按下抬起) np._clear_combo() np.set_attack_key(true) np._process(0.016) _ck(np._combo_index == 1, "Tap 1: starts Combo 1") np.set_attack_key(false) # 在直接输入窗内 (t=0.30s > 0.25s) 再次按空格 np._local_time = np._motion_start_t + 0.30 np.set_attack_key(true) np._process(0.016) _ck(np._combo_index == 2, "Tap 2 at t=0.30s (>NextComboTime) immediately advances to Combo 2") np.set_attack_key(false)