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

175 lines
6.0 KiB
GDScript

# test_quest_hud_parity.gd —— 任务剧情与信件卷轴系统 1:1 回归测试套件
# 运行方式:
# /opt/homebrew/bin/godot --headless --path metin2-client/project --script test_quest_hud_parity.gd
extends SceneTree
const UiManager = preload("res://ui/ui_manager.gd")
const CharStatusUI = preload("res://ui/char_status_ui.gd")
const QuestLog = preload("res://ui/quest_log.gd")
class MockClient extends Node:
signal quest_info(index: int)
var sent_script_buttons: Array[int] = []
var quests: Array = []
func get_quests() -> Array:
return quests
func script_button(idx: int) -> void:
sent_script_buttons.append(idx)
# Required dummy methods for CharStatusUI setup
func get_points() -> Dictionary:
var pts: Array = []
pts.resize(255)
pts.fill(0)
return {"points": pts, "hp": 100, "max_hp": 100, "sp": 50, "max_sp": 50, "level": 1, "exp": 0, "next_exp": 100, "gold": 0}
func get_main_vid() -> int: return 1
func get_entity(_vid: int) -> Dictionary: return {"name": "Hero", "guild": 0, "race": 0}
func get_guild_name(_gid: int) -> String: return ""
func get_skills() -> Array: return []
func get_skill_group() -> int: return 0
var _checks_passed := 0
var _checks_failed := 0
func _assert(cond: bool, msg: String) -> void:
if cond:
_checks_passed += 1
print(" [PASS] %s" % msg)
else:
_checks_failed += 1
printerr(" [FAIL] %s" % msg)
func _init() -> void:
print("\n=== Running test_quest_hud_parity (Direction 1: Quest HUD & Dialog 1:1) ===")
_test_quest_log_layout()
_test_quest_log_buttons()
_test_char_status_quest_page()
_finish()
func _test_quest_log_layout() -> void:
print("\n--- Test 1: 40250 Quest Button Mathematical Layout (interfacemodule.py) ---")
# 800x600 screen
var y_count_600 := QuestLog.quest_button_y_count(600.0)
# (600 - 330) // 63 = 270 // 63 = 4
_assert(y_count_600 == 4, "Screen height 600 yields yCount = 4")
var y_pos_600 := QuestLog.quest_button_y_pos(600.0)
# 170 * 600 // 600 = 170
_assert(y_pos_600 == 170, "Screen height 600 yields yPos = 170")
# Slot positions
# slot 0: col 0, row 0 -> (20, 170)
var pos0 := QuestLog.quest_button_slot_pos(0, y_count_600, 20, y_pos_600)
_assert(pos0 == Vector2(20, 170), "Slot 0 position is (20, 170)")
# slot 3: col 0, row 3 -> (20, 170 + 3*63 = 359)
var pos3 := QuestLog.quest_button_slot_pos(3, y_count_600, 20, y_pos_600)
_assert(pos3 == Vector2(20, 359), "Slot 3 position is (20, 359)")
# slot 4: col 1, row 0 -> (20 + 100 = 120, 170)
var pos4 := QuestLog.quest_button_slot_pos(4, y_count_600, 20, y_pos_600)
_assert(pos4 == Vector2(120, 170), "Slot 4 column wrap position is (120, 170)")
func _test_quest_log_buttons() -> void:
print("\n--- Test 2: Quest Buttons HUD & Ctrl+Q Toggle ---")
var client := MockClient.new()
get_root().add_child(client)
var ql: Node = QuestLog.new()
get_root().add_child(ql)
ql.setup(client, get_root())
_assert(ql.quest_buttons_visible() == true, "Quest buttons visible by default")
ql.toggle_buttons() # Ctrl+Q
_assert(ql.quest_buttons_visible() == false, "toggle_buttons (Ctrl+Q) hides quest buttons")
ql.toggle_buttons() # Ctrl+Q again
_assert(ql.quest_buttons_visible() == true, "toggle_buttons (Ctrl+Q) restores quest buttons")
# Adding quest buttons (BINARY_RecvQuest)
ql.recv_quest(101, "初入江湖", "file", "")
ql.recv_quest(102, "消灭野狼", "file", "")
_assert(ql.quest_button_count() == 2, "2 quest buttons recorded")
var order: Array[int] = ql.quest_button_order()
_assert(order[0] == 102 and order[1] == 101, "Latest quest button inserted at front (1:1 40250)")
ql.queue_free()
client.queue_free()
func _test_char_status_quest_page() -> void:
print("\n--- Test 3: CharStatusUI Quest Page 1:1 Interaction & Protocol ---")
var assets := AssetRoot.path()
if not FileAccess.file_exists(assets.path_join("uiscript/uiscript/characterwindow.py")):
print(" (skip: no characterwindow.py)")
return
var ui: CanvasLayer = UiManager.new()
get_root().add_child(ui)
ui._ready()
var client := MockClient.new()
get_root().add_child(client)
# Seed 7 quests to test pagination (> 5)
for i in range(7):
client.quests.append({
"index": 100 + i,
"title": "任务_%d" % i,
"counter_name": "击杀数" if i == 0 else "",
"counter_value": 5 if i == 0 else 0,
"clock_name": "限时" if i == 1 else "",
"clock_value": 125 if i == 1 else 0,
})
var cs: Node = CharStatusUI.new()
get_root().add_child(cs)
cs.setup(ui, client, assets)
cs.open("QUEST")
_assert(cs.is_open(), "Character window opened in QUEST state")
# Check Scrollbar visibility (> 5 quests)
var sb = cs._node("Quest_ScrollBar")
_assert(sb != null and sb.visible == true, "Quest_ScrollBar visible when quests > 5")
_assert(sb.max_value == 2, "Quest_ScrollBar max_value is 2 (7 - 5)")
# Check labels for first page (0..4)
var name0 = cs._node("Quest_Name_00")
_assert(name0 != null and name0.text == "任务_0", "Quest_Name_00 shows '任务_0'")
var cnt0 = cs._node("Quest_LastCount_00")
_assert(cnt0 != null and cnt0.text == "击杀数 : 5", "Quest_LastCount_00 shows counter '击杀数 : 5'")
var time1 = cs._node("Quest_LastTime_01")
_assert(time1 != null and time1.text == "限时 02:05", "Quest_LastTime_01 formats 125s as '限时 02:05'")
# Click on Quest_Name_00: should send script_button(-2147483648 + 100)
var ev := InputEventMouseButton.new()
ev.button_index = MOUSE_BUTTON_LEFT
ev.pressed = true
name0.gui_input.emit(ev)
_assert(client.sent_script_buttons.size() == 1, "Clicking quest row sent script_button")
var expected_val: int = -2147483648 + 100
_assert(client.sent_script_buttons[0] == expected_val, "script_button value is -2147483648 + 100 (%d)" % expected_val)
# Scroll to next page
sb.value = 2 # showing start index = 2
sb.value_changed.emit(2.0)
_assert(name0.text == "任务_2", "After scroll, Quest_Name_00 shows '任务_2'")
cs.close()
cs.queue_free()
client.queue_free()
ui.queue_free()
func _finish() -> void:
print("\n=== SUMMARY: %d passed, %d failed ===" % [_checks_passed, _checks_failed])
if _checks_failed == 0:
print("ALL TESTS PASSED!\n")
quit(0)
else:
printerr("SOME TESTS FAILED!\n")
quit(1)