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