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

267 lines
13 KiB
GDScript

# taskbar_dock_test.gd —— 验证右下角功能栏及对应窗口(角色、仓库等)40250 对齐
extends SceneTree
const UiManager = preload("res://ui/ui_manager.gd")
const CharStatusUI = preload("res://ui/char_status_ui.gd")
const SafeboxUI = preload("res://ui/safebox_ui.gd")
const Quickbar = preload("res://ui/quickbar.gd")
const SkillTable = preload("res://ui/skill_table.gd")
const Hud = preload("res://hud.gd")
class FakeClient extends Node:
signal points_changed(points: Dictionary)
signal entity_main_set(vid: int)
signal safebox_changed()
signal inventory_changed()
signal skills_changed()
signal quickslots_changed()
var _main_vid := 1
var _safebox_open := false
var calls := []
func get_main_vid() -> int: return _main_vid
func get_points() -> Dictionary:
var pts: Array = []
pts.resize(255); pts.fill(0)
pts[1] = 50; pts[3] = 1000; pts[4] = 2000; pts[5] = 1000; pts[6] = 1000
pts[7] = 200; pts[8] = 200; pts[12] = 40; pts[13] = 30; pts[14] = 20; pts[15] = 10
pts[26] = 3; pts[27] = 5
return {"points": pts, "hp": 1000, "max_hp": 1000, "sp": 200, "max_sp": 200, "level": 50, "exp": 1000, "next_exp": 2000, "gold": 50000}
func get_entity(vid: int) -> Dictionary:
return {"name": "WarriorTest", "guild": 0, "race": 0}
func get_skill_group() -> int: return 1
func get_skills() -> Array:
return [{"id": 1, "level": 15, "master": 0}, {"id": 2, "level": 1, "master": 1}]
func get_safebox_items() -> Array:
return [{"cell": 0, "vnum": 10, "count": 1}, {"cell": 4, "vnum": 27001, "count": 50}]
func get_inventory() -> Array:
return [{"cell": 0, "vnum": 11200, "count": 1}, {"cell": 1, "vnum": 27001, "count": 50}]
func get_safebox_size() -> int: return 3
func get_safebox_gold() -> int: return 12345
func is_safebox_open() -> bool: return _safebox_open
func safebox_close() -> void:
_safebox_open = false
calls.append("safebox_close")
func safebox_checkout(cell: int, win: int, target: int) -> bool:
calls.append(["checkout", cell, win, target])
return true
func say(type: int, text: String) -> bool:
calls.append(["say", type, text])
return true
func get_item(window: int, cell: int) -> Dictionary:
if cell == 1:
return {"vnum": 27001, "count": 50}
return {}
var _fail := 0
func _ck(c: bool, m: String) -> void:
if not c:
_fail += 1
printerr("FAIL: " + m)
func _init() -> void:
_run()
if _fail == 0:
print("PASS: taskbar_dock_test (dock buttons + CharacterWindow tabs + SafeboxWindow 40250 parity)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var assets := AssetRoot.path()
var ui: CanvasLayer = UiManager.new()
get_root().add_child(ui)
ui._ready()
var fc := FakeClient.new()
get_root().add_child(fc)
var sk_tbl := SkillTable.new()
var desc := assets.path_join("locale/locale/en/skilldesc.txt")
if FileAccess.file_exists(desc):
sk_tbl.load_file(desc)
# 1. 验证 Quickbar Dock
var qb := Quickbar.new()
get_root().add_child(qb)
qb.setup(fc, sk_tbl, ui, func(): return null, assets)
var dock := qb.get_dock()
_ck(dock != null and is_instance_valid(dock), "taskbar dock exists")
_ck(dock.anchor_left == 1.0 and dock.offset_left == -178 and dock.offset_bottom == 37,
"TaskBarDock correctly anchored to top-right (offset_left=-178, offset_bottom=37)")
var btn_char := qb.get_button("character")
var btn_inv := qb.get_button("inventory")
var btn_msg := qb.get_button("messenger")
var btn_safe := qb.get_button("safebox")
var btn_sys := qb.get_button("system")
_ck(btn_char != null and btn_char.position == Vector2(34, 3), "CharacterButton at (34, 3)")
_ck(btn_inv != null and btn_inv.position == Vector2(68, 3), "InventoryButton at (68, 3)")
_ck(btn_msg != null and btn_msg.position == Vector2(102, 3), "MessengerButton at (102, 3)")
_ck(btn_safe != null and btn_safe.position == Vector2(0, 3), "SafeboxButton at (0, 3)")
_ck(btn_sys != null and btn_sys.position == Vector2(136, 3), "SystemButton at (136, 3)")
# 1.05 验证 40250 CenterBar 居中定位与槽位显示
var cb: Control = qb._root.find_child("CenterBar", true, false)
_ck(cb != null and cb.offset_left == -128 and cb.offset_right == 237,
"CenterBar horizontally centered at offset_left=-128, offset_right=237")
var qb_board: Control = cb.find_child("quickslot_board", true, false) if cb else null
_ck(qb_board != null and qb_board.position == Vector2(42, 0), "quickslot_board at (42, 0)")
var slot0: Control = qb._slots[0].btn
var corner0: TextureRect = slot0.find_child("Corner", true, false) if slot0 else null
_ck(corner0 != null and corner0.position == Vector2(3, 3), "Slot 0 corner indicator at (3, 3)")
# 验证物品装配后显示真实图标与数量,而非 #id 文本
qb.assign(0, "item", 1, false)
_ck(qb._slots[0].icon.visible and qb._slots[0].icon.texture != null, "Slot 0 item icon loaded")
_ck(qb._slots[0].lbl.text == "50", "Slot 0 shows item count 50")
# 1.1 验证 40250 左下角状态栏 (Gauge_Board + EXP_Gauge_Board)
var gb: Control = qb._root.find_child("Gauge_Board", true, false)
_ck(gb != null and gb.position == Vector2(0, -10) and gb.size == Vector2(158, 47),
"Gauge_Board exists at (0, -10) with size 158x47")
var rampage: TextureRect = gb.find_child("RampageGauge", true, false) if gb else null
_ck(rampage != null and qb._rampage_frames.size() == 17, "RampageGauge has 17 animation frames")
var hp_board: Control = gb.find_child("HPGauge_Board", true, false) if gb else null
_ck(hp_board != null and hp_board.size == Vector2(95, 11), "HPGauge_Board exists at 95x11")
var hp_rec: ColorRect = hp_board.find_child("HPRecoveryGaugeBar", true, false) if hp_board else null
_ck(hp_rec != null, "HPRecoveryGaugeBar exists inside HPGauge_Board")
var hp_fill: Control = hp_board.find_child("HPFill", true, false) if hp_board else null
_ck(hp_fill != null and hp_fill.clip_contents, "HPFill has clip_contents = true")
var hp_img: TextureRect = hp_fill.find_child("HPGauge", true, false) if hp_fill else null
_ck(hp_img != null and qb._hp_frames.size() == 7, "HPGauge loaded 7-frame liquid ripple textures")
_ck(qb._sp_frames.size() == 7 and qb._st_frames.size() == 7, "SPGauge & STGauge loaded 7-frame textures")
# 验证 set_hp / set_sp / set_st / set_exp 与 40250 悬停提示
qb.set_hp(500, 1000)
_ck(hp_board.tooltip_text == "HP : 500 / 1000", "HP tooltip matches 40250 (HP : 500 / 1000)")
_ck(hp_fill.size.x == 48, "HP fill width clipped to 48px for 50%")
qb.set_sp(100, 200)
_ck(qb._sp_board.tooltip_text == "SP : 100 / 200", "SP tooltip matches 40250 (SP : 100 / 200)")
_ck(qb._sp_fill.size.x == 48, "SP fill width clipped to 48px for 50%")
qb.set_st(50, 100)
_ck(qb._st_board.tooltip_text == "ST : 50 / 100", "ST tooltip matches 40250 (ST : 50 / 100)")
_ck(qb._st_fill.size.x == 48, "ST fill width clipped to 48px for 50%")
qb.set_exp(500, 2000)
_ck(qb._exp_board.tooltip_text == "EXP : 25.00%", "EXP tooltip matches 40250 (EXP : 25.00%)")
_ck(qb._exp_clips[0].visible and qb._exp_clips[0].size.y == 19, "EXP point 1 fully visible")
_ck(not qb._exp_clips[1].visible, "EXP point 2 hidden at 25%")
# 1.2 验证 Hud 无浮动状态条且正确转发数据至 Quickbar
var hud := Hud.new()
get_root().add_child(hud)
hud._build_status()
_ck(hud._status_box == null, "HUD _build_status does not create floating prototype status box")
hud.quickbar = qb
hud.set_vitals(750, 1000, 180, 200)
_ck(qb._cur_hp == 750 and hp_board.tooltip_text == "HP : 750 / 1000", "HUD set_vitals forwarded to Quickbar")
hud.set_exp(1000, 2000)
_ck(qb._cur_exp == 1000 and qb._exp_board.tooltip_text == "EXP : 50.00%", "HUD set_exp forwarded to Quickbar")
hud.queue_free()
# 2. 验证 CharacterWindow
var char_ui := CharStatusUI.new()
get_root().add_child(char_ui)
char_ui.setup(ui, fc, assets, sk_tbl)
char_ui.open("STATUS")
_ck(char_ui.is_open(), "CharacterWindow opened")
# 验证 TitleBar 与 CloseButton
var char_tb: Control = char_ui._node("Character_TitleBar")
_ck(char_tb != null, "Character_TitleBar exists")
var close_btn: Button = char_tb.find_child("CloseButton", true, false) if char_tb else null
_ck(close_btn != null, "CloseButton on Character_TitleBar exists")
# 切换到 SKILL 页签并验证格子与点数
char_ui._set_state("SKILL")
var skill_page: Control = char_ui._node("Skill_Page")
_ck(skill_page != null and skill_page.visible, "Skill_Page visible")
var active_slot: Control = char_ui._node("Skill_Active_Slot")
_ck(active_slot != null, "Skill_Active_Slot exists")
var s1: Control = active_slot.get_node_or_null("slot_1") if active_slot else null
_ck(s1 != null, "Skill slot_1 exists")
var s1_btn: TextureButton = s1.find_child("Btn", true, false) if s1 else null
_ck(s1_btn != null and s1_btn.texture_normal != null, "Skill slot_1 has valid skill icon")
var s1_lvl: Label = s1.find_child("Lvl", true, false) if s1 else null
_ck(s1_lvl != null and s1_lvl.text == "15", "Skill slot_1 level is 15")
var etc_slot: Control = char_ui._node("Skill_ETC_Slot")
_ck(etc_slot != null, "Skill_ETC_Slot exists")
var etc_101: Control = etc_slot.get_node_or_null("slot_101") if etc_slot else null
_ck(etc_101 != null, "Skill_ETC_Slot slot_101 exists")
# 切换到 EMOTICON 页签
char_ui._set_state("EMOTICON")
var emote_page: Control = char_ui._node("Emoticon_Page")
_ck(emote_page != null and emote_page.visible, "Emoticon_Page visible")
var solo_slot: Control = char_ui._node("SoloEmotionSlot")
_ck(solo_slot != null, "SoloEmotionSlot exists")
var e1: Control = solo_slot.get_node_or_null("slot_1") if solo_slot else null
_ck(e1 != null, "Emote slot_1 exists")
var e1_btn: TextureButton = e1.find_child("Btn", true, false) if e1 else null
_ck(e1_btn != null and e1_btn.texture_normal != null, "Emote slot_1 has emote icon")
# 切换到 QUEST 页签
char_ui._set_state("QUEST")
var quest_page: Control = char_ui._node("Quest_Page")
_ck(quest_page != null and quest_page.visible, "Quest_Page visible")
# 测试 TitleBar 关闭按钮
var quest_tb: Control = char_ui._node("Quest_TitleBar")
var q_close: Button = quest_tb.find_child("CloseButton", true, false) if quest_tb else null
_ck(q_close != null, "Quest_TitleBar has CloseButton")
if q_close:
q_close.pressed.emit()
_ck(not char_ui.is_open(), "CharacterWindow closed by TitleBar CloseButton")
# 3. 验证 SafeboxWindow
var safe_ui := SafeboxUI.new()
get_root().add_child(safe_ui)
safe_ui.setup(fc, ui, null, assets)
fc._safebox_open = true
safe_ui.open()
_ck(safe_ui.is_open(), "SafeboxWindow opened")
_ck(safe_ui._root.size == Vector2(530, 418), "SafeboxWindow size is 530x418 (got %s)" % str(safe_ui._root.size))
var safe_tb: Control = safe_ui._root.find_child("TitleBar", true, false)
_ck(safe_tb != null, "Safebox TitleBar exists")
var safe_title: Label = safe_ui._root.find_child("TitleName", true, false)
_ck(safe_title != null and safe_title.text.begins_with("仓库"), "Safebox TitleName is 仓库")
var safe_close: Button = safe_tb.find_child("CloseButton", true, false) if safe_tb else null
_ck(safe_close != null, "Safebox TitleBar has CloseButton")
var wnd_item: Control = safe_ui._root.find_child("wndItem", true, false)
_ck(wnd_item != null and wnd_item.size == Vector2(160, 288), "wndItem 5x9 grid (160x288) exists")
var safe_s0: Control = safe_ui._cells.get(0, null)
_ck(safe_s0 != null, "Safebox slot 0 exists")
var safe_s0_icon: TextureRect = safe_s0.find_child("Icon", true, false) if safe_s0 else null
_ck(safe_s0_icon != null and safe_s0_icon.texture != null, "Safebox slot 0 has item icon")
var safe_s4: Control = safe_ui._cells.get(4, null)
_ck(safe_s4 != null, "Safebox slot 4 exists")
var safe_s4_cnt: Label = safe_s4.find_child("Count", true, false) if safe_s4 else null
_ck(safe_s4_cnt != null and safe_s4_cnt.text == "50", "Safebox slot 4 count label is 50")
# 右键取出测试
safe_ui._on_cell_input(0, InputEventMouseButton.new())
var right_click := InputEventMouseButton.new()
right_click.button_index = MOUSE_BUTTON_RIGHT
right_click.pressed = true
safe_ui._on_cell_input(0, right_click)
_ck(fc.calls.any(func(c): return c is Array and c.size() > 0 and c[0] == "checkout" and c[1] == 0),
"Safebox right click triggers checkout")
# 分页按钮 (I, II, III)
_ck(safe_ui._page_buttons.size() == 3, "Safebox has 3 page buttons")
safe_ui._page_buttons[1].pressed.emit()
_ck(safe_ui._page == 1, "Switched to page 1 (II)")
safe_ui._page_buttons[0].pressed.emit()
_ck(safe_ui._page == 0, "Switched back to page 0 (I)")
# 修改密码按钮
_ck(safe_ui._change_pw_btn != null and safe_ui._change_pw_btn.text == "修改密码", "ChangePasswordButton exists")
# 关闭按钮
_ck(safe_ui._exit_btn != null and safe_ui._exit_btn.text == "关闭", "ExitButton exists")
safe_ui._exit_btn.pressed.emit()
_ck(not safe_ui.is_open(), "ExitButton closed SafeboxWindow")