Files
mtgodot-poc/project/quest_test.gd
T

237 lines
12 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# quest_test —— P7GC_SCRIPT 渲染 + quest_confirm + quest_log + NPC 点击 headless 自检。
# godot --headless --path project --script quest_test.gd
extends SceneTree
const QuestDialog = preload("res://ui/quest_dialog.gd")
const QuestLog = preload("res://ui/quest_log.gd")
class FakeClient 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 answers := []
var cancels := 0
var inputs := []
var confirms := []
var clicks := []
var quests := []
var script_buttons := []
func is_in_game() -> bool: return true
func get_quests() -> Array: return quests
func script_answer(a) -> bool: answers.append(a); return true
func quest_cancel() -> bool: cancels += 1; return true
func quest_input(text) -> bool: inputs.append(text); return true
func quest_confirm(yes, pid) -> bool: confirms.append([yes, pid]); return true
func script_button(index) -> bool: script_buttons.append(index); return true
func click_npc(vid) -> bool: clicks.append(vid); return true
func get_entity(_v) -> Dictionary: 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: quest_test (script parse + dialog + confirm + log)")
quit(0)
else:
printerr("%d check(s) failed" % _fail)
quit(1)
func _run() -> void:
var canvas := CanvasLayer.new()
get_root().add_child(canvas)
var fc := FakeClient.new()
get_root().add_child(fc)
# --- 脚本解析 ---
var qd: Node = QuestDialog.new()
get_root().add_child(qd)
qd.setup(fc, canvas)
var p: Dictionary = qd.parse_script("Greetings, hero.[ENTER]The village needs you.[ENTER][QUESTION arg(\"I accept\") arg(\"Not now\")]")
_ck(p.body.contains("Greetings, hero.") and p.body.contains("village needs"), "plain text kept")
_ck(p.body.split("\n").size() == 2, "[ENTER] -> line break (%d lines)" % p.body.split("\n").size())
_ck(p.choices == ["I accept", "Not now"], "[QUESTION] -> 2 choices")
var p2: Dictionary = qd.parse_script("Bye.[DONE]")
_ck(p2.has_next and p2.choices.is_empty(), "[DONE] -> continue button, no choices")
var p3: Dictionary = qd.parse_script("[COLOR256 value;255 value;128 value;0]Gold[ENTER][COLOR value;0 value;1 value;0]Green[ITEM value;19][MOB value;77][WINDOW_SIZE value;640 value;360][TEXT_HORIZONTAL_ALIGN_CENTER][CONFIRM_WAIT value;30]")
_ck(p3.body.contains("[color=#ff8000]") and p3.body.contains("Gold") and p3.body.contains("[color=#00ff00]"),
"COLOR/COLOR256 -> RichTextLabel color spans")
_ck(p3.body.contains("物品 #19") and p3.body.contains("怪物 #77"), "ITEM/MOB -> named placeholders")
_ck(p3.window_size == [640, 360] and p3.text_centered and p3.confirm_wait and p3.confirm_timeout == 30,
"WINDOW_SIZE/TEXT_CENTER/CONFIRM_WAIT metadata")
var p4: Dictionary = qd.parse_script("[QUESTBUTTON idx(12) name(\"Read me\") icon_type(\"file\") icon_name(\"quest.sub\")][ADDMAPSIGNAL x(1200) y(3400)][SETCMAPPOS x(8) y(-4)][CLEARMAPSIGNAL][SET_CAMERA value(1) value(2)][FADE_OUT speed(0.25)]")
_ck(p4.quest_buttons.size() == 1 and p4.quest_buttons[0].index == 12 and
p4.quest_buttons[0].title == "Read me", "QUESTBUTTON metadata")
_ck(p4.map_signals.size() == 1 and p4.map_signals[0] == Vector2(1200, 3400) and
p4.clear_map_signals and p4.atlas_center == [8, -4], "map signal metadata")
_ck(p4.camera_events.size() == 1 and p4.fade_events.size() == 1 and
p4.fade_events[0].kind == "FADE_OUT", "camera/fade metadata")
# --- 对话弹窗 + 选择 -> script_answer ---
fc.script_dialog.emit(1, "Pick one.[QUESTION arg(\"A\") arg(\"B\") arg(\"C\")]")
await process_frame
_ck(qd.is_open(), "dialog shown on script_dialog")
_ck(qd._btnrow.get_child_count() == 3, "3 choice buttons")
(qd._btnrow.get_child(1) as Button).pressed.emit() # 选 B (index 1)
_ck(fc.answers == [1], "choice B -> script_answer(1)")
_ck(not qd.is_open(), "dialog closes after choice")
# 无选项 -> 「继续」-> script_answer(255)
fc.script_dialog.emit(0, "Just text.[NEXT]")
await process_frame
_ck(qd._btnrow.get_child_count() == 1, "1 continue button")
(qd._btnrow.get_child(0) as Button).pressed.emit()
_ck(fc.answers == [1, 255], "continue -> script_answer(255)")
# DONE is a button action in ClientVS22. Parsing / receiving the token must
# not fire DoneEvent before the player confirms the close button.
var done_events := 0
qd.done_event.connect(func(): done_events += 1)
var done_parse: Dictionary = qd.parse_script("Finished.[DONE]")
_ck(done_parse.next_button_type == "DONE" and done_events == 0,
"DONE parse stays side-effect free")
fc.script_dialog.emit(0, "Finished.[DONE]")
await process_frame
(qd._btnrow.get_child(0) as Button).pressed.emit()
_ck(done_events == 1 and fc.answers == [1, 255, 255],
"DONE click -> DoneEvent + script_answer(255)")
# ESC dismisses the active script through the legacy QUEST_CANCEL packet.
fc.script_dialog.emit(0, "Cancelable quest.[NEXT]")
await process_frame
var esc := InputEventKey.new()
esc.pressed = true
esc.keycode = KEY_ESCAPE
qd._unhandled_input(esc)
_ck(fc.cancels == 1, "ESC -> quest_cancel()")
_ck(not qd.is_open(), "ESC closes quest dialog")
# [INPUT] renders a LineEdit and sends the entered text through QUEST_INPUT_STRING.
var pi: Dictionary = qd.parse_script("Tell me your name.[INPUT]")
_ck(pi.has_input, "[INPUT] parsed")
fc.script_dialog.emit(0, "Tell me your name.[INPUT]")
await process_frame
var input := qd._btnrow.get_child(0) as LineEdit
input.text = "Alice"
(qd._btnrow.get_child(1) as Button).pressed.emit()
_ck(fc.inputs == ["Alice"], "[INPUT] submit -> quest_input('Alice')")
_ck(not qd.is_open(), "input submit closes quest dialog")
# EventManager side effects are surfaced to the interface owner.
var received := []
var cleared := []
var signal_points := []
qd.quest_button_received.connect(func(index, title, icon_type, icon_name): received.append([index, title, icon_type, icon_name]))
qd.quest_button_cleared.connect(func(index): cleared.append(index))
qd.map_signal_added.connect(func(x, y): signal_points.append(Vector2(x, y)))
fc.script_dialog.emit(0, "[QUESTBUTTON idx(12) name(\"Read me\") icon_type(\"file\") icon_name(\"quest.sub\")][QUESTBUTTON_CLOSE idx(8)][ADDMAPSIGNAL x(1200) y(3400)][CLEARMAPSIGNAL]")
await process_frame
_ck(received == [[12, "Read me", "file", "quest.sub"]] and cleared == [8],
"QUESTBUTTON events reach interface")
_ck(signal_points == [Vector2(1200, 3400)], "ADDMAPSIGNAL reaches minimap owner")
# --- quest_confirm ---
fc.quest_confirm_ask.emit("Join the party?", 30, 999)
await process_frame
_ck(qd.is_open(), "confirm shown")
# 找「接受」按钮
var acc: Button = null
for n in qd._btnrow.find_children("*", "Button", true, false):
if n.text == "接受":
acc = n
_ck(acc != null, "accept button present")
if acc:
acc.pressed.emit()
_ck(fc.confirms == [[true, 999]], "accept -> quest_confirm(true, 999)")
# --- quest_log ---
var ql: Node = QuestLog.new()
get_root().add_child(ql)
ql.setup(fc, canvas)
qd.quest_button_received.connect(func(index, title, icon_type, icon_name): ql.recv_quest(index, title, icon_type, icon_name))
qd.quest_button_cleared.connect(ql.clear_quest)
fc.quests = [
{"index": 1, "title": "Slay 3 wolves", "counter_name": "Wolves", "counter_value": 2, "clock_name": "Time", "clock_value": 65, "icon": "missing/icon.sub"},
{"index": 2, "title": "", "counter_name": "", "counter_value": 0, "clock_name": "", "clock_value": 0}, # 完成 -> 不显示
]
ql.toggle()
_ck(ql.is_open(), "quest log opened")
await process_frame
# 只 1 条有标题的
var titles := 0
for box in ql._list.get_children():
for lbl in box.find_children("*", "Label", true, false) if box is VBoxContainer else []:
if String(lbl.text).begins_with("◆"):
titles += 1
_ck(titles == 1, "quest log shows 1 titled quest (got %d)" % titles)
_ck(ql._clock_labels.has(1) and String(ql._clock_labels[1].text).contains("1分5秒"),
"quest clock renders minutes and seconds")
# ClientVS22 only renders QUEST_MAX_NUM=5 rows and maps the scrollbar to a
# discrete questShowingStartIndex. Empty icon paths use the default scroll icon.
fc.quests.clear()
for i in range(7):
fc.quests.append({"index": 100 + i, "title": "Quest %d" % i,
"counter_name": "", "counter_value": 0, "clock_name": "", "clock_value": 0, "icon": ""})
ql.refresh()
await process_frame
_ck(ql.quest_count() == 7 and ql._quest_scrollbar.visible, "quest log exposes all quests and scroll bar")
_ck(ql.visible_quest_indices() == [0, 1, 2, 3, 4], "quest log initially shows 5 rows")
ql.set_scroll_ratio(1.0)
await process_frame
_ck(ql.quest_start_index() == 2 and ql.visible_quest_indices() == [2, 3, 4, 5, 6],
"quest scroll maps to last 5 rows")
ql.select_quest(6)
_ck(fc.script_buttons == [4, -2147483648 + 106], "quest row selection -> event quest button packet")
fc.script_dialog.emit(0, "[QUESTBUTTON idx(4) name(\"Talk to guard\") icon_type(\"highlight\") icon_name(\"\")]")
await process_frame
_ck(ql.quest_button_count() == 1 and ql._button_root.visible, "quest button strip receives QUESTBUTTON")
(ql._button_list.get_child(0) as Button).pressed.emit()
_ck(fc.script_buttons == [4] and ql.quest_button_count() == 0, "quest button click -> script_button and remove")
fc.quest_info.emit(1) # 刷新不炸
await process_frame
_ck(true, "quest_info refresh ok")
# --- 任务信件按钮条:多列定位(1:1 interfacemodule.__ArrangeQuestButton,§5.2 增量 109---
# 参考公式:yPos = 170*H//600 ; yCount = (H-330)//63 ; 第 i 个 =
# (xPos + (i//yCount)*100 , yPos + (i%yCount)*63) ; xPos = 20 / 130(组队窗显示)
_ck(QuestLog.quest_button_y_count(600.0) == 4, "quest btn: yCount(600) == 4")
_ck(QuestLog.quest_button_y_count(1080.0) == 11, "quest btn: yCount(1080) == 11")
_ck(QuestLog.quest_button_y_count(350.0) == 1, "quest btn: yCount clamps to 1 below ZeroDivision")
_ck(QuestLog.quest_button_y_pos(600.0) == 170, "quest btn: yPos(600) == 170")
_ck(QuestLog.quest_button_y_pos(1200.0) == 340, "quest btn: yPos scales with height")
ql.set_screen_size_override(Vector2(800, 600))
for i in range(6):
ql.recv_quest(10 + i, "Letter %d" % i, "file", "")
await process_frame
# insert(0, btn):最新在前
_ck(ql.quest_button_order() == [15, 14, 13, 12, 11, 10], "quest btn: newest inserted at front")
_ck(ql.quest_button_position(15) == Vector2(20, 170), "quest btn: slot 0 at (xPos, yPos)")
_ck(ql.quest_button_position(12) == Vector2(20, 359), "quest btn: slot 3 stacks down one column")
_ck(ql.quest_button_position(11) == Vector2(120, 170), "quest btn: slot 4 wraps to column 2")
_ck(ql.quest_button_position(10) == Vector2(120, 233), "quest btn: slot 5 is column 2 row 1")
_ck(ql._button_list.get_child(0).position == Vector2(20, 170), "quest btn: node position matches formula")
# 同 index 再来 -> 移到最前
ql.recv_quest(13, "Letter 3 (again)", "file", "")
await process_frame
_ck(ql.quest_button_order() == [13, 15, 14, 12, 11, 10], "quest btn: re-received index moves to front")
# 组队窗显示 -> xPos 130
ql.set_party_shown(true)
await process_frame
_ck(ql.quest_button_position(13) == Vector2(130, 170), "quest btn: party window shifts xPos to 130")
_ck(ql._button_list.get_child(0).position == Vector2(130, 170), "quest btn: node repositioned on party toggle")
ql.set_party_shown(false)
ql.clear_quest(13)
await process_frame
_ck(not ql.quest_button_order().has(13) and ql.quest_button_count() == 5,
"quest btn: clear drops index from order")
for i in range(6):
ql.clear_quest(10 + i)
await process_frame
_ck(ql.quest_button_order().is_empty() and not ql._button_root.visible,
"quest btn: emptied strip hides root")