feat(crafting): 工作台 3×3 合成界面——右键开台、槽位交换、输出格合成、关闭退回
- 会话层(工作台与合成表.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 全链路截图验证
This commit is contained in:
134
internal/ui/crafting.go
Normal file
134
internal/ui/crafting.go
Normal file
@@ -0,0 +1,134 @@
|
||||
//go:build gl
|
||||
|
||||
// 工作台界面(UI还原.md §3.4、工作台与合成表.md §5):
|
||||
// 176×166 窗口(gui/container/crafting_table.png)+ 3×3 合成网格 (30,17)
|
||||
// + 输出格 (124,35) + 热键栏行 (7,141);点击槽位与手持栈交换/合并。
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"mc/internal/item"
|
||||
)
|
||||
|
||||
// CraftingScreen 工作台合成界面。
|
||||
type CraftingScreen struct {
|
||||
bgTex uint32 // crafting_table.png(256×256,实际内容 176×166)
|
||||
items *item.Registry
|
||||
itemTex uint32
|
||||
itemUV map[string][4]float32
|
||||
// 方块物品图标回退来源(原版 block/* 图标在 blocks 图集)
|
||||
blockTex uint32
|
||||
blockUV map[string][4]float32
|
||||
|
||||
// 布局(逻辑 427×240 基准,UI还原.md §3.4)
|
||||
winW, winH float32 // 176×166
|
||||
}
|
||||
|
||||
// NewCraftingScreen 创建工作台界面。
|
||||
func NewCraftingScreen(bgTex uint32, items *item.Registry) *CraftingScreen {
|
||||
return &CraftingScreen{bgTex: bgTex, items: items,
|
||||
itemUV: map[string][4]float32{}, blockUV: map[string][4]float32{}, winW: 176, winH: 166}
|
||||
}
|
||||
|
||||
// SetItemAtlas 设置物品图标纹理与 UV 表(与 HUD 共用 items 图集)。
|
||||
func (s *CraftingScreen) SetItemAtlas(tex uint32, uv map[string][4]float32) {
|
||||
s.itemTex = tex
|
||||
s.itemUV = uv
|
||||
}
|
||||
|
||||
// SetBlockAtlas 设置方块图集(方块物品图标回退来源)。
|
||||
func (s *CraftingScreen) SetBlockAtlas(tex uint32, uv map[string][4]float32) {
|
||||
s.blockTex = tex
|
||||
s.blockUV = uv
|
||||
}
|
||||
|
||||
// 合成台窗口槽位(相对窗口左上角,UI还原.md §3.4)。
|
||||
var craftSlots = [10][2]float32{
|
||||
{30, 17}, {48, 17}, {66, 17},
|
||||
{30, 35}, {48, 35}, {66, 35},
|
||||
{30, 53}, {48, 53}, {66, 53}, // 3×3 网格
|
||||
{124, 35}, // 输出格
|
||||
}
|
||||
|
||||
// HitTest 命中检测:返回 "grid0".."grid8" / "result" / "hot0".."hot8" / ""。
|
||||
func (s *CraftingScreen) HitTest(mx, my float32, screenW, screenH, scale float32) string {
|
||||
bx, by := screenW/2-s.winW/2*scale, screenH/2-s.winH/2*scale
|
||||
hit := func(px, py float32) bool {
|
||||
return mx >= bx+px*scale && mx < bx+(px+18)*scale &&
|
||||
my >= by+py*scale && my < by+(py+18)*scale
|
||||
}
|
||||
for i, p := range craftSlots {
|
||||
if hit(p[0], p[1]) {
|
||||
if i < 9 {
|
||||
return "grid" + string(rune('0'+i))
|
||||
}
|
||||
return "result"
|
||||
}
|
||||
}
|
||||
// 热键栏行(窗口内底部,与物品栏窗口同规格)
|
||||
for i := 0; i < 9; i++ {
|
||||
if hit(7+float32(i)*18, 141) {
|
||||
return "hot" + string(rune('0'+i))
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Draw 绘制工作台界面(调用方已 Begin;mx/my 为鼠标像素坐标,held 为手持栈)。
|
||||
func (s *CraftingScreen) Draw(r *Renderer, grid [9]item.Stack, result item.Stack, hasResult bool, hotbar []item.Stack, held item.Stack, mx, my, scale float32) {
|
||||
w, h := float32(r.w), float32(r.h)
|
||||
// 半透明遮罩(原版容器界面背景压暗)
|
||||
r.DrawRect(0, 0, w, h, [4]float32{0, 0, 0, 0.35})
|
||||
// 窗口背景(纹理 256×256,内容区 176×166,UI还原.md §3.2)
|
||||
bx, by := w/2-s.winW/2*scale, h/2-s.winH/2*scale
|
||||
if s.bgTex != 0 {
|
||||
r.pushQuad(quad{x: bx, y: by, w: s.winW * scale, h: s.winH * scale,
|
||||
u0: 0, v0: 0, u1: 176.0 / 256.0, v1: 166.0 / 256.0,
|
||||
r: 1, g: 1, b: 1, a: 1, tex: s.bgTex})
|
||||
}
|
||||
// 3×3 网格 + 输出格
|
||||
for i, p := range craftSlots {
|
||||
var st item.Stack
|
||||
if i < 9 {
|
||||
st = grid[i]
|
||||
} else if hasResult {
|
||||
st = result
|
||||
}
|
||||
s.drawSlot(r, st, bx+p[0]*scale+1*scale, by+p[1]*scale+1*scale, scale)
|
||||
}
|
||||
// 热键栏行
|
||||
for i := 0; i < 9 && i < len(hotbar); i++ {
|
||||
s.drawSlot(r, hotbar[i], bx+(7+float32(i)*18)*scale+1*scale, by+141*scale+1*scale, scale)
|
||||
}
|
||||
// 手持栈跟随鼠标
|
||||
if !held.Empty() {
|
||||
s.drawSlot(r, held, mx-8*scale, my-8*scale, scale)
|
||||
}
|
||||
}
|
||||
|
||||
// drawSlot 在指定位置绘制物品图标(16×16 逻辑像素)。
|
||||
func (s *CraftingScreen) drawSlot(r *Renderer, st item.Stack, x, y, scale float32) {
|
||||
if st.Empty() || s.items == nil {
|
||||
return
|
||||
}
|
||||
def, ok := s.items.Get(st.Name)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
// 原版约定:block/* 图标在 blocks 图集,item/* 在 items 图集
|
||||
tex := s.itemTex
|
||||
uv, found := s.itemUV[def.Texture]
|
||||
if !found {
|
||||
uv, found = s.blockUV[def.Texture]
|
||||
tex = s.blockTex
|
||||
}
|
||||
if found && tex != 0 {
|
||||
r.pushQuad(quad{x: x, y: y, w: 16 * scale, h: 16 * scale,
|
||||
u0: uv[0], v0: uv[1], u1: uv[2], v1: uv[3],
|
||||
r: 1, g: 1, b: 1, a: 1, tex: tex})
|
||||
}
|
||||
if st.Count > 1 {
|
||||
r.DrawText(x+10*scale, y+8*scale, fmt.Sprintf("%d", st.Count), scale, [4]float32{1, 1, 1, 1})
|
||||
}
|
||||
}
|
||||
@@ -15,9 +15,11 @@ type HUD struct {
|
||||
sess *game.Session
|
||||
items *item.Registry
|
||||
|
||||
// 物品图标:items 图集纹理与 UV
|
||||
itemTex uint32
|
||||
itemUV map[string][4]float32
|
||||
// 物品图标: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
|
||||
@@ -40,7 +42,7 @@ type HUD struct {
|
||||
|
||||
// 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}
|
||||
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 图集)。
|
||||
@@ -49,6 +51,13 @@ func (h *HUD) SetItemAtlas(tex uint32, uv map[string][4]float32) {
|
||||
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
|
||||
@@ -172,10 +181,17 @@ func (h *HUD) drawItemIcon(r *Renderer, stack item.Stack, x, y float32) {
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if uv, found := h.itemUV[def.Texture]; found && h.itemTex != 0 {
|
||||
// 原版约定: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: h.itemTex})
|
||||
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})
|
||||
|
||||
Reference in New Issue
Block a user