# test_quest_dialog_parity.gd —— Phase 3 任务系统与对话流 40250 行为对齐验证 extends SceneTree const QuestDialog = preload("res://ui/quest_dialog.gd") const QuestLog = preload("res://ui/quest_log.gd") var _failed := false var _pass_count := 0 func _check(ok: bool, msg: String) -> void: if ok: _pass_count += 1 print(" [PASS] " + msg) else: _failed = true push_error("FAIL: " + msg) print(" [FAIL] " + msg) class MockClient extends Node: signal script_dialog(skin: int, text: String) signal quest_confirm_ask(msg: String, timeout: int, request_pid: int) signal quest_info(index: int) var script_answers: Array = [] var script_buttons: Array = [] var quest_inputs: Array = [] var quest_confirms: Array = [] var quest_cancels := 0 var quests: Dictionary = {} func script_answer(answer: int) -> bool: script_answers.append(answer) return true func script_button(idx: int) -> bool: script_buttons.append(idx) return true func quest_input(text: String) -> bool: quest_inputs.append(text) return true func quest_confirm(yes: bool, request_pid: int) -> bool: quest_confirms.append({"yes": yes, "pid": request_pid}) return true func quest_cancel() -> bool: quest_cancels += 1 script_answers.append(254) return true func get_quests() -> Array: return quests.values() func is_in_game() -> bool: return true func _init() -> void: print("=== Running test_quest_dialog_parity (Phase 3 Parity) ===") _test_pipe_question_parsing() _test_choice_pagination() _test_input_submission() _test_next_and_done() _test_esc_cancel() _test_skin_modes() _test_left_image_layout() _test_quest_button_and_log() _test_quest_confirm() if _failed: print("\n=== QUEST DIALOG PARITY TESTS FAILED ===") quit(1) else: print("\n=== ALL QUEST DIALOG PARITY TESTS PASSED (%d checks) ===" % _pass_count) quit(0) # --- Test 1: 40250 Pipe-delimited [QUESTION] Parsing --- func _test_pipe_question_parsing() -> void: print("\n--- Test 1: 40250 Standard Pipe-delimited [QUESTION] ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) # 40250 standard wire script from questlua.cpp:642 GotoSelectState var script := "老铁匠看着你说道:[ENTER]请问你需要什么帮助?[QUESTION 1;拜访铁匠|2;购买强化材料|3;打听消息|4;离开]" client.script_dialog.emit(0, script) qd.process_events(100.0) _check(qd.is_open(), "Quest dialog opened") var parsed: Dictionary = qd.parse_script(script) _check(parsed.choices.size() == 4, "Parsed exactly 4 choices") _check(parsed.choices[0] == "拜访铁匠", "Choice 0 is '拜访铁匠'") _check(parsed.choices[1] == "购买强化材料", "Choice 1 is '购买强化材料'") _check(parsed.choices[2] == "打听消息", "Choice 2 is '打听消息'") _check(parsed.choices[3] == "离开", "Choice 3 is '离开'") # Click choice 2 ("购买强化材料") -> 0-indexed choice 1 sent to server var btn2: Button = qd._btnrow.get_child(1) btn2.pressed.emit() _check(client.script_answers == [1], "Clicking choice 2 sends 0-indexed script_answer(1)") _check(not qd.is_open(), "Dialog closes after choice selection") qd.queue_free() host.queue_free() client.queue_free() # --- Test 2: Choice Pagination (> 8 choices per 40250 uiquest.py) --- func _test_choice_pagination() -> void: print("\n--- Test 2: Choice Pagination (> 8 choices) ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) # 12 choices var script := "[QUESTION " for i in range(1, 13): if i > 1: script += "|" script += "%d;传送地点_%d" % [i, i] script += "]" client.script_dialog.emit(0, script) qd.process_events(100.0) _check(qd.is_open(), "Dialog opened for 12 choices") # Page 1: 8 choice buttons + 1 navigation row = 9 children _check(qd._btnrow.get_child_count() == 9, "Page 1 renders 8 choice buttons + 1 nav row") var first_btn: Button = qd._btnrow.get_child(0) _check(first_btn.text == "传送地点_1", "First button on Page 1 is '传送地点_1'") # Nav row is child 8 var nav_row: HBoxContainer = qd._btnrow.get_child(8) _check(nav_row != null, "Navigation row exists") var next_btn: Button = nav_row.get_child(nav_row.get_child_count() - 1) _check(next_btn.text == "下一页", "Next page button present") # Switch to Page 2 next_btn.pressed.emit() _check(qd._current_choice_page == 1, "Switched to Page 2") # Page 2: remaining 4 choice buttons + 1 nav row = 5 children _check(qd._btnrow.get_child_count() == 5, "Page 2 renders remaining 4 buttons + 1 nav row") var page2_btn2: Button = qd._btnrow.get_child(1) # This is 10th overall choice (index 9) _check(page2_btn2.text == "传送地点_10", "Second button on Page 2 is '传送地点_10'") # Click choice 10 -> global index 9 sent to server page2_btn2.pressed.emit() _check(client.script_answers == [9], "Clicking option on Page 2 sends global index script_answer(9)") _check(not qd.is_open(), "Dialog closed after selection on page 2") qd.queue_free() host.queue_free() client.queue_free() # --- Test 3: [INPUT] Submission Flow --- func _test_input_submission() -> void: print("\n--- Test 3: [INPUT] Submission Flow ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) client.script_dialog.emit(0, "请输入暗号密码:[INPUT]") qd.process_events(100.0) _check(qd.is_open(), "Dialog with input opened") var line_edit: LineEdit = null var submit_btn: Button = null for child in qd._btnrow.get_children(): if child is LineEdit: line_edit = child elif child is Button and child.text == "确定": submit_btn = child _check(line_edit != null, "LineEdit control created for [INPUT]") _check(submit_btn != null, "Submit button created") line_edit.text = "metin2_open_sesame" submit_btn.pressed.emit() _check(client.quest_inputs == ["metin2_open_sesame"], "Submitted input string sent via quest_input") _check(not qd.is_open(), "Dialog closes after input submission") qd.queue_free() host.queue_free() client.queue_free() # --- Test 4: [NEXT] and [DONE] Protocols --- func _test_next_and_done() -> void: print("\n--- Test 4: [NEXT] and [DONE] Protocols ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) # 4.1: [NEXT] button client.script_dialog.emit(0, "这是第一段剧情。[NEXT]") qd.process_events(100.0) var next_btn: Button = qd._btnrow.get_child(0) _check(next_btn.text == "继续", "Button label is '继续'") next_btn.pressed.emit() _check(client.script_answers == [254], "[NEXT] click sends script_answer(254) to resume") # 4.2: [DONE] button client.script_answers.clear() var done_emitted := [false] qd.done_event.connect(func(): done_emitted[0] = true ) client.script_dialog.emit(0, "任务已经完成![DONE]") qd.process_events(100.0) var done_btn: Button = qd._btnrow.get_child(0) _check(done_btn.text == "关闭", "Button label is '关闭'") done_btn.pressed.emit() _check(client.script_answers == [254], "[DONE] click sends script_answer(254)") _check(done_emitted[0], "done_event signal emitted on [DONE] click") qd.queue_free() host.queue_free() client.queue_free() # --- Test 5: ESC Key Cancel Flow --- func _test_esc_cancel() -> void: print("\n--- Test 5: ESC Key Cancel Flow (40250 OnCancel) ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) client.script_dialog.emit(0, "选择你的阵营:[QUESTION 1;红帝国|2;黄帝国|3;蓝帝国]") qd.process_events(100.0) _check(qd.is_open(), "Dialog open waiting for choice") # Simulate ESC key press var esc := InputEventKey.new() esc.keycode = KEY_ESCAPE esc.pressed = true qd._unhandled_input(esc) _check(client.quest_cancels == 1 or client.script_answers == [254], "ESC key triggers quest cancellation") _check(not qd.is_open(), "Dialog closed on ESC") qd.queue_free() host.queue_free() client.queue_free() # --- Test 6: Skin Modes (40250 uiquest.py:328) --- func _test_skin_modes() -> void: print("\n--- Test 6: Skin Modes (NORMAL vs NOWINDOW vs CINEMA) ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) # 6.1: SKIN_NORMAL (0) client.script_dialog.emit(0, "普通对话框[DONE]") qd.process_events(100.0) _check(qd._panel.visible, "SKIN_NORMAL displays background panel") # 6.2: SKIN_CINEMA (5) client.script_dialog.emit(5, "电影过场模式[DONE]") qd.process_events(100.0) _check(not qd._panel.visible, "SKIN_CINEMA hides background panel") # 6.3: SKIN_NOWINDOW (1) client.script_dialog.emit(1, "无边框纯文本模式[DONE]") qd.process_events(100.0) _check(not qd._panel.visible, "SKIN_NOWINDOW hides background panel") qd.queue_free() host.queue_free() client.queue_free() # --- Test 7: Left Image Layout Offset (LEFTIMAGE) --- func _test_left_image_layout() -> void: print("\n--- Test 7: Left Image Portrait Layout Offset ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) # Dialog without portrait client.script_dialog.emit(0, "无立绘对话[DONE]") qd.process_events(100.0) var default_text_x: float = qd._text.position.x _check(default_text_x == 16.0, "Default text position x is 16.0") # Dialog with LEFTIMAGE var script := "[LEFTIMAGE src;ui/game/questboard/npc_guard.sub]守卫向你致敬:[ENTER]城外近期有野狼出没,请多加小心![DONE]" client.script_dialog.emit(0, script) qd.process_events(100.0) # Even if texture is dummy/mocked, layout handles parsed LEFTIMAGE panel spec var parsed: Dictionary = qd.parse_script(script) _check(parsed.images.size() == 1 and parsed.images[0]["panel"] == "LEFTIMAGE", "Parsed LEFTIMAGE correctly") qd.queue_free() host.queue_free() client.queue_free() # --- Test 8: Quest Button & Quest Log Integration --- func _test_quest_button_and_log() -> void: print("\n--- Test 8: Quest Button & Quest Log Flow ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) var ql := QuestLog.new() root.add_child(ql) ql.setup(client, host) qd.quest_button_received.connect(func(idx, title, itype, iname): ql.recv_quest(idx, title, itype, iname)) # Script emits QUESTBUTTON var script := "[QUESTBUTTON idx(7) name(\"消灭恶狼\") icon_type(\"highlight\") icon_name(\"\")][DONE]" client.script_dialog.emit(0, script) qd.process_events(100.0) _check(ql._quest_button_data.has(7), "QuestLog received quest button with index 7") _check(ql._quest_button_data[7]["title"] == "消灭恶狼", "Quest title is '消灭恶狼'") # Player clicks quest letter / button on sidebar ql._start_quest(7) _check(client.script_buttons == [7], "Clicking quest button sends script_button(7)") _check(not ql._quest_button_data.has(7), "Quest button cleared from active list after click") ql.queue_free() qd.queue_free() host.queue_free() client.queue_free() # --- Test 9: Quest Confirm (GC_QUEST_CONFIRM) --- func _test_quest_confirm() -> void: print("\n--- Test 9: Quest Confirmation Modal Flow ---") var client := MockClient.new() var host := Control.new() root.add_child(host) var qd := QuestDialog.new() root.add_child(qd) qd.setup(client, host) # Server asks confirmation (e.g. party dungeon warp) client.quest_confirm_ask.emit("是否确认进入地牢副本?", 30, 99999) _check(qd.is_open(), "Confirm dialog opened") _check(qd._text.text == "是否确认进入地牢副本?", "Dialog message matches") # Find "接受" button var accept_btn: Button = null for row_child in qd._btnrow.get_children(): if row_child is HBoxContainer: for b in row_child.get_children(): if b is Button and b.text == "接受": accept_btn = b _check(accept_btn != null, "'接受' button exists") accept_btn.pressed.emit() _check(client.quest_confirms == [{"yes": true, "pid": 99999}], "Clicking 接受 sends quest_confirm(true, 99999)") _check(not qd.is_open(), "Confirm dialog closes after decision") qd.queue_free() host.queue_free() client.queue_free()