//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) }