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

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()