Files
mtgodot-poc/project/test_combo_continuous_flow.gd
T
shenandshen 66d217b313 feat(client): 完成40250客户端核心功能1:1对齐与桥梁高度采样修复
- 桥梁与静态物体高度采样修复:
  - 严格对齐 40250 CMapOutdoor::GetHeight 与 CAttributeInstance::GetHeight
  - 解析 .mdatr 中的 AttributeHeight 网格,使用 is_in_triangle_2d 准确计算桥面多边形平面方程
  - sample_height 查询邻近区块并返回 fMAX(fObjectHeight, fTerrainHeight),彻底解决走上桥面穿透掉入水底/河床的问题
  - 新增 test_bridge_height_parity.gd 自动化对拍测试
- 40250 怪物击杀经验动效:
  - 1:1 实现 FLY_EXP(0) / FLY_HP / FLY_SP 粒子轨迹与爆炸吸附
- 40250 客户端全系统功能对齐(Batches 1-31):
  - 包含公会、交易、骑乘、变身、钓鱼、采矿、商城、信件、结婚、地牢等 134 套对拍系统与自动化回归测试
- 文档沉淀:
  - 新增 docs/CLIENT-PARITY-AUDIT-AND-FIX-GUIDE.md 客户端对拍缺陷发现与修复工程指南
2026-09-19 08:51:25 -07:00

211 lines
7.2 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)