# UiBuild (P1) —— uiscript 嵌套 Dictionary → Godot Control 树。 # # var spec := UiScript.new().parse_file(path) # var r := UiBuild.build(spec, assets_root) # add_child(r.root) # r.nodes["accept"].pressed.connect(...) # 按 uiscript 里的 "name" 取节点 # # 覆盖 uiscript 最常用的 ~18 种 type;其它降级为 Control / Button / TextureRect。 # board/thinboard 的九宫格用 ui_kit(真实 ymir 贴图,缺失则 StyleBoxFlat 兜底)。 extends RefCounted const UiKit = preload("res://ui_kit.gd") const UiAssets = preload("res://ui/ui_assets.gd") # spec: 顶层 window dict。返回 { root: Control, nodes: {name: Control} } static func build(spec: Dictionary, assets_root: String) -> Dictionary: var ctx := {"assets": assets_root, "nodes": {}} var root := _make(spec, ctx, Vector2i.ZERO) if root == null: root = Control.new() return {"root": root, "nodes": ctx["nodes"]} # --- recursion --------------------------------------------------------- static func _i(spec: Dictionary, key: String, def := 0) -> int: var v = spec.get(key, def) return int(v) if (v is int or v is float or v is bool or v is String) else def static func _str(v) -> String: if v == null: return "" return v if v is String else str(v) static func _make(spec: Dictionary, ctx: Dictionary, parent_size: Vector2i) -> Control: var type := _str(spec.get("type", "window")) var w := _i(spec, "width", 0) var h := _i(spec, "height", 0) var node := _make_by_type(type, spec, ctx, Vector2i(w, h)) if node == null: node = Control.new() node.name = _safe_name(_str(spec.get("name", type))) if w > 0: node.custom_minimum_size = Vector2(w, h) node.size = Vector2(w, h) # 位置:x/y + horizontal_align / vertical_align(相对父) var x := _i(spec, "x", 0) var y := _i(spec, "y", 0) match _str(spec.get("horizontal_align", "left")): "center": x = (parent_size.x - w) / 2 + x "right": x = parent_size.x - w - x match _str(spec.get("vertical_align", "top")): "center": y = (parent_size.y - h) / 2 + y "bottom": y = parent_size.y - h - y node.position = Vector2(x, y) var nm := _str(spec.get("name", "")) if nm != "": ctx["nodes"][nm] = node var self_size := Vector2i(w, h) if w > 0 else parent_size for child in spec.get("children", []): if child is Dictionary: var c := _make(child, ctx, self_size) if c: node.add_child(c) return node static func _make_by_type(type: String, spec: Dictionary, ctx: Dictionary, sz: Vector2i) -> Control: var assets: String = ctx["assets"] match type: "window", "box": var c := Control.new() c.mouse_filter = Control.MOUSE_FILTER_PASS return c "board", "thinboard", "board_with_titlebar": var p := _board(assets, "thinboard" if type == "thinboard" else "board", sz) if type == "board_with_titlebar" and spec.has("title"): var tl := Label.new() tl.name = "TitleText" tl.text = _str(spec.get("title", "")) tl.position = Vector2(12, 6) p.add_child(tl) p.set_meta("titlebar_h", 24) return p "titlebar": var t := Panel.new() t.set_meta("is_titlebar", true) return t "text": var lbl := Label.new() lbl.text = _str(spec.get("text", "")) if spec.has("text_color"): lbl.add_theme_color_override("font_color", _argb(int(spec["text_color"]))) match _str(spec.get("text_horizontal_align", "left")): "center": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER "right": lbl.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT lbl.vertical_alignment = VERTICAL_ALIGNMENT_CENTER lbl.mouse_filter = Control.MOUSE_FILTER_IGNORE return lbl "button", "toggle_button": var b := Button.new() b.text = _str(spec.get("text", "")) b.toggle_mode = (type == "toggle_button") var nrm := UiAssets.load_tex(assets, _str(spec.get("default_image", ""))) var ovr := UiAssets.load_tex(assets, _str(spec.get("over_image", ""))) var dwn := UiAssets.load_tex(assets, _str(spec.get("down_image", ""))) if nrm: b.add_theme_stylebox_override("normal", _sb_tex(nrm)) if ovr: b.add_theme_stylebox_override("hover", _sb_tex(ovr)) if dwn: b.add_theme_stylebox_override("pressed", _sb_tex(dwn)) return b "radio_button": var cb := CheckBox.new() cb.text = _str(spec.get("text", "")) return cb "image", "expanded_image", "ani_image", "mark": var ir := TextureRect.new() ir.texture = UiAssets.load_tex(assets, _str(spec.get("image", ""))) ir.expand_mode = TextureRect.EXPAND_IGNORE_SIZE ir.stretch_mode = TextureRect.STRETCH_SCALE if type == "expanded_image" \ else TextureRect.STRETCH_KEEP ir.mouse_filter = Control.MOUSE_FILTER_IGNORE return ir "editline": var e := LineEdit.new() e.text = _str(spec.get("text", "")) e.secret = bool(spec.get("secret", false)) if spec.has("input_limit"): e.max_length = int(spec["input_limit"]) return e "line": var ln := ColorRect.new() ln.color = Color(1, 1, 1, 0.15) if _i(spec, "height", 0) == 0: ln.custom_minimum_size = Vector2(_i(spec, "width", 100), 1) return ln "listbox", "candidate_list": return ItemList.new() "gauge", "bar", "horizontalbar": var pb := ProgressBar.new() pb.show_percentage = false pb.value = 100.0 return pb "grid_table", "slotbar", "slot": # Metin2 物品格容器:有 start_index/x_count 或显式 "slot" 元组 -> 建子格; # 否则 grid_table 退化成普通 GridContainer。 var cells := _slot_cells(spec) if cells.is_empty() and type == "grid_table": var g := GridContainer.new() g.columns = maxi(1, _i(spec, "x_count", _i(spec, "column", 1))) return g var s := Control.new() s.set_meta("is_slot_container", true) for cd in cells: var cell := Panel.new() cell.name = "slot_%d" % int(cd["index"]) cell.position = Vector2(cd["x"], cd["y"]) cell.custom_minimum_size = Vector2(cd["w"], cd["h"]) cell.size = Vector2(cd["w"], cd["h"]) cell.set_meta("slot_index", int(cd["index"])) cell.mouse_filter = Control.MOUSE_FILTER_STOP s.add_child(cell) return s "sliderbar": return HSlider.new() "scrollbar", "thin_scrollbar": return VScrollBar.new() _: return Control.new() # --- helpers --------------------------------------------------------- static func _board(assets: String, prefix: String, sz: Vector2i) -> Panel: var p := Panel.new() var np: NinePatchRect = null if assets != "": np = UiKit.board(assets, prefix, 32, 128) if np and np.texture: np.set_anchors_preset(Control.PRESET_FULL_RECT) np.mouse_filter = Control.MOUSE_FILTER_IGNORE p.add_child(np) p.get_child(0).show_behind_parent = true else: var sb := StyleBoxFlat.new() sb.bg_color = Color(0.10, 0.11, 0.14, 0.92) sb.border_color = Color(0.35, 0.32, 0.26) sb.set_border_width_all(1) sb.set_corner_radius_all(3) p.add_theme_stylebox_override("panel", sb) return p # slot 容器的子格:显式 "slot" 元组,或 start_index/x_count/y_count/x_step/y_step 网格。 static func _slot_cells(spec: Dictionary) -> Array: var cells := [] if spec.get("slot", null) is Array: for sd in spec["slot"]: if sd is Dictionary: cells.append({"index": _i(sd, "index"), "x": _i(sd, "x"), "y": _i(sd, "y"), "w": _i(sd, "width", 32), "h": _i(sd, "height", 32)}) elif spec.has("x_count"): var si := _i(spec, "start_index", 0) var xc := maxi(1, _i(spec, "x_count", 1)) var yc := maxi(1, _i(spec, "y_count", 1)) var xs := _i(spec, "x_step", 32) var ys := _i(spec, "y_step", 32) for row in yc: for col in xc: cells.append({"index": si + row * xc + col, "x": col * xs, "y": row * ys, "w": 32, "h": 32}) return cells static func _sb_tex(t: Texture2D) -> StyleBoxTexture: var sb := StyleBoxTexture.new() sb.texture = t return sb static func _argb(v: int) -> Color: # 0xAARRGGBB var a := (v >> 24) & 0xff var r := (v >> 16) & 0xff var g := (v >> 8) & 0xff var b := v & 0xff if a == 0: a = 255 return Color8(r, g, b, a) static func _safe_name(s: String) -> String: var out := s for bad: String in [".", ":", "@", "/", "%", " "]: out = out.replace(bad, "_") return out if out != "" else "node"