# W7 HUD —— 小地图(拼 minimap.dds)+ 状态栏 / 快捷栏 / 背包。 # §6.1:窗口/血条/快捷栏用真实 `ETC/ymir work/ui/pattern/` 九宫格贴图(AssetResolver 机制), # 精确布局仍待 §0 的 ingame-shinsoo-ui.json。SHINSOO §9-W7 / PARITY §6.1。 # 用法:var hud := preload("res://hud.gd").new(); hud.setup(world, player); add_child(hud) extends CanvasLayer var _world: Node var _player: Node3D var _minimap_rect: TextureRect var _minimap_frame: Control var _dot: ColorRect var _map_w_m := 1024.0 var _map_h_m := 1280.0 var _inv: Control var _assets: String var _pat := "ETC/ymir work/ui/pattern/" var _tex_cache := {} var _hp_fill: NinePatchRect var _mp_fill: NinePatchRect var _stamina_fill: ColorRect var _stamina_label: Label var _exp_fill: ColorRect var _lvl_label: Label var _hp_label: Label var _hp_w := 240.0 var _energy_fill: ColorRect var _energy_label: Label # 目标血条(P0) var _target_panel: Control var _target_name: Label var _target_fill: ColorRect # 状态图标条(P4) var _affect_row: HBoxContainer # GC_DUNGEON destination compass (ClientVS22: AlarmHaveToGo). var _dungeon_panel: Control var _dungeon_label: Label var _dungeon_arrow: Label var _dungeon_destination := Vector3.ZERO func setup(world: Node, player: Node3D) -> void: _world = world _player = player _assets = String(world.get("assets_root")) var rep: Dictionary = _world.call("get_load_report") _map_w_m = float(rep["map_size_x"]) * 256.0 _map_h_m = float(rep["map_size_y"]) * 256.0 _build_minimap(int(rep["map_size_x"]), int(rep["map_size_y"])) _build_status() _build_hotbar() _build_inventory() _build_dungeon_compass() func reload_world(new_world: Node) -> void: _world = new_world if _world == null: return var rep: Dictionary = _world.call("get_load_report") _map_w_m = float(rep.get("map_size_x", 1)) * 256.0 _map_h_m = float(rep.get("map_size_y", 1)) * 256.0 if _minimap_frame and is_instance_valid(_minimap_frame): _minimap_frame.queue_free() _minimap_frame = null _minimap_rect = null _dot = null if int(rep.get("map_size_x", 0)) > 0 and int(rep.get("map_size_y", 0)) > 0: _build_minimap(int(rep["map_size_x"]), int(rep["map_size_y"])) # --- real UI texture loading (§6.1) ------------------------------------------- func _ui_tex(name: String) -> Texture2D: if _tex_cache.has(name): return _tex_cache[name] var img := Image.new() var tex: Texture2D = null var cand := _assets.path_join(_pat + name + ".tga") if not FileAccess.file_exists(cand): # fall back: scan asset packs for "/ymir work/ui/pattern/.tga" var da := DirAccess.open(_assets) if da: for sub in da.get_directories(): var p := _assets.path_join(sub).path_join("ymir work/ui/pattern/" + name + ".tga") if FileAccess.file_exists(p): cand = p break if FileAccess.file_exists(cand) and img.load(cand) == OK: tex = ImageTexture.create_from_image(img) _tex_cache[name] = tex return tex # Composite a set of pattern pieces into one atlas and return a NinePatchRect. # pieces = {corner_lt, corner_rt, corner_lb, corner_rb, line_t, line_l, base} func _nine(prefix: String, m: int, base_size: int) -> NinePatchRect: var total := m + base_size + m var atlas := Image.create_empty(total, total, false, Image.FORMAT_RGBA8) atlas.fill(Color(0, 0, 0, 0)) var put := func(name: String, x: int, y: int) -> void: var t := _ui_tex(name) if t == null: return var im := t.get_image() im.convert(Image.FORMAT_RGBA8) atlas.blit_rect(im, Rect2i(Vector2i.ZERO, im.get_size()), Vector2i(x, y)) put.call(prefix + "_corner_lefttop", 0, 0) put.call(prefix + "_corner_righttop", m + base_size, 0) put.call(prefix + "_corner_leftbottom", 0, m + base_size) put.call(prefix + "_corner_rightbottom", m + base_size, m + base_size) # edges: stretch the piece across the middle band var edge := func(name: String, r: Rect2i) -> void: var t := _ui_tex(name) if t == null: return var im := t.get_image() im.convert(Image.FORMAT_RGBA8) im.resize(r.size.x, r.size.y, Image.INTERPOLATE_BILINEAR) atlas.blit_rect(im, Rect2i(Vector2i.ZERO, r.size), r.position) edge.call(prefix + "_line_top", Rect2i(m, 0, base_size, m)) edge.call(prefix + "_line_bottom", Rect2i(m, m + base_size, base_size, m)) edge.call(prefix + "_line_left", Rect2i(0, m, m, base_size)) edge.call(prefix + "_line_right", Rect2i(m + base_size, m, m, base_size)) var b := _ui_tex(prefix + "_base") if b: var bi := b.get_image() bi.convert(Image.FORMAT_RGBA8) bi.resize(base_size, base_size, Image.INTERPOLATE_BILINEAR) atlas.blit_rect(bi, Rect2i(Vector2i.ZERO, Vector2i(base_size, base_size)), Vector2i(m, m)) var np := NinePatchRect.new() np.texture = ImageTexture.create_from_image(atlas) np.patch_margin_left = m np.patch_margin_right = m np.patch_margin_top = m np.patch_margin_bottom = m return np # horizontal 3-slice gauge (left cap | tiled centre | right cap) as a NinePatchRect func _gauge(center_name: String, h: int) -> NinePatchRect: var pieces := [center_name + "_left" if _ui_tex(center_name + "_left") else center_name, center_name + "_center" if _ui_tex(center_name + "_center") else center_name, center_name + "_right" if _ui_tex(center_name + "_right") else center_name] var seg := 16 var atlas := Image.create_empty(seg * 3, h, false, Image.FORMAT_RGBA8) atlas.fill(Color(0, 0, 0, 0)) for i in 3: var t := _ui_tex(pieces[i]) if t == null: continue var im := t.get_image() im.convert(Image.FORMAT_RGBA8) im.resize(seg, h, Image.INTERPOLATE_BILINEAR) atlas.blit_rect(im, Rect2i(Vector2i.ZERO, Vector2i(seg, h)), Vector2i(i * seg, 0)) var np := NinePatchRect.new() np.texture = ImageTexture.create_from_image(atlas) np.patch_margin_left = seg np.patch_margin_right = seg return np # --- widgets ---------------------------------------------------------------- func _build_minimap(nx: int, ny: int) -> void: var cell := 64 var big := Image.create_empty(nx * cell, ny * cell, false, Image.FORMAT_RGBA8) big.fill(Color(0.08, 0.09, 0.11, 1.0)) for tx in nx: for ty in ny: var p: String = _world.call("chunk_dir", tx, ty) + "/minimap.dds" if not FileAccess.file_exists(p): continue var img: Image = _world.call("load_dds", p) if img == null: continue img.resize(cell, cell, Image.INTERPOLATE_BILINEAR) big.blit_rect(img, Rect2i(0, 0, cell, cell), Vector2i(tx * cell, ty * cell)) var mm_h := 192 * ny / nx if nx > 0 else 192 var frame := _nine("board", 32, 128) _minimap_frame = frame frame.set_anchors_preset(Control.PRESET_TOP_RIGHT) frame.position = Vector2(-192 - 24 - 12, 12) frame.size = Vector2(192 + 24, mm_h + 24) add_child(frame) _minimap_rect = TextureRect.new() _minimap_rect.texture = ImageTexture.create_from_image(big) _minimap_rect.expand_mode = TextureRect.EXPAND_IGNORE_SIZE _minimap_rect.stretch_mode = TextureRect.STRETCH_SCALE _minimap_rect.position = Vector2(12, 12) _minimap_rect.size = Vector2(192, mm_h) frame.add_child(_minimap_rect) _dot = ColorRect.new() _dot.color = Color(1, 0.9, 0.2) _dot.size = Vector2(6, 6) _minimap_rect.add_child(_dot) func _build_status() -> void: var box := VBoxContainer.new() box.set_anchors_preset(Control.PRESET_BOTTOM_LEFT) box.position = Vector2(18, -122) box.add_theme_constant_override("separation", 6) add_child(box) for spec in [["gauge_red", 0.82], ["gauge_blue", 0.60]]: var slot := _gauge("gauge_slot", 24) slot.custom_minimum_size = Vector2(_hp_w, 24) slot.size = Vector2(_hp_w, 24) var fill := _gauge(spec[0], 24) fill.position = Vector2(0, 0) fill.size = Vector2(_hp_w * float(spec[1]), 24) slot.add_child(fill) box.add_child(slot) if spec[0] == "gauge_red": _hp_fill = fill _hp_label = Label.new() _hp_label.add_theme_font_size_override("font_size", 11) _hp_label.position = Vector2(6, 5) slot.add_child(_hp_label) else: _mp_fill = fill # 体力条(40250 POINT_STAMINA,冲刺/骑乘时由 ServerCommand 驱动)。 var stamina_slot := ColorRect.new() stamina_slot.color = Color(0, 0, 0, 0.55) stamina_slot.custom_minimum_size = Vector2(_hp_w, 10) stamina_slot.size = Vector2(_hp_w, 10) _stamina_fill = ColorRect.new() _stamina_fill.color = Color(0.25, 0.85, 0.35) _stamina_fill.size = Vector2(0, 10) stamina_slot.add_child(_stamina_fill) _stamina_label = Label.new() _stamina_label.add_theme_font_size_override("font_size", 9) _stamina_label.position = Vector2(6, -1) stamina_slot.add_child(_stamina_label) box.add_child(stamina_slot) # 能量条(原客户端 POINT_ENERGY,0..100)。资源缺失时仍用纯色条保持可读。 var energy_slot := ColorRect.new() energy_slot.color = Color(0, 0, 0, 0.55) energy_slot.custom_minimum_size = Vector2(_hp_w, 10) energy_slot.size = Vector2(_hp_w, 10) _energy_fill = ColorRect.new() _energy_fill.color = Color(0.95, 0.65, 0.18) _energy_fill.size = Vector2(0, 10) energy_slot.add_child(_energy_fill) _energy_label = Label.new() _energy_label.add_theme_font_size_override("font_size", 9) _energy_label.position = Vector2(6, -1) energy_slot.add_child(_energy_label) box.add_child(energy_slot) # 经验条(细)+ 等级 var exp_slot := ColorRect.new() exp_slot.color = Color(0, 0, 0, 0.5) exp_slot.custom_minimum_size = Vector2(_hp_w, 6) exp_slot.size = Vector2(_hp_w, 6) _exp_fill = ColorRect.new() _exp_fill.color = Color(0.9, 0.8, 0.2) _exp_fill.size = Vector2(0, 6) exp_slot.add_child(_exp_fill) box.add_child(exp_slot) _lvl_label = Label.new() _lvl_label.add_theme_font_size_override("font_size", 12) _lvl_label.text = "Lv 1" box.add_child(_lvl_label) # --- P0: live data setters ------------------------------------------------- func set_vitals(hp: int, max_hp: int, sp: int, max_sp: int) -> void: if _hp_fill: _hp_fill.size.x = _hp_w * clampf(float(hp) / maxf(1.0, max_hp), 0.0, 1.0) if _mp_fill: _mp_fill.size.x = _hp_w * clampf(float(sp) / maxf(1.0, max_sp), 0.0, 1.0) if _hp_label: _hp_label.text = "%d / %d" % [hp, max_hp] func set_exp(xp: int, next_xp: int) -> void: if _exp_fill: _exp_fill.size.x = _hp_w * clampf(float(xp) / maxf(1.0, next_xp), 0.0, 1.0) func set_level(lv: int) -> void: if _lvl_label: _lvl_label.text = "Lv %d" % lv func set_energy(value: int, max_value: int = 100) -> void: var cap := maxi(1, max_value) var v := clampi(value, 0, cap) if _energy_fill: _energy_fill.size.x = _hp_w * float(v) / float(cap) if _energy_label: _energy_label.text = "%d / %d" % [v, cap] func set_stamina(value: int, max_value: int) -> void: var cap := maxi(1, max_value) var v := clampi(value, 0, cap) if _stamina_fill: _stamina_fill.size.x = _hp_w * float(v) / float(cap) if _stamina_label: _stamina_label.text = "%d / %d" % [v, cap] # P4:状态图标条。affects = [{type, point_idx, value, flag, duration}, ...] func set_affects(affects: Array) -> void: if _affect_row == null: _affect_row = HBoxContainer.new() _affect_row.set_anchors_preset(Control.PRESET_TOP_LEFT) _affect_row.position = Vector2(18, 18) _affect_row.add_theme_constant_override("separation", 3) add_child(_affect_row) for c in _affect_row.get_children(): c.queue_free() for a in affects: var sq := ColorRect.new() sq.custom_minimum_size = Vector2(22, 22) var t := int(a.get("type", 0)) sq.color = Color.from_hsv(fmod(t * 0.13, 1.0), 0.55, 0.9, 0.95) sq.tooltip_text = "affect #%d pt%d %+d %ds" % [t, int(a.get("point_idx", 0)), int(a.get("value", 0)), int(a.get("duration", 0))] _affect_row.add_child(sq) func set_target(nm: String, hp_pct: int) -> void: if _target_panel == null: _build_target_panel() _target_panel.visible = true _target_name.text = nm if nm != "" else "Target" _target_fill.size.x = 180.0 * clampf(hp_pct / 100.0, 0.0, 1.0) func clear_target() -> void: if _target_panel: _target_panel.visible = false func set_dungeon_destination(active: bool, world_pos: Vector3) -> void: _dungeon_destination = world_pos if _dungeon_panel: _dungeon_panel.visible = active if not active: _dungeon_label.text = "" func _build_dungeon_compass() -> void: _dungeon_panel = Control.new() _dungeon_panel.name = "DungeonCompass" _dungeon_panel.set_anchors_preset(Control.PRESET_CENTER_TOP) _dungeon_panel.position = Vector2(-110, 18) _dungeon_panel.size = Vector2(220, 42) _dungeon_panel.visible = false add_child(_dungeon_panel) var bg := ColorRect.new() bg.color = Color(0.04, 0.05, 0.08, 0.78) bg.size = _dungeon_panel.size bg.mouse_filter = Control.MOUSE_FILTER_IGNORE _dungeon_panel.add_child(bg) _dungeon_label = Label.new() _dungeon_label.position = Vector2(30, 5) _dungeon_label.add_theme_font_size_override("font_size", 12) _dungeon_panel.add_child(_dungeon_label) _dungeon_arrow = Label.new() _dungeon_arrow.text = "▲" _dungeon_arrow.position = Vector2(8, 9) _dungeon_arrow.add_theme_font_size_override("font_size", 18) _dungeon_panel.add_child(_dungeon_arrow) func _build_target_panel() -> void: _target_panel = Control.new() _target_panel.set_anchors_preset(Control.PRESET_CENTER_TOP) _target_panel.position = Vector2(-100, 16) _target_panel.size = Vector2(200, 40) add_child(_target_panel) _target_name = Label.new() _target_name.add_theme_font_size_override("font_size", 12) _target_name.position = Vector2(10, 0) _target_panel.add_child(_target_name) var slot := ColorRect.new() slot.color = Color(0, 0, 0, 0.55) slot.position = Vector2(10, 18) slot.size = Vector2(180, 12) _target_panel.add_child(slot) _target_fill = ColorRect.new() _target_fill.color = Color(0.85, 0.2, 0.2) _target_fill.size = Vector2(180, 12) slot.add_child(_target_fill) func _build_hotbar() -> void: var row := HBoxContainer.new() row.set_anchors_preset(Control.PRESET_CENTER_BOTTOM) row.position = Vector2(-8 * 23, -56) row.add_theme_constant_override("separation", 4) add_child(row) for i in 8: var slot := _nine("thinboard", 16, 16) slot.custom_minimum_size = Vector2(42, 42) var n := Label.new() n.text = str(i + 1) n.position = Vector2(4, 2) n.add_theme_font_size_override("font_size", 11) slot.add_child(n) row.add_child(slot) func _build_inventory() -> void: _inv = _nine("board", 32, 128) _inv.set_anchors_preset(Control.PRESET_CENTER_RIGHT) _inv.position = Vector2(-280, -200) _inv.size = Vector2(256, 384) _inv.visible = false var t := Label.new() t.text = " Inventory (I to close)" t.position = Vector2(16, 12) _inv.add_child(t) add_child(_inv) func _input(ev: InputEvent) -> void: if ev is InputEventKey and ev.pressed and ev.keycode == KEY_I: _inv.visible = not _inv.visible func _process(_dt: float) -> void: if _player: if _dungeon_panel and _dungeon_panel.visible: var d := _dungeon_destination - _player.global_position var distance := Vector2(d.x, d.z).length() _dungeon_label.text = "副本目标 %.0fm" % distance # Player forward is +Z in this scene; rotate the north-facing arrow # relative to the current player yaw, keeping the target prompt local. _dungeon_arrow.rotation = atan2(d.x, d.z) - _player.rotation.y if _minimap_rect: var u := clampf(_player.position.x / _map_w_m, 0.0, 1.0) var v := clampf(_player.position.z / _map_h_m, 0.0, 1.0) _dot.position = Vector2(u * _minimap_rect.size.x - 3, v * _minimap_rect.size.y - 3)