Files
mtgodot-poc/project/ui/quest_dialog.gd
T
shenandClaude Sonnet 5 47baf6c0c6 Metin2 game client (P0–P11) + mobile asset pipeline
Networked client on the existing Godot 4.7 + libgr2 renderer:
- net: m2dev wire protocol (libsodium KX + XChaCha20), auth/select/game
  phases, EntityStore world model, ~all GC/CG headers. char create/delete,
  private shop / mall / cube, SHOP_GC_START_EX, guild, party (+ CG_PARTY_SET_STATE),
  quests, dragon soul, refine, safebox, exchange.
- UI: in-game windows migrated 1:1 from the reference uiscript/root .py —
  char status (/stat), inventory+equipment, select-item ([SELECT_ITEM] quest
  token), system-option + game-option + ESC system menu, private-shop 39-grid,
  party info board, shop tabs, atlas, minimap, quickbar, chat, …
- EterGrnLib polish: GR2 material blend/two-sided, LOD crossfade, motion-event
  dispatch, contact shadow, ray-AABB picking, weapon grip pre-transform.

Portable asset IO (A1) — all extension/libgr2/formats/mtproto reads routed
through godot::FileAccess (res:// PCK works on iOS/Android); standalone-lib
*_path() kept for the non-Godot CTests. AssetResolver + PropertyRegistry
switched to a baked index (bake_asset_index.gd) instead of std::filesystem.

Mobile builds: build-{android,ios}.sh, export-android.sh, pack-assets.sh,
gen-debug-keystore.sh. Assets ship as a zip mounted at runtime by
project/asset_pack.gd (adb push now; HTTP download is a drop-in later).

ctest 10/10, 34 GDScript suites, macOS/iOS/Android all build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013EJxkHiNKS4kybHS3XKyAJ
2026-08-31 20:02:12 +09:00

227 lines
6.7 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.
# 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_askGC_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