- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
234 lines
7.2 KiB
GDScript
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)
|