Files

402 lines
15 KiB
GDScript
Raw Permalink 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 _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()
# 技能快捷栏由 ui/quickbar.gd 管理:它恢复服务端的 36 个槽位、显示
# 技能图标并处理冷却。这里以前画的静态空槽会和它重叠,让实际技能看起来
# 没有加载,因此 HUD 不再创建第二条快捷栏。
_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 "<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)
_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_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)