- 桥梁与静态物体高度采样修复: - 严格对齐 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 客户端对拍缺陷发现与修复工程指南
372 lines
14 KiB
GDScript
372 lines
14 KiB
GDScript
extends SceneTree
|
|
|
|
const SafeboxUI = preload("res://ui/safebox_ui.gd")
|
|
const MallUI = preload("res://ui/mall_ui.gd")
|
|
|
|
var _pass := 0
|
|
var _fail := 0
|
|
|
|
func _ck(ok: bool, msg: String) -> void:
|
|
if ok:
|
|
_pass += 1
|
|
print(" [PASS] %s" % msg)
|
|
else:
|
|
_fail += 1
|
|
printerr(" [FAIL] %s" % msg)
|
|
|
|
class MockProto extends Node:
|
|
func item(vnum: int) -> Dictionary:
|
|
match vnum:
|
|
10: return {"name": "Sword+0", "locale_name": "单手剑+0", "type": 1, "sub_type": 0}
|
|
19: return {"name": "Sword+9", "locale_name": "单手剑+9", "type": 1, "sub_type": 0}
|
|
27001: return {"name": "Red Potion(S)", "locale_name": "小型红药水", "type": 3, "sub_type": 0}
|
|
27993: return {"name": "Pearl", "locale_name": "珍珠", "type": 3, "sub_type": 0}
|
|
_: return {"name": "Item%d" % vnum, "locale_name": "道具%d" % vnum, "type": 3, "sub_type": 0}
|
|
|
|
class MockMouseController extends Node:
|
|
var _attached: Dictionary = {}
|
|
var _targets: Dictionary = {}
|
|
|
|
func is_attached() -> bool:
|
|
return not _attached.is_empty()
|
|
|
|
func attached() -> Dictionary:
|
|
return _attached
|
|
|
|
func attach_item(win: int, cell: int, vnum: int, count: int, _tex = null, _owner = null) -> void:
|
|
_attached = {"window": win, "cell": cell, "vnum": vnum, "count": count}
|
|
|
|
func cancel() -> void:
|
|
_attached.clear()
|
|
|
|
func register_owner(_o) -> void:
|
|
pass
|
|
|
|
func unregister_owner(_o) -> void:
|
|
pass
|
|
|
|
func register_target(rect: Control, cb: Callable, _owner = null) -> void:
|
|
_targets[rect] = cb
|
|
|
|
func unregister_target(rect: Control) -> void:
|
|
_targets.erase(rect)
|
|
|
|
class MockClient extends Node:
|
|
signal safebox_opened(size: int)
|
|
signal safebox_closed()
|
|
signal safebox_changed()
|
|
signal safebox_password_required()
|
|
signal safebox_wrong_password()
|
|
signal mall_opened(size: int)
|
|
signal mall_closed()
|
|
signal mall_changed()
|
|
signal mall_password_required()
|
|
signal inventory_changed()
|
|
|
|
var _safebox_open := false
|
|
var _safebox_size := 1
|
|
var _safebox_gold := 0
|
|
var _safebox_items := []
|
|
var _mall_open := false
|
|
var _mall_size := 45
|
|
var _mall_items := []
|
|
var _inventory := []
|
|
var _char_pos := Vector3.ZERO
|
|
var _main_vid := 10001
|
|
var calls: Array[Dictionary] = []
|
|
|
|
func get_main_vid() -> int: return _main_vid
|
|
|
|
func get_entity(vid: int) -> Dictionary:
|
|
if vid == _main_vid:
|
|
return {"vid": vid, "pos": _char_pos}
|
|
return {}
|
|
|
|
func is_safebox_open() -> bool: return _safebox_open
|
|
func get_safebox_size() -> int: return _safebox_size
|
|
func get_safebox_gold() -> int: return _safebox_gold
|
|
func get_safebox_items() -> Array: return _safebox_items
|
|
|
|
func is_mall_open() -> bool: return _mall_open
|
|
func get_mall_size() -> int: return _mall_size
|
|
func get_mall_items() -> Array: return _mall_items
|
|
|
|
func get_inventory() -> Array: return _inventory
|
|
|
|
func safebox_checkin(safe_pos: int, inv_window: int, inv_cell: int) -> bool:
|
|
calls.append({"method": "safebox_checkin", "args": [safe_pos, inv_window, inv_cell]})
|
|
return true
|
|
|
|
func safebox_checkout(safe_pos: int, inv_window: int, inv_cell: int) -> bool:
|
|
calls.append({"method": "safebox_checkout", "args": [safe_pos, inv_window, inv_cell]})
|
|
return true
|
|
|
|
func safebox_move(from_cell: int, to_cell: int, count: int) -> bool:
|
|
calls.append({"method": "safebox_move", "args": [from_cell, to_cell, count]})
|
|
return true
|
|
|
|
func safebox_close() -> bool:
|
|
_safebox_open = false
|
|
calls.append({"method": "safebox_close", "cmd": "/safebox_close"})
|
|
return true
|
|
|
|
func mall_checkout(mall_pos: int, inv_window: int, inv_cell: int) -> bool:
|
|
calls.append({"method": "mall_checkout", "args": [mall_pos, inv_window, inv_cell]})
|
|
return true
|
|
|
|
func mall_close() -> bool:
|
|
_mall_open = false
|
|
calls.append({"method": "mall_close", "cmd": "/mall_close"})
|
|
return true
|
|
|
|
func safebox_password(pw: String) -> bool:
|
|
calls.append({"method": "safebox_password", "cmd": "/safebox_password " + pw})
|
|
return true
|
|
|
|
func safebox_change_password(old_pw: String, new_pw: String) -> bool:
|
|
calls.append({"method": "safebox_change_password", "cmd": "/safebox_change_password %s %s" % [old_pw, new_pw]})
|
|
return true
|
|
|
|
func mall_password(pw: String) -> bool:
|
|
calls.append({"method": "mall_password", "cmd": "/mall_password " + pw})
|
|
return true
|
|
|
|
func _init() -> void:
|
|
print("\n=== Running test_safebox_mall_parity (Phase 4 Extension Parity) ===")
|
|
_run_all()
|
|
print("\n=======================================================")
|
|
if _fail == 0:
|
|
print("=== ALL SAFEBOX & MALL PARITY TESTS PASSED (%d checks) ===" % _pass)
|
|
quit(0)
|
|
else:
|
|
printerr("=== SOME CHECKS FAILED: %d pass, %d fail ===" % [_pass, _fail])
|
|
quit(1)
|
|
|
|
func _run_all() -> void:
|
|
var client := MockClient.new()
|
|
var proto := MockProto.new()
|
|
var mouse := MockMouseController.new()
|
|
var host := Control.new()
|
|
get_root().add_child(client)
|
|
get_root().add_child(mouse)
|
|
get_root().add_child(host)
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 1: Safebox Open/Close, Auto-Open, and 10.0m Auto-Close
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 1: Safebox Open/Close & 10.0m Limit Range (40250 uisafebox.py) ---")
|
|
var safe := SafeboxUI.new()
|
|
safe.item_mouse = mouse
|
|
host.add_child(safe)
|
|
safe.setup(client, host, proto)
|
|
|
|
_ck(not safe.is_open(), "Safebox initially closed")
|
|
client._char_pos = Vector3(100.0, 50.0, 0.0)
|
|
client._safebox_open = true
|
|
client._safebox_size = 3
|
|
client.safebox_opened.emit(3)
|
|
|
|
_ck(safe.is_open(), "Safebox auto-opened on safebox_opened signal")
|
|
_ck(safe._has_open_pos and safe._open_char_pos == Vector3(100.0, 50.0, 0.0), "Safebox recorded opening character position (100, 50, 0)")
|
|
_ck(safe._page_buttons.size() == 3, "Safebox has 3 pages configured")
|
|
|
|
# Move within 5m
|
|
client._char_pos = Vector3(104.0, 50.0, 0.0)
|
|
safe._process(0.016)
|
|
_ck(safe.is_open(), "Safebox remains open at 4.0m distance")
|
|
|
|
# Move beyond 10m
|
|
client._char_pos = Vector3(112.0, 50.0, 0.0)
|
|
safe._process(0.016)
|
|
_ck(not safe.is_open(), "Safebox auto-closed after exceeding 10.0m limit range")
|
|
var has_safe_close := false
|
|
for c in client.calls:
|
|
if c.get("method") == "safebox_close" and c.get("cmd") == "/safebox_close":
|
|
has_safe_close = true
|
|
break
|
|
_ck(has_safe_close, "Safebox auto-close sent /safebox_close chat command to server")
|
|
|
|
# Server closes safebox
|
|
client._safebox_open = true
|
|
safe.open()
|
|
_ck(safe.is_open(), "Safebox re-opened")
|
|
client.calls.clear()
|
|
client._safebox_open = false
|
|
client.safebox_closed.emit()
|
|
_ck(not safe.is_open(), "Safebox closed on safebox_closed server signal")
|
|
_ck(client.calls.is_empty(), "Safebox closed by server does not send /safebox_close back")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 2: Mall Open/Close & 10.0m Auto-Close
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 2: Mall Open/Close & 10.0m Limit Range (40250 uisafebox.py MallWindow) ---")
|
|
var mall := MallUI.new()
|
|
mall.item_mouse = mouse
|
|
host.add_child(mall)
|
|
mall.setup(client, host, proto)
|
|
|
|
_ck(not mall.is_open(), "Mall initially closed")
|
|
client._char_pos = Vector3(200.0, 80.0, 0.0)
|
|
client._mall_open = true
|
|
client._mall_size = 45
|
|
client.mall_opened.emit(45)
|
|
|
|
_ck(mall.is_open(), "Mall auto-opened on mall_opened signal")
|
|
_ck(mall._has_open_pos and mall._open_char_pos == Vector3(200.0, 80.0, 0.0), "Mall recorded opening character position (200, 80, 0)")
|
|
|
|
# Move within 6m
|
|
client._char_pos = Vector3(200.0, 86.0, 0.0)
|
|
mall._process(0.016)
|
|
_ck(mall.is_open(), "Mall remains open at 6.0m distance")
|
|
|
|
# Move beyond 10m
|
|
client._char_pos = Vector3(200.0, 95.0, 0.0)
|
|
client.calls.clear()
|
|
mall._process(0.016)
|
|
_ck(not mall.is_open(), "Mall auto-closed after exceeding 10.0m limit range")
|
|
var has_mall_close := false
|
|
for c in client.calls:
|
|
if c.get("method") == "mall_close" and c.get("cmd") == "/mall_close":
|
|
has_mall_close = true
|
|
break
|
|
_ck(has_mall_close, "Mall auto-close sent /mall_close chat command to server")
|
|
|
|
# Server closes mall
|
|
client._mall_open = true
|
|
mall.open()
|
|
_ck(mall.is_open(), "Mall re-opened")
|
|
client.calls.clear()
|
|
client._mall_open = false
|
|
client.mall_closed.emit()
|
|
_ck(not mall.is_open(), "Mall closed on mall_closed server signal")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 3: Sockets & Attrs Data Preservation
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 3: Sockets & Attrs Data Preservation in Safebox and Mall ---")
|
|
var rich_item := {
|
|
"cell": 0,
|
|
"vnum": 19,
|
|
"count": 1,
|
|
"flags": 0,
|
|
"anti_flags": 0,
|
|
"sockets": [28430, 28431, 0],
|
|
"attrs": [
|
|
{"type": 1, "value": 500},
|
|
{"type": 5, "value": 12}
|
|
]
|
|
}
|
|
client._safebox_items = [rich_item]
|
|
client._mall_items = [rich_item]
|
|
safe.refresh()
|
|
mall.refresh()
|
|
|
|
var safe_tt := safe._tooltip_for(rich_item)
|
|
var mall_tt := mall._tooltip_for(rich_item)
|
|
_ck(safe_tt.contains("500") or safe_tt.contains("HP") or safe_tt.contains("生命"), "Safebox tooltip includes attribute 1 (HP +500)")
|
|
_ck(safe_tt.contains("12") or safe_tt.contains("STR") or safe_tt.contains("力量"), "Safebox tooltip includes attribute 2 (STR +12)")
|
|
_ck(mall_tt.contains("500") or mall_tt.contains("HP") or mall_tt.contains("生命"), "Mall tooltip includes attribute 1 (HP +500)")
|
|
_ck(mall_tt.contains("12") or mall_tt.contains("STR") or mall_tt.contains("力量"), "Mall tooltip includes attribute 2 (STR +12)")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 4: Safebox Operations (Checkin, Checkout, Move)
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 4: Safebox Operations (40250 Checkin, Checkout, Move) ---")
|
|
client._safebox_open = true
|
|
safe.open()
|
|
client.calls.clear()
|
|
client._inventory = [{"cell": 5, "vnum": 27001, "count": 20}] # Free slot 0 is empty
|
|
|
|
# Drag drop from inventory (win 1, cell 5) into empty safebox slot 8
|
|
var drop_payload := {"window": 1, "cell": 5, "vnum": 27001, "count": 20}
|
|
var drop_res := safe._drop_to_slot(drop_payload, 8)
|
|
_ck(drop_res, "Dropping inventory item into empty Safebox slot succeeds")
|
|
_ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_checkin" and client.calls[0]["args"] == [8, 1, 5],
|
|
"Drop sent safebox_checkin(safe_pos=8, inv_win=1, inv_cell=5)")
|
|
|
|
# Right-click inventory item -> deposit to next free safebox slot
|
|
client.calls.clear()
|
|
client._safebox_items = [{"cell": 0, "vnum": 10, "count": 1}] # Next free is 1
|
|
safe.deposit(1, 5)
|
|
_ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_checkin" and client.calls[0]["args"] == [1, 1, 5],
|
|
"Right-clicking inventory item triggers deposit into next free safebox slot 1")
|
|
|
|
# Right-click safebox slot -> checkout to first free inventory slot
|
|
client.calls.clear()
|
|
var ev_rc := InputEventMouseButton.new()
|
|
ev_rc.button_index = MOUSE_BUTTON_RIGHT
|
|
ev_rc.pressed = true
|
|
safe._on_cell_input(0, ev_rc)
|
|
_ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_checkout" and client.calls[0]["args"] == [0, 1, 0],
|
|
"Right-clicking safebox slot 0 triggers checkout to inventory free slot 0")
|
|
|
|
# Safebox internal slot move (e.g. from cell 0 to cell 6)
|
|
client.calls.clear()
|
|
var safe_move_payload := {"window": 3, "cell": 0, "vnum": 10, "count": 1}
|
|
var move_res := safe._drop_to_slot(safe_move_payload, 6)
|
|
_ck(move_res, "Moving item between safebox slots succeeds")
|
|
_ck(client.calls.size() > 0 and client.calls[0]["method"] == "safebox_move" and client.calls[0]["args"] == [0, 6, 1],
|
|
"Internal safebox move sent safebox_move(from=0, to=6, count=1)")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 5: Mall Operations and MALL_CANNOT_INSERT Rejection
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 5: Mall Operations & MALL_CANNOT_INSERT (40250 uisafebox.py:640) ---")
|
|
client.calls.clear()
|
|
client._inventory = [{"cell": 2, "vnum": 10, "count": 1}] # Free slot 0 is empty
|
|
client._mall_items = [{"cell": 4, "vnum": 27993, "count": 5}]
|
|
mall.refresh()
|
|
|
|
# Right-click mall slot 4 -> checkout to inventory free slot 0
|
|
var ev_mall_rc := InputEventMouseButton.new()
|
|
ev_mall_rc.button_index = MOUSE_BUTTON_RIGHT
|
|
ev_mall_rc.pressed = true
|
|
mall._on_grid_input(4, ev_mall_rc)
|
|
_ck(client.calls.size() > 0 and client.calls[0]["method"] == "mall_checkout" and client.calls[0]["args"] == [4, 1, 0],
|
|
"Right-clicking mall slot 4 triggers mall_checkout(mall_pos=4, inv_win=1, inv_cell=0)")
|
|
|
|
# Attempt to deposit inventory item into mall slot -> rejected
|
|
client.calls.clear()
|
|
mouse.attach_item(1, 2, 10, 1)
|
|
var ev_mall_drop := InputEventMouseButton.new()
|
|
ev_mall_drop.button_index = MOUSE_BUTTON_LEFT
|
|
ev_mall_drop.pressed = true
|
|
mall._on_grid_input(7, ev_mall_drop)
|
|
_ck(not mouse.is_attached(), "Dragging item into Mall cancels attached mouse item")
|
|
_ck(mall._status.text == "无法将物品放入商城仓库!", "Mall rejects insertion with exact 40250 warning (MALL_CANNOT_INSERT)")
|
|
_ck(client.calls.is_empty(), "No packet sent on rejected mall deposit attempt")
|
|
|
|
# Attempt drop via _drop_to_slot
|
|
var mall_drop_res := mall._drop_to_slot({"window": 1, "cell": 2, "vnum": 10, "count": 1}, 7)
|
|
_ck(not mall_drop_res, "mall._drop_to_slot strictly returns false")
|
|
|
|
# -------------------------------------------------------------
|
|
# Test 6: Password Protocols and Error Notifications
|
|
# -------------------------------------------------------------
|
|
print("\n--- Test 6: Password Protocols & Wrong Password (40250 /safebox_password) ---")
|
|
client.calls.clear()
|
|
client.safebox_password("000000")
|
|
_ck(client.calls.size() > 0 and client.calls[0]["cmd"] == "/safebox_password 000000",
|
|
"Safebox password sends /safebox_password 000000")
|
|
|
|
client.calls.clear()
|
|
client.safebox_change_password("000000", "123456")
|
|
_ck(client.calls.size() > 0 and client.calls[0]["cmd"] == "/safebox_change_password 000000 123456",
|
|
"Safebox change password sends /safebox_change_password 000000 123456")
|
|
|
|
client.calls.clear()
|
|
client.mall_password("888888")
|
|
_ck(client.calls.size() > 0 and client.calls[0]["cmd"] == "/mall_password 888888",
|
|
"Mall password sends /mall_password 888888")
|
|
|
|
client.safebox_wrong_password.emit()
|
|
_ck(safe._status.text == "仓库密码错误", "safebox_wrong_password signal shows 仓库密码错误")
|
|
|
|
# Clean up
|
|
proto.queue_free()
|
|
safe.queue_free()
|
|
mall.queue_free()
|
|
client.queue_free()
|
|
mouse.queue_free()
|
|
host.queue_free()
|