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:
@@ -59,6 +59,7 @@ var (
|
||||
crackStage = flag.Int("crackstage", -1, "dev 测试:截图前把破坏进度视觉设置为该阶段(0–9)")
|
||||
spawnTest = flag.String("spawntest", "", "dev 测试:截图前在视线前方 5 格生成一个实体(zombie/animal/villager)")
|
||||
escTest = flag.Bool("esctest", false, "dev 测试:截图前注入一次 ESC(验证暂停菜单与光标释放链路)")
|
||||
craftTest = flag.Bool("crafttest", false, "dev 测试:截图前打开工作台并预填 2×2 木板(验证合成界面)")
|
||||
)
|
||||
|
||||
// clientApp 客户端应用:把会话与渲染粘合起来(engine.Ticker + engine.Renderer)。
|
||||
@@ -70,10 +71,12 @@ type clientApp struct {
|
||||
uiR *ui.Renderer
|
||||
hud *ui.HUD
|
||||
|
||||
// 屏幕状态机:0 主菜单 / 1 游戏中 / 2 暂停(UI还原.md §2)
|
||||
// 屏幕状态机:0 主菜单 / 1 游戏中 / 2 暂停 / 3 工作台(UI还原.md §2)
|
||||
screen int
|
||||
titleScreen *ui.TitleScreen
|
||||
pauseScreen *ui.PauseScreen
|
||||
craftScreen *ui.CraftingScreen
|
||||
held item.Stack // 合成界面手持栈(跟随鼠标)
|
||||
|
||||
// 联机(多人同步.md §2:无连接时为单机)
|
||||
net *netclient.Client
|
||||
@@ -106,10 +109,12 @@ type clientApp struct {
|
||||
frames int // 渲染帧计数(截图时机用)
|
||||
lastTime time.Time
|
||||
|
||||
lookInjected bool // dev 测试:-lookdeg 已注入
|
||||
crackShot bool // dev 测试:-crackstage 已注入
|
||||
spawnShot bool // dev 测试:-spawntest 已注入
|
||||
escShot bool // dev 测试:-esctest 已注入
|
||||
lookInjected bool // dev 测试:-lookdeg 已注入
|
||||
crackShot bool // dev 测试:-crackstage 已注入
|
||||
spawnShot bool // dev 测试:-spawntest 已注入
|
||||
escShot bool // dev 测试:-esctest 已注入
|
||||
craftShot bool // dev 测试:-crafttest 已注入
|
||||
craftClickShot bool // dev 测试:-crafttest 点击输出格已注入
|
||||
}
|
||||
|
||||
// worldMeshJob 待上传网格。
|
||||
@@ -172,6 +177,27 @@ func (a *clientApp) Tick(dt float64) {
|
||||
}
|
||||
a.in.escPressed = false
|
||||
return
|
||||
case 3: // 工作台(工作台与合成表.md §5:点击槽位交换、ESC 关闭)
|
||||
// dev 测试:crafttest 第二阶段——合成输出格(tick 线程,确定性早于截图)
|
||||
if *craftTest && !a.craftClickShot {
|
||||
a.craftClickShot = true
|
||||
a.handleCraftClick("result")
|
||||
a.logf("crafttest 第二阶段: 合成输出格(2×2 木板 → 工作台)")
|
||||
}
|
||||
if a.in.escPressed {
|
||||
a.closeCrafting()
|
||||
a.screen = 1
|
||||
a.requestCursor(true)
|
||||
a.in.escPressed = false
|
||||
return
|
||||
}
|
||||
if a.in.clicked {
|
||||
a.handleCraftClick(a.craftScreen.HitTest(a.in.clickX, a.in.clickY, float32(a.winW), float32(a.winH), 3))
|
||||
a.in.clicked = false
|
||||
}
|
||||
a.in.dyaw, a.in.dpitch = 0, 0 // 界面打开时不转动视角
|
||||
a.sess.Tick(dt)
|
||||
return
|
||||
}
|
||||
// 游戏中
|
||||
if a.in.escPressed {
|
||||
@@ -181,6 +207,12 @@ func (a *clientApp) Tick(dt float64) {
|
||||
return
|
||||
}
|
||||
a.in.apply(a.sess)
|
||||
// 右键命中工作台 → 打开合成界面(apply 内 TryOpenCrafting 已置状态)
|
||||
if a.sess.CraftingOpen() && a.screen == 1 {
|
||||
a.screen = 3
|
||||
a.requestCursor(false)
|
||||
a.logf("打开工作台合成界面")
|
||||
}
|
||||
// 手持方块 UV(选中槽变化时重算;渲染线程消费,渲染.md §5.2)
|
||||
if !a.handInit || a.sess.Inv.Selected != a.handSlot {
|
||||
a.computeHand()
|
||||
@@ -192,7 +224,7 @@ func (a *clientApp) Tick(dt float64) {
|
||||
a.sess.Cam.Pitch = *pitchDeg * math.Pi / 180.0
|
||||
}
|
||||
if *lookDeg != 0 {
|
||||
dx := float64(*lookDeg) * math.Pi / 180.0 / 0.0025 // 目标弧度 → 像素位移
|
||||
dx := float64(*lookDeg) * math.Pi / 180.0 / 0.0025 // 目标弧度 → 像素位移
|
||||
a.in.cursorCallback(nil, float64(a.winW)/2, float64(a.winH)/2) // 复位基准
|
||||
a.in.cursorCallback(nil, float64(a.winW)/2+dx, float64(a.winH)/2)
|
||||
a.logf("lookdeg 注入完成: +%.1f° 水平移动", *lookDeg)
|
||||
@@ -293,6 +325,82 @@ func (a *clientApp) injectDevVisuals() {
|
||||
}
|
||||
a.logf("spawntest 注入: %s 于 (%d,%d,%d)", *spawnTest, ex, gy+1, ez)
|
||||
}
|
||||
if *craftTest && !a.craftShot {
|
||||
a.craftShot = true
|
||||
// 预填 2×2 木板(槽 0/1/3/4)→ 产物为工作台(工作台与合成表.md §5 合成链路)
|
||||
for _, i := range []int{0, 1, 3, 4} {
|
||||
a.sess.CraftingSwap(i, item.Stack{Name: "oak_planks", Count: 1})
|
||||
}
|
||||
// 热键栏放 16 木板供界面展示与后续合成
|
||||
a.sess.Inv.Slots[0] = item.Stack{Name: "oak_planks", Count: 16}
|
||||
a.sess.OpenCrafting()
|
||||
a.screen = 3
|
||||
a.requestCursor(false)
|
||||
a.logf("crafttest 注入: 打开工作台并预填 2×2 木板")
|
||||
}
|
||||
}
|
||||
|
||||
// handleCraftClick 工作台界面点击:网格/热键栏槽与手持栈交换,输出格执行合成。
|
||||
func (a *clientApp) handleCraftClick(slot string) {
|
||||
switch {
|
||||
case slot == "":
|
||||
return
|
||||
case slot == "result":
|
||||
// 输出格:空手持取走产物;同名手持合并产物继续合成(工作台与合成表.md §5)
|
||||
res, ok := a.sess.CraftingResult()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if a.held.Empty() {
|
||||
if r, ok := a.sess.CraftingTakeResult(); ok {
|
||||
a.held = r
|
||||
}
|
||||
} else if a.held.Name == res.Name && a.held.Count < a.held.Limit(a.sess.Items) {
|
||||
if r, ok := a.sess.CraftingTakeResult(); ok {
|
||||
merged, remain := inventory.Merge(a.held.Limit(a.sess.Items), a.held, r)
|
||||
a.held = merged
|
||||
if !remain.Empty() {
|
||||
if rem := a.sess.Inv.Add(remain); !rem.Empty() {
|
||||
a.sess.Entities.SpawnDrop(rem.Name, int(rem.Count), a.sess.Player.Pos[0], a.sess.Player.Pos[1]+1, a.sess.Player.Pos[2])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case len(slot) == 5 && slot[:4] == "grid":
|
||||
i := int(slot[4] - '0')
|
||||
a.held = a.sess.CraftingSwap(i, a.held)
|
||||
case len(slot) == 4 && slot[:3] == "hot":
|
||||
i := int(slot[3] - '0')
|
||||
if i >= 0 && i < inventory.HotbarSize {
|
||||
cur := a.sess.Inv.Slots[i]
|
||||
switch {
|
||||
case a.held.Empty():
|
||||
a.sess.Inv.Slots[i] = item.Stack{}
|
||||
a.held = cur
|
||||
case cur.Empty():
|
||||
a.sess.Inv.Slots[i] = a.held
|
||||
a.held = item.Stack{}
|
||||
case cur.Name == a.held.Name:
|
||||
merged, remain := inventory.Merge(a.held.Limit(a.sess.Items), cur, a.held)
|
||||
a.sess.Inv.Slots[i] = merged
|
||||
a.held = remain
|
||||
default:
|
||||
a.sess.Inv.Slots[i] = a.held
|
||||
a.held = cur
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// closeCrafting 关闭合成界面:材料退回背包、手持栈放回背包。
|
||||
func (a *clientApp) closeCrafting() {
|
||||
a.sess.CloseCrafting()
|
||||
if !a.held.Empty() {
|
||||
if rem := a.sess.Inv.Add(a.held); !rem.Empty() {
|
||||
a.sess.Entities.SpawnDrop(rem.Name, int(rem.Count), a.sess.Player.Pos[0], a.sess.Player.Pos[1]+1, a.sess.Player.Pos[2])
|
||||
}
|
||||
a.held = item.Stack{}
|
||||
}
|
||||
}
|
||||
|
||||
// requestCursor 请求切换光标捕获模式(由 tick 写入,渲染线程消费——
|
||||
@@ -353,10 +461,15 @@ func (a *clientApp) Render() {
|
||||
// 消费光标模式请求(主线程执行 GLFW 调用)
|
||||
if req := a.cursorReq.Load(); req >= 0 {
|
||||
a.rend.SetCursorMode(req == 1)
|
||||
if req == 1 {
|
||||
// 重新捕获:光标跳回窗口中心,重置基准防视角瞬跳(UI与输入.md §6)
|
||||
a.in.lastX, a.in.lastY = float64(a.winW)/2, float64(a.winH)/2
|
||||
a.in.firstMouse = true
|
||||
}
|
||||
a.cursorReq.Store(-1)
|
||||
}
|
||||
// dev 测试:截图前注入破坏裂纹/实体/ESC(必须在 3D 绘制之前,截图帧才能拍到)
|
||||
if a.frames == 195 && (*crackStage >= 0 || *spawnTest != "" || *escTest) {
|
||||
// dev 测试:截图前注入破坏裂纹/实体/ESC/工作台(必须在 3D 绘制之前,截图帧才能拍到)
|
||||
if a.frames == 195 && (*crackStage >= 0 || *spawnTest != "" || *escTest || *craftTest) {
|
||||
a.injectDevVisuals()
|
||||
}
|
||||
// 上传 tick 构建好的网格(GL 调用必须在渲染线程)
|
||||
@@ -394,9 +507,11 @@ func (a *clientApp) Render() {
|
||||
}
|
||||
|
||||
// 破坏裂纹叠加(方块交互与动画.md §4:progress→destroy_stage 0–9)
|
||||
if prog := a.sess.BreakProgress(); prog > 0 {
|
||||
t := a.sess.BreakTarget()
|
||||
a.rend.DrawCrackOverlay(float32(t.X), float32(t.Y), float32(t.Z), int(prog*10))
|
||||
if a.screen == 1 {
|
||||
if prog := a.sess.BreakProgress(); prog > 0 {
|
||||
t := a.sess.BreakTarget()
|
||||
a.rend.DrawCrackOverlay(float32(t.X), float32(t.Y), float32(t.Z), int(prog*10))
|
||||
}
|
||||
}
|
||||
// 手持方块上传与绘制(渲染.md §5.2;上传必须是渲染线程)
|
||||
a.mu.Lock()
|
||||
@@ -414,7 +529,9 @@ func (a *clientApp) Render() {
|
||||
a.rend.ClearHand()
|
||||
}
|
||||
}
|
||||
a.rend.DrawHand()
|
||||
if a.screen == 1 {
|
||||
a.rend.DrawHand()
|
||||
}
|
||||
|
||||
// UI pass(UI与输入.md §2:3D 之后、状态隔离)
|
||||
a.uiR.Begin()
|
||||
@@ -427,6 +544,13 @@ func (a *clientApp) Render() {
|
||||
case 2:
|
||||
a.hud.Draw(a.uiR) // 暂停时世界仍可见(UI还原.md §2)
|
||||
a.pauseScreen.Draw(a.uiR, mx, my)
|
||||
case 3: // 工作台合成界面(UI还原.md §3.4)
|
||||
var grid [9]item.Stack
|
||||
for i := range grid {
|
||||
grid[i] = a.sess.CraftSlot(i)
|
||||
}
|
||||
res, hasRes := a.sess.CraftingResult()
|
||||
a.craftScreen.Draw(a.uiR, grid, res, hasRes, a.sess.Inv.Slots[:inventory.HotbarSize], a.held, mx, my, 3)
|
||||
}
|
||||
a.uiR.Flush()
|
||||
a.rend.EndFrame()
|
||||
@@ -681,9 +805,10 @@ func main() {
|
||||
uiR.SetFont(fontAtlas)
|
||||
}
|
||||
hud := ui.NewHUD(sess, items, 427, 240, guiScale)
|
||||
var itemTex uint32
|
||||
itemUV := make(map[string][4]float32, len(itemUVs))
|
||||
if itemImg != nil {
|
||||
itemTex := uiR.UploadImage(itemImg)
|
||||
itemUV := make(map[string][4]float32, len(itemUVs))
|
||||
itemTex = uiR.UploadImage(itemImg)
|
||||
for k, rect := range itemUVs {
|
||||
W, H := float32(itemImg.Bounds().Dx()), float32(itemImg.Bounds().Dy())
|
||||
itemUV[k] = [4]float32{
|
||||
@@ -693,6 +818,17 @@ func main() {
|
||||
}
|
||||
hud.SetItemAtlas(itemTex, itemUV)
|
||||
}
|
||||
// 方块物品图标回退 blocks 图集(原版 block/* 图标不在 items 图集)
|
||||
blockUITex := uiR.UploadImage(atlasImg)
|
||||
blockUV := make(map[string][4]float32, len(uvRects))
|
||||
for k, rect := range uvRects {
|
||||
W, H := float32(atlasImg.Bounds().Dx()), float32(atlasImg.Bounds().Dy())
|
||||
blockUV[k] = [4]float32{
|
||||
float32(rect.Min.X) / W, float32(rect.Min.Y) / H,
|
||||
float32(rect.Max.X) / W, float32(rect.Max.Y) / H,
|
||||
}
|
||||
}
|
||||
hud.SetBlockAtlas(blockUITex, blockUV)
|
||||
// 原版 HUD 精灵(UI还原.md §3.4:gui/sprites/hud/*)
|
||||
loadTex := func(key string) uint32 {
|
||||
p, ok := am.ResolveTexture(key)
|
||||
@@ -728,8 +864,16 @@ func main() {
|
||||
}
|
||||
titleScreen, pauseScreen := buildScreens(uiR, am, lang)
|
||||
|
||||
// 工作台合成界面(UI还原.md §3.4:gui/container/crafting_table.png + 3×3 网格)
|
||||
craftScreen := ui.NewCraftingScreen(loadTex("gui/container/crafting_table"), items)
|
||||
craftScreen.SetItemAtlas(itemTex, itemUV)
|
||||
craftScreen.SetBlockAtlas(blockUITex, blockUV)
|
||||
|
||||
// 6) 输入(鼠标捕获 + 键鼠回调)
|
||||
in := newInputState(rend.Window())
|
||||
// 光标初始位置 = 窗口中心(捕获模式下 GLFW 会把光标重置到中心;
|
||||
// 合成界面手持栈跟随光标,初始化为中心避免出现在左上角)
|
||||
in.cursorX, in.cursorY = float64(cfg.Client.WindowWidth)/2, float64(cfg.Client.WindowHeight)/2
|
||||
|
||||
app := &clientApp{
|
||||
sess: sess,
|
||||
@@ -740,6 +884,7 @@ func main() {
|
||||
hud: hud,
|
||||
titleScreen: titleScreen,
|
||||
pauseScreen: pauseScreen,
|
||||
craftScreen: craftScreen,
|
||||
log: log,
|
||||
remotePlayers: make(map[uint32][3]float64),
|
||||
meshBuilder: mesh.NewBuilder(reg, uvFn),
|
||||
|
||||
@@ -204,9 +204,11 @@ func (in *inputState) apply(s *game.Session) {
|
||||
s.EndBreak()
|
||||
}
|
||||
|
||||
// 放置
|
||||
// 放置(右键;命中工作台则打开合成界面,工作台与合成表.md §5)
|
||||
if in.placeDown {
|
||||
s.PlaceSelected()
|
||||
if !s.TryOpenCrafting() {
|
||||
s.PlaceSelected()
|
||||
}
|
||||
in.placeDown = false
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user