- 会话层(工作台与合成表.md §5):TryOpenCrafting 右键命中工作台开台; CraftingSwap 网格槽与手持栈交换/合并;CraftingTakeResult 匹配配方 (有序优先)消耗材料取产物;CloseCrafting 材料退回背包(放不下掉落) - 客户端屏幕状态 3:gui/container/crafting_table.png 176×166 窗口、 3×3 网格 (30,17) + 输出格 (124,35) + 热键栏行 (7,141)(UI还原.md §3.4); ESC 关闭并回收手持栈;界面打开时释放光标、关闭恢复捕获并重置鼠标基准 - 物品图标双图集:block/* 图标回退 blocks 图集(原版 items 图集不收录 block 纹理,此前方块物品图标全部缺失) - 单测:shaped 2×2 木板→工作台、shapeless 原木→木板、木镐多符号、 关闭退回材料;dev 参数 -crafttest 全链路截图验证
200 lines
6.0 KiB
Go
200 lines
6.0 KiB
Go
//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(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
|
||
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{}, blockUV: 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
// 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
|
||
}
|
||
// 原版约定: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 * 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: tex})
|
||
}
|
||
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})
|
||
}
|
||
}
|