Files
mtgodot-poc/project/test_no_auto_move_regression.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

234 lines
7.2 KiB
GDScript

# test_no_auto_move_regression.gd
# Regression test suite verifying that the character does not move automatically
# without user input under any circumstances (target picking, skill casting, attack key release).
extends SceneTree
const PlayerCtl = preload("res://player_controller.gd")
const NetPlay = preload("res://net_play.gd")
const NetWorld = preload("res://net_world.gd")
var _failed := 0
func _check(condition: bool, msg: String) -> void:
if condition:
print(" [PASS] %s" % msg)
else:
print(" [FAIL] %s" % msg)
_failed += 1
class MockClient extends Node:
signal entity_main_set(vid: int)
signal entity_moved(vid: int)
signal points_changed(points: Dictionary)
signal vitals_changed(vid: int)
signal target_info(vid: int, hp_percent: int)
signal entity_despawned(vid: int)
signal entity_dead(vid: int)
var ents: Dictionary = {}
func get_entity(vid: int) -> Dictionary:
return ents.get(vid, {})
func is_in_game() -> bool:
return true
func set_target(_vid: int) -> void:
pass
func move(_func_id: int, _arg: int, _rot: int, _x: int, _y: int) -> void:
pass
func fishing(_rot: float) -> bool:
return false
func _init() -> void:
print("=== Running test_no_auto_move_regression ===")
_test_pick_actor_engages_auto_attack_and_can_cancel()
_test_skill_cast_clears_movement_and_reservation()
_test_spacebar_attack_release_stops_movement()
_test_manual_movement_clears_reservation()
_test_clear_target_stops_pc()
if _failed == 0:
print("=== ALL NO-AUTO-MOVE REGRESSION TESTS PASSED (0 failures) ===")
quit(0)
else:
printerr("=== TESTS FAILED: %d errors ===" % _failed)
quit(1)
func _setup_rig() -> Dictionary:
var client := MockClient.new()
var net_world := NetWorld.new()
var pc := PlayerCtl.new()
var player_node := Node3D.new()
player_node.position = Vector3(0, 0, 0)
pc.player = player_node
pc.active = true
var np := NetPlay.new()
np.setup(client, pc, net_world)
# Main player entity
client.ents[1000] = {
"vid": 1000,
"kind": 0,
"empire": 1,
"x": 0, "y": 0, "z": 0,
"dead": false,
"in_safe": false
}
np._main_vid = 1000
# Mob entity at (20, 0, 0)
var mob_node := Node3D.new()
mob_node.position = Vector3(20, 0, 0)
mob_node.set_meta("vid", 2000)
net_world._by_vid[2000] = mob_node
client.ents[2000] = {
"vid": 2000,
"kind": 2, # KIND_MONSTER
"empire": 0,
"x": 2000, "y": 0, "z": 0,
"dead": false,
"in_safe": false
}
return {
"client": client,
"net_world": net_world,
"pc": pc,
"player_node": player_node,
"np": np,
"mob_node": mob_node
}
func _cleanup_rig(rig: Dictionary) -> void:
rig.mob_node.free()
rig.player_node.free()
rig.pc.free()
rig.np.free()
rig.net_world.free()
rig.client.free()
# 1. Clicking an enemy sets _auto_attack_vid to target (40250 __SetAutoAttackTargetActorID)
func _test_pick_actor_engages_auto_attack_and_can_cancel() -> void:
print("\n--- Test 1: Actor pick engages auto attack (40250 parity) and can be cancelled ---")
var rig := _setup_rig()
var np: NetPlay = rig.np
var mob_node: Node3D = rig.mob_node
# Simulate clicking the mob
np._on_pick(mob_node)
_check(np._target_vid == 2000, "Target VID is set to picked mob")
_check(np._auto_attack_vid == 2000, "_auto_attack_vid is set to mob VID for auto attack")
# Manual movement (WASD) cancels auto-attack
np.on_manual_move()
_check(np._auto_attack_vid == 0, "_auto_attack_vid cleared on manual move")
_cleanup_rig(rig)
# 2. Casting a skill must stop character movement, clear reservations, and not resume after finish
func _test_skill_cast_clears_movement_and_reservation() -> void:
print("\n--- Test 2: Skill cast clears movement and does not resume after skill ends ---")
var rig := _setup_rig()
var np: NetPlay = rig.np
var pc: PlayerCtl = rig.pc
var player_node: Node3D = rig.player_node
var mob_node: Node3D = rig.mob_node
# Player targets mob and begins moving towards it
np._on_pick(mob_node)
pc.walk_to(Vector3(15, 0, 0))
_check(pc.is_going() == true, "pc is walking before skill cast")
# Cast skill (e.g. warrior mental skill)
np.start_skill_cast(0.5, false)
_check(pc.is_going() == false, "pc.is_going() is immediately stopped on skill cast")
_check(np._reserved_mode == NetPlay.ReservedMode.NONE, "np._reserved_mode cleared on skill cast")
_check(np._auto_attack_vid == 0, "np._auto_attack_vid is 0 on skill cast")
var initial_pos := player_node.position
# Simulate frames during skill lock
for i in 5:
np._process(0.1)
pc._process(0.1)
_check(player_node.position == initial_pos, "player remains stationary during skill cast")
# Simulate skill timeout / unlocking
np._local_time += 1.0
_check(np.is_lock() == false, "np is unlocked after skill duration")
# Simulate frames AFTER skill completes (no player input)
for i in 10:
np._process(0.1)
pc._process(0.1)
_check(pc.is_going() == false, "pc.is_going() remains false after skill completion")
_check(np._reserved_mode == NetPlay.ReservedMode.NONE, "reservation remains NONE after skill completion")
_check(np._auto_attack_vid == 0, "_auto_attack_vid remains 0 after skill completion")
_check(player_node.position == initial_pos, "player position unchanged (no spontaneous movement!)")
_cleanup_rig(rig)
# 3. Spacebar attack release stops movement and clears auto-attack
func _test_spacebar_attack_release_stops_movement() -> void:
print("\n--- Test 3: Releasing attack key (Spacebar) cancels auto-attack and stops movement ---")
var rig := _setup_rig()
var np: NetPlay = rig.np
var pc: PlayerCtl = rig.pc
np._target_vid = 2000
np.set_attack_key(true)
np._update_auto_attack()
_check(np._auto_attack_vid == 2000, "_auto_attack_vid set while holding space with target")
# Releasing attack key
np.set_attack_key(false)
_check(np._auto_attack_vid == 0, "_auto_attack_vid cleared on spacebar release")
_check(np._reserved_mode == NetPlay.ReservedMode.NONE, "_reserved_mode cleared on spacebar release")
_check(pc.is_going() == false, "pc stopped on spacebar release")
_cleanup_rig(rig)
# 4. Manual movement (WASD) cancels any pending reserved actions and auto-attack
func _test_manual_movement_clears_reservation() -> void:
print("\n--- Test 4: Manual movement cancels reservations and auto-attack ---")
var rig := _setup_rig()
var np: NetPlay = rig.np
var pc: PlayerCtl = rig.pc
np._reserve_click_actor(2000)
np._auto_attack_vid = 2000
_check(np._reserved_mode == NetPlay.ReservedMode.CLICK_ACTOR, "Reserved mode is CLICK_ACTOR")
# Player presses WASD -> on_manual_move() called
np.on_manual_move()
_check(np._reserved_mode == NetPlay.ReservedMode.NONE, "on_manual_move cleared reserved action")
_check(np._auto_attack_vid == 0, "on_manual_move cleared auto-attack")
_cleanup_rig(rig)
# 5. Clearing target stops pc
func _test_clear_target_stops_pc() -> void:
print("\n--- Test 5: Clearing target stops player controller ---")
var rig := _setup_rig()
var np: NetPlay = rig.np
var pc: PlayerCtl = rig.pc
np._target_vid = 2000
pc.walk_to(Vector3(10, 0, 0))
_check(pc.is_going() == true, "pc is going towards destination")
np._clear_target()
_check(np._target_vid == 0, "target vid is cleared")
_check(pc.is_going() == false, "pc.is_going() cleared on target clear")
_check(np._auto_attack_vid == 0, "_auto_attack_vid is 0")
_check(np._reserved_mode == NetPlay.ReservedMode.NONE, "reserved mode is NONE")
_cleanup_rig(rig)