Files
mtgodot-poc/project/hud.gd
T

356 lines
13 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.
# 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 _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
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()
# --- 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 "<pack>/ymir work/ui/pattern/<name>.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)
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 _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 == null or _minimap_rect == null:
return
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)