Files
ngzz-mc/internal/ui/hud.go
NianGao Dev d52bf1126f feat(client): 植被染色、破坏裂纹叠加、手持方块与实体广告牌渲染;原版 HUD 精灵
- 植被染色(渲染.md §4.1):blocks.json 增 tint 标记,图集构建后按
  colormap/grass|foliage 中心色乘色——原版材质包草顶/树叶为灰度图,不染色全灰
- 破坏裂纹(方块交互与动画.md §4):destroy_stage_0..9 裂纹盒叠加破坏目标方块
- 第一人称手持方块(渲染.md §5.2):选中槽放置方块六面 UV 视图空间立方体
- 实体广告牌(渲染.md §5.3):僵尸/村民/动物按贴图渲染,动物按 ID 轮换外观
- HUD 换原版精灵:crosshair/hotbar/hotbar_selection/heart/food(UI还原.md §3.4)
- 主菜单标题/按钮标签按请求字号光栅化(修复 24px 固定字号);版本角标 CJK
- 恢复背面剔除(相机修复后 '脚下黑块' 误诊解除);ESC 光标请求原子化防竞态
- dev 测试参数:-give/-crackstage/-spawntest/-esctest
2026-08-16 10:08:40 +08:00

184 lines
5.5 KiB
Go
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.
//go:build gl
// HUD 抬头显示:准星、热键栏、生命/饥饿、破坏进度(UI还原.md §2、§3.4 规格)。
package ui
import (
"fmt"
"mc/internal/game"
"mc/internal/item"
)
// HUD 游戏内 HUD。
type HUD struct {
sess *game.Session
items *item.Registry
// 物品图标:items 图集纹理与 UV
itemTex uint32
itemUV 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
haveSprites bool
// 逻辑分辨率与缩放(UI还原.md §3.1:427×240 基准 × 整数缩放)
baseW, baseH float32
scale float32
// OnlineCount 在线玩家数(联机时由调用方注入;nil 表示单机不显示)
OnlineCount func() 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{}, baseW: baseW, baseH: baseH, scale: scale}
}
// SetItemAtlas 设置物品图标纹理与 UV 表(items 图集)。
func (h *HUD) SetItemAtlas(tex uint32, uv map[string][4]float32) {
h.itemTex = tex
h.itemUV = 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
}
// Draw 每帧绘制 HUD(调用方已 Begin)。
func (h *HUD) Draw(r *Renderer) {
w, hgt := float32(r.w), float32(r.h)
s := h.scale
// 准星(原版 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)
}
// 生命: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 := 20 // 饥饿值(饥饿系统后置,恒定 20)
fy := hgt - 39*s
for i := 0; i < 10; i++ {
x := hx + 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})
}
}
}
// 破坏进度(方块交互与动画.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})
}
}
// 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) {
s := h.scale
if stack.Empty() {
return
}
def, ok := h.items.Get(stack.Name)
if !ok {
return
}
if uv, found := h.itemUV[def.Texture]; found && h.itemTex != 0 {
r.pushQuad(quad{x: x, y: y, w: 16 * s, h: 16 * s,
u0: uv[0], v0: uv[1], u1: uv[2], v1: uv[3],
r: 1, g: 1, b: 1, a: 1, tex: h.itemTex})
}
if stack.Count > 1 {
r.DrawText(x+10*s, y+8*s, fmt.Sprintf("%d", stack.Count), 1*s, [4]float32{1, 1, 1, 1})
}
}