Files
ngzz-mc/internal/ui/hud.go
2026-09-19 14:21:08 +08:00

381 lines
11 KiB
Go
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.
//go:build gl
// HUD 抬头显示:准星、热键栏、生命/饥饿、破坏进度(UI还原.md §2、§3.4 规格)。
package ui
import (
"fmt"
"math"
"mc/internal/game"
"mc/internal/item"
"mc/internal/physics"
)
// HUD 游戏内 HUD。
type HUD struct {
sess *game.Session
items *item.Registry
// 物品图标:items 图集纹理与 UV(item/*);方块物品回退 blocks 图集(block/*)
itemTex uint32
itemUV map[string][4]float32
blockTex uint32
blockUV map[string][4]float32
// 原版 HUD 精灵(UI还原.md §3.4:gui/sprites/hud/*)
crossTex uint32 // crosshair.png 15×15
hotbarTex uint32 // hotbar.png 182×22
hotbarSel uint32 // hotbar_selection.png 24×24
heartFull uint32 // heart/full.png 9×9
heartHalf uint32 // heart/half.png
heartCont uint32 // heart/container.png
foodFull uint32 // food_full.png 9×9
foodHalf uint32 // food_half.png
airFull uint32 // air.png 9×9
airBurst uint32 // air_bursting.png
haveSprites bool
// 准星识别条(左下角:图标+名称+暗底,UI还原.md §3.4)
targetTex uint32
targetW float32
targetH float32
targetBlock string
// 附近音效字幕(识别条下方)
captionTex []uint32
captionW []float32
captionH []float32
// 小地图(左上角,UI还原.md §3.5)
minimapTex uint32
// 水下渲染(渲染.md §8 水体效果:蓝色叠加)
underwater bool
// 逻辑分辨率与缩放(UI还原.md §3.1:427×240 基准 × 整数缩放)
baseW, baseH float32
scale float32
// OnlineCount 在线玩家数(联机时由调用方注入;nil 表示单机不显示)
OnlineCount func() int
punchT float32
punchSlot int
}
// NewHUD 创建 HUD。
func NewHUD(sess *game.Session, items *item.Registry, baseW, baseH, scale float32) *HUD {
return &HUD{sess: sess, items: items, itemUV: map[string][4]float32{}, blockUV: map[string][4]float32{}, baseW: baseW, baseH: baseH, scale: scale}
}
// SetSession 切换存档后换会话。
func (h *HUD) SetSession(sess *game.Session) { h.sess = sess }
// SetItemAtlas 设置物品图标纹理与 UV 表(items 图集)。
func (h *HUD) SetItemAtlas(tex uint32, uv map[string][4]float32) {
h.itemTex = tex
h.itemUV = uv
}
// SetBlockAtlas 设置方块图集(方块物品图标的回退来源,设计.md §1.3 原版
// block/* 图标在 blocks 图集而非 items 图集)。
func (h *HUD) SetBlockAtlas(tex uint32, uv map[string][4]float32) {
h.blockTex = tex
h.blockUV = uv
}
// SetSprites 设置原版 HUD 精灵纹理(跨 package 上传后注入)。
func (h *HUD) SetSprites(cross, hotbar, hotbarSel, heartFull, heartHalf, heartCont, foodFull, foodHalf uint32) {
h.crossTex, h.hotbarTex, h.hotbarSel = cross, hotbar, hotbarSel
h.heartFull, h.heartHalf, h.heartCont = heartFull, heartHalf, heartCont
h.foodFull, h.foodHalf = foodFull, foodHalf
h.haveSprites = cross != 0
}
// SetAirSprites 呼吸气泡(UI还原.md §3.4)。
func (h *HUD) SetAirSprites(full, burst uint32) {
h.airFull, h.airBurst = full, burst
}
// SetTarget 设置准星识别条(名称纹理 + 方块注册名;tex=0 清除)。
func (h *HUD) SetTarget(tex uint32, w, hgt float32, blockName string) {
h.targetTex, h.targetW, h.targetH, h.targetBlock = tex, w, hgt, blockName
}
// SetCaptions 设置附近音效字幕行纹理(渲染线程注入)。
func (h *HUD) SetCaptions(tex []uint32, w, hgt []float32) {
h.captionTex, h.captionW, h.captionH = tex, w, hgt
}
// SetMinimap 设置小地图纹理(渲染线程注入;tex=0 清除)。
func (h *HUD) SetMinimap(tex uint32) { h.minimapTex = tex }
// SetUnderwater 设置水下状态(蓝色叠加与雾效)。
func (h *HUD) SetUnderwater(b bool) { h.underwater = b }
// SetScale 更新整数 guiScale(窗口缩放后,UI还原.md §3.1)。
func (h *HUD) SetScale(scale float32) {
if scale < 1 {
scale = 1
}
h.scale = scale
}
// Punch 热键栏切槽弹出缩回(约 150ms,1.4× → 1.0×)。
func (h *HUD) Punch(slot int) {
h.punchSlot = slot
h.punchT = 1
}
// TickAnims 衰减弹出。
func (h *HUD) TickAnims(dt float32) {
if h.punchT > 0 {
h.punchT -= dt / 0.15
if h.punchT < 0 {
h.punchT = 0
}
}
}
// HotbarIconPos 热键栏槽中心(屏幕像素)。
func (h *HUD) HotbarIconPos(slot int, screenW, screenH float32) (x, y float32) {
s := h.scale
barW, barH := 182*s, 22*s
bx, by := screenW/2-barW/2, screenH-barH
if slot < 0 {
slot = 0
}
if slot > 8 {
slot = 8
}
return bx + float32(slot)*20*s + 3*s + 8*s, by + 3*s + 8*s
}
// DrawIconAt 在任意位置画物品图标(拾取飞入)。
func (h *HUD) DrawIconAt(r *Renderer, name string, x, y, size float32) {
if name == "" || h.items == nil {
return
}
def, ok := h.items.Get(name)
if !ok {
return
}
tex := h.itemTex
uv, found := h.itemUV[def.Texture]
if !found {
uv, found = h.blockUV[def.Texture]
tex = h.blockTex
}
if found && tex != 0 {
r.pushQuad(quad{x: x - size/2, y: y - size/2, w: size, h: size,
u0: uv[0], v0: uv[1], u1: uv[2], v1: uv[3],
r: 1, g: 1, b: 1, a: 1, tex: tex})
}
}
// Draw 每帧绘制 HUD(调用方已 Begin)。
func (h *HUD) Draw(r *Renderer) {
w, hgt := float32(r.w), float32(r.h)
s := h.scale
// 水下效果:蓝色叠加(渲染.md §8 水体)
if h.underwater {
r.DrawRect(0, 0, w, hgt, [4]float32{0.05, 0.2, 0.45, 0.42})
}
// 准星(原版 crosshair.png 15×15,UI还原.md §3.4)
cx, cy := w/2, hgt/2
if h.haveSprites {
h.blit(r, h.crossTex, cx-7.5*s, cy-7.5*s, 15*s, 15*s)
} else {
white := [4]float32{1, 1, 1, 1}
r.DrawRect(cx-1*s, cy-8*s, 2*s, 16*s, white)
r.DrawRect(cx-8*s, cy-1*s, 16*s, 2*s, white)
}
// 热键栏(原版 hotbar.png 182×22,底部居中,UI还原.md §3.4)
barW, barH := 182*s, 22*s
bx, by := cx-barW/2, hgt-barH
if h.haveSprites {
h.blit(r, h.hotbarTex, bx, by, barW, barH)
} else {
r.DrawRect(bx, by, barW, barH, [4]float32{0, 0, 0, 0.5})
}
for i := 0; i < 9; i++ {
if i == h.sess.Inv.Selected {
if h.haveSprites {
h.blit(r, h.hotbarSel, bx+float32(i)*20*s-2*s, by-1*s, 24*s, 24*s)
} else {
r.DrawRect(bx+float32(i)*20*s+1*s, by+3*s, 20*s, 20*s, [4]float32{1, 1, 1, 0.9})
}
}
h.drawItemIcon(r, h.sess.Inv.Slots[i], bx+float32(i)*20*s+3*s, by+3*s, i)
}
// 生命:10 心(原版 9×9 图标,屏幕左下,UI还原.md §3.4 布局:
// 饥饿行 y=h-39,生命行在其上 y=h-49)
hp := h.sess.PlayerHealth()
hx := w/2 - 91*s
hy := hgt - 49*s
for i := 0; i < 10; i++ {
x := hx + float32(i)*8*s
// 底:容器(空槽)
if h.haveSprites {
h.blit(r, h.heartCont, x, hy, 9*s, 9*s)
}
switch {
case hp >= (i+1)*2: // 满心
if h.haveSprites {
h.blit(r, h.heartFull, x, hy, 9*s, 9*s)
} else {
r.DrawRect(x, hy, 7*s, 7*s, [4]float32{1, 0.1, 0.1, 1})
}
case hp == i*2+1: // 半心
if h.haveSprites {
h.blit(r, h.heartHalf, x, hy, 9*s, 9*s)
} else {
r.DrawRect(x, hy, 3.5*s, 7*s, [4]float32{1, 0.1, 0.1, 1})
}
default: // 空
if !h.haveSprites {
r.DrawRect(x, hy, 7*s, 7*s, [4]float32{0.2, 0.2, 0.2, 0.6})
}
}
}
// 饥饿:10 鸡腿(原版 food_full,屏幕**右下**——血条左、饥饿右的经典布局)
food := h.sess.PlayerHunger()
fy := hgt - 39*s
fx := w/2 + 11*s // 与左侧生命行镜像对称(w/2+11 .. w/2+91)
for i := 0; i < 10; i++ {
x := fx + float32(i)*8*s
switch {
case food >= (i+1)*2:
if h.haveSprites {
h.blit(r, h.foodFull, x, fy, 9*s, 9*s)
} else {
r.DrawRect(x, fy, 7*s, 7*s, [4]float32{0.6, 0.4, 0.15, 1})
}
case food == i*2+1:
if h.haveSprites {
h.blit(r, h.foodHalf, x, fy, 9*s, 9*s)
} else {
r.DrawRect(x, fy, 3.5*s, 7*s, [4]float32{0.6, 0.4, 0.15, 1})
}
}
}
// 呼吸气泡:眼睛在水时画在饥饿行上方(UI还原.md §3.4)
if h.sess.Player.EyeInWater || h.sess.PlayerAir() < physics.AirMax-0.01 {
bubbles := int(math.Ceil(h.sess.PlayerAir() / 1.5)) // 15s / 10 格
if bubbles > 10 {
bubbles = 10
}
ay := hgt - 49*s
for i := 0; i < 10; i++ {
x := fx + float32(i)*8*s
if i < bubbles {
if h.haveSprites && h.airFull != 0 {
h.blit(r, h.airFull, x, ay, 9*s, 9*s)
} else {
r.DrawRect(x, ay, 7*s, 7*s, [4]float32{0.7, 0.85, 1, 0.9})
}
} else if h.haveSprites && h.airBurst != 0 && i == bubbles {
h.blit(r, h.airBurst, x, ay, 9*s, 9*s)
}
}
}
// 破坏进度(方块交互与动画.md §4:3D 裂纹由渲染层叠加,HUD 不画进度条)
// 坐标调试(后置移除)
r.DrawText(w/2, hgt-70*s, fmt.Sprintf("XYZ %.1f / %.1f / %.1f", h.sess.Player.Pos[0], h.sess.Player.Pos[1], h.sess.Player.Pos[2]), 1*s, [4]float32{1, 1, 1, 1})
// 联机在线人数(多人同步.md §3)
if h.OnlineCount != nil {
r.DrawText(w/2, hgt-82*s, fmt.Sprintf("在线玩家: %d", h.OnlineCount()), 1*s, [4]float32{1, 1, 1, 1})
}
// 准星识别条 + 附近音效字幕(UI还原.md §3.4)
baseY := hgt - 96*s
if h.targetTex != 0 || h.targetBlock != "" {
pad, icon, gap := 3*s, 16*s, 4*s
barH := icon + pad*2
if h.targetH+pad*2 > barH {
barH = h.targetH + pad*2
}
barW := pad + icon + gap + h.targetW + pad
if barW < 40*s {
barW = 40 * s
}
r.DrawRect(2*s, baseY-barH, barW, barH, [4]float32{0, 0, 0, 0.62})
h.drawItemIcon(r, item.Stack{Name: h.targetBlock, Count: 1}, 2*s+pad, baseY-barH+pad, -1)
if h.targetTex != 0 {
ty := baseY - barH + (barH-h.targetH)/2
h.blit(r, h.targetTex, 2*s+pad+icon+gap, ty, h.targetW, h.targetH)
}
}
capY := baseY + 1*s
for i := range h.captionTex {
if h.captionTex[i] == 0 {
continue
}
cw, ch := h.captionW[i], h.captionH[i]
r.DrawRect(2*s, capY, cw+6*s, ch+2*s, [4]float32{0, 0, 0, 0.45})
h.blit(r, h.captionTex[i], 5*s, capY+1*s, cw, ch)
capY += ch + 3*s
}
// 小地图(左上角 96×96 逻辑像素 + 边框;玩家标记已烘焙进地图图——
// 覆盖层 quad 在批处理末尾存在颜色异常,见 updateMinimap drawMarker)
if h.minimapTex != 0 {
const m = 96.0
h.blit(r, h.minimapTex, 2*s, 2*s, m*s, m*s)
r.DrawRect(1.5*s, 1.5*s, (m+1)*s, 1*s, [4]float32{0, 0, 0, 0.7})
r.DrawRect(1.5*s, (m+1.5)*s, (m+1)*s, 1*s, [4]float32{0, 0, 0, 0.7})
r.DrawRect(1.5*s, 1.5*s, 1*s, (m+1)*s, [4]float32{0, 0, 0, 0.7})
r.DrawRect((m+0.5)*s, 1.5*s, 1*s, (m+1)*s, [4]float32{0, 0, 0, 0.7})
}
}
// blit 以纹理原比例绘制一个四边形(UI 精灵)。
func (h *HUD) blit(r *Renderer, tex uint32, x, y, w, hgt float32) {
if tex == 0 {
return
}
r.pushQuad(quad{x: x, y: y, w: w, h: hgt, u0: 0, v0: 0, u1: 1, v1: 1,
r: 1, g: 1, b: 1, a: 1, tex: tex})
}
// drawItemIcon 在槽位绘制物品图标 + 数量。
func (h *HUD) drawItemIcon(r *Renderer, stack item.Stack, x, y float32, slot int) {
s := h.scale
if stack.Empty() {
return
}
def, ok := h.items.Get(stack.Name)
if !ok {
return
}
sc := s
if slot >= 0 && slot == h.punchSlot && h.punchT > 0 {
sc = s * (1 + 0.4*h.punchT)
x -= (16*sc - 16*s) / 2
y -= (16*sc - 16*s) / 2
}
// 原版约定:block/* 图标在 blocks 图集,item/* 在 items 图集
tex := h.itemTex
uv, found := h.itemUV[def.Texture]
if !found {
uv, found = h.blockUV[def.Texture]
tex = h.blockTex
}
if found && tex != 0 {
r.pushQuad(quad{x: x, y: y, w: 16 * sc, h: 16 * sc,
u0: uv[0], v0: uv[1], u1: uv[2], v1: uv[3],
r: 1, g: 1, b: 1, a: 1, tex: tex})
}
if stack.Count > 1 {
r.DrawText(x+10*sc, y+8*sc, fmt.Sprintf("%d", stack.Count), 1*s, [4]float32{1, 1, 1, 1})
}
drawDurabilityBar(r, stack, def, x, y, sc)
}