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