# QuestDialog (P7) —— 渲染 GC_SCRIPT 的 NPC 对话(EventManager 脚本,客户端只渲染)。 # # var qd := preload("res://ui/quest_dialog.gd").new() # add_child(qd) # qd.setup(m2client, canvas_parent) # # 脚本格式(`CPythonEventManager`):纯文本 + `[命令 arg(值)]` token 交错。 # [ENTER] 换行 # [CLEAR] 清空 # [NEXT] / [DONE] 「继续 / 关闭」按钮 -> M2Client.script_answer(255) # [QUESTION arg("是") arg("否")] 选项按钮 -> script_answer(0..N-1) # [LETTER value("...")] 追加文本(旧写法) # [INPUT] 显示文本输入框,提交 -> M2Client.quest_input() # [SELECT_ITEM] 要求从背包选一颗魔石 -> 发 select_item_requested 信号(打开 SelectItemUI) # [CONFIRM_WAIT] 等:本版忽略 # # quest_confirm_ask(GC_QUEST_CONFIRM)也在这里弹 是/否 -> quest_confirm(yes, pid)。 extends Node signal opened() signal closed() # EventManager EVENT_TYPE_SELECT_ITEM —— 对应 interfacemodule.BINARY_OpenSelectItemWindow signal select_item_requested() var client: Node var _root: Control var _text: RichTextLabel var _btnrow: VBoxContainer var _confirm_pid := 0 func setup(m2client: Node, parent: Node) -> void: client = m2client set_process_unhandled_input(true) _build(parent) if client.has_signal("script_dialog"): client.script_dialog.connect(_on_script) if client.has_signal("quest_confirm_ask"): client.quest_confirm_ask.connect(_on_confirm) func is_open() -> bool: return _root != null and _root.visible # --- 收 GC_SCRIPT ------------------------------------------------------ func _on_script(_skin: int, text: String) -> void: var parsed := parse_script(text) if parsed.body.strip_edges() == "" and parsed.choices.is_empty(): close() return _text.text = parsed.body _fill_buttons(parsed.choices, parsed.has_next, parsed.has_input) _root.visible = true opened.emit() if parsed.has_select_item: select_item_requested.emit() func _on_confirm(msg: String, _timeout: int, request_pid: int) -> void: _confirm_pid = request_pid _text.text = msg _fill_confirm() _root.visible = true opened.emit() func close() -> void: if _root: _root.visible = false closed.emit() func _unhandled_input(event: InputEvent) -> void: if not is_open() or not (event is InputEventKey): return var key := event as InputEventKey if key.pressed and not key.echo and key.keycode == KEY_ESCAPE: # The legacy client sends QUEST_CANCEL when the active script is dismissed # with Escape; ordinary answer buttons continue to use SCRIPT_ANSWER. if client and client.has_method("quest_cancel"): client.quest_cancel() close() get_viewport().set_input_as_handled() # --- 脚本解析 -------------------------------------------------------- # -> { body: String, choices: [String], has_next: bool } func parse_script(src: String) -> Dictionary: var body := "" var choices := [] var has_next := false var has_input := false var has_select_item := false var i := 0 var n := src.length() while i < n: var c := src[i] if c == "[": var close_i := src.find("]", i) if close_i == -1: body += src.substr(i) break var tok := src.substr(i + 1, close_i - i - 1).strip_edges() i = close_i + 1 var name := tok.split(" ", false)[0].to_upper() if tok != "" else "" match name: "ENTER": body += "\n" "CLEAR", "CLEAR_TEXT": body = "" "NEXT", "DONE": has_next = true "QUESTION": for m in _rx_args(tok): choices.append(m) "INPUT": has_input = true "SELECT_ITEM": has_select_item = true "LETTER": var vv := _rx_args(tok) if vv.size() > 0: body += vv[0] _: pass # COLOR/DELAY/WAIT/IMAGE/... 忽略 else: body += c i += 1 return {"body": body.strip_edges(), "choices": choices, "has_next": has_next, "has_input": has_input, "has_select_item": has_select_item} # 抓 tok 里所有 arg("...") / value("...") 的字符串 func _rx_args(tok: String) -> Array: var out := [] var i := 0 while true: var q1 := tok.find('"', i) if q1 == -1: break var q2 := tok.find('"', q1 + 1) if q2 == -1: break out.append(tok.substr(q1 + 1, q2 - q1 - 1)) i = q2 + 1 return out # --- UI -------------------------------------------------------------- func _build(parent: Node) -> void: _root = Control.new() _root.set_anchors_preset(Control.PRESET_CENTER) _root.position = Vector2(-230, -160) _root.size = Vector2(460, 300) _root.visible = false _root.set_meta("is_titlebar", true) parent.add_child(_root) var panel := Panel.new() panel.set_anchors_preset(Control.PRESET_FULL_RECT) var sb := StyleBoxFlat.new() sb.bg_color = Color(0.08, 0.07, 0.06, 0.97) sb.border_color = Color(0.5, 0.42, 0.3) sb.set_border_width_all(1) sb.set_corner_radius_all(4) panel.add_theme_stylebox_override("panel", sb) _root.add_child(panel) _text = RichTextLabel.new() _text.bbcode_enabled = true _text.position = Vector2(16, 14) _text.size = Vector2(428, 200) _text.add_theme_font_size_override("normal_font_size", 13) _root.add_child(_text) _btnrow = VBoxContainer.new() _btnrow.position = Vector2(16, 222) _btnrow.custom_minimum_size = Vector2(428, 0) _btnrow.add_theme_constant_override("separation", 4) _root.add_child(_btnrow) func _clear_buttons() -> void: for c in _btnrow.get_children(): c.queue_free() func _fill_buttons(choices: Array, has_next: bool, has_input := false) -> void: _clear_buttons() if has_input: var input := LineEdit.new() input.placeholder_text = "请输入" input.custom_minimum_size = Vector2(428, 28) _btnrow.add_child(input) var submit := _mkbtn("提交") submit.pressed.connect(func(): _submit_input(input)) input.text_submitted.connect(func(_text): _submit_input(input)) return if choices.is_empty(): var b := _mkbtn("继续" if has_next else "关闭") b.pressed.connect(func(): client.script_answer(255) close()) return for i in choices.size(): var idx := i var b := _mkbtn(String(choices[i])) b.pressed.connect(func(): client.script_answer(idx) close()) func _submit_input(input: LineEdit) -> void: if client and client.has_method("quest_input") and client.quest_input(input.text): close() func _fill_confirm() -> void: _clear_buttons() var row := HBoxContainer.new() row.add_theme_constant_override("separation", 8) _btnrow.add_child(row) for spec in [["接受", true], ["拒绝", false]]: var yes: bool = spec[1] var b := Button.new() b.text = spec[0] b.custom_minimum_size = Vector2(120, 30) b.pressed.connect(func(): client.quest_confirm(yes, _confirm_pid) close()) row.add_child(b) func _mkbtn(text: String) -> Button: var b := Button.new() b.text = text b.custom_minimum_size = Vector2(428, 26) _btnrow.add_child(b) return b