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:
@@ -50,6 +50,10 @@ type Session struct {
|
||||
|
||||
// 创造模式(跳过进度直接破坏)
|
||||
Creative bool
|
||||
|
||||
// 工作台状态(工作台与合成表.md §5:3×3 网格 + 输出格)
|
||||
craftOpen bool
|
||||
craftGrid [9]item.Stack
|
||||
}
|
||||
|
||||
// NewSession 创建会话。
|
||||
@@ -242,6 +246,133 @@ func (s *Session) overlapsPlayer(x, y, z int32) bool {
|
||||
float64(z)+1 > pz-hw && float64(z) < pz+hw
|
||||
}
|
||||
|
||||
// ---- 工作台合成(工作台与合成表.md §5)----
|
||||
|
||||
// craftingTableID 工作台方块 ID(注册表查询缓存值)。
|
||||
func (s *Session) craftingTableID() uint16 {
|
||||
id, ok := s.Reg.ID("crafting_table")
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// TryOpenCrafting 视线 5 格内命中工作台则打开合成界面(右键触发)。
|
||||
// 返回 true 表示已打开(调用方不应再执行放置)。
|
||||
func (s *Session) TryOpenCrafting() bool {
|
||||
h, ok := s.Cast(5)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
if s.World.Block(h.X, h.Y, h.Z).ID() != s.craftingTableID() {
|
||||
return false
|
||||
}
|
||||
s.craftOpen = true
|
||||
return true
|
||||
}
|
||||
|
||||
// OpenCrafting 直接打开合成界面(开发测试/后续按键绑定用)。
|
||||
func (s *Session) OpenCrafting() { s.craftOpen = true }
|
||||
|
||||
// CraftingOpen 合成界面是否打开。
|
||||
func (s *Session) CraftingOpen() bool { return s.craftOpen }
|
||||
|
||||
// CraftSlot 读网格槽(只读,UI 用)。
|
||||
func (s *Session) CraftSlot(i int) item.Stack {
|
||||
if i < 0 || i >= 9 {
|
||||
return item.Stack{}
|
||||
}
|
||||
return s.craftGrid[i]
|
||||
}
|
||||
|
||||
// CraftingSwap 网格槽与手持栈交换/合并(同物品合并到上限),返回新的手持栈。
|
||||
func (s *Session) CraftingSwap(i int, held item.Stack) item.Stack {
|
||||
if i < 0 || i >= 9 {
|
||||
return held
|
||||
}
|
||||
cur := s.craftGrid[i]
|
||||
switch {
|
||||
case held.Empty():
|
||||
s.craftGrid[i] = item.Stack{}
|
||||
return cur
|
||||
case cur.Empty():
|
||||
s.craftGrid[i] = held
|
||||
return item.Stack{}
|
||||
case cur.Name == held.Name:
|
||||
merged, remain := inventory.Merge(held.Limit(s.Items), cur, held)
|
||||
s.craftGrid[i] = merged
|
||||
return remain
|
||||
default:
|
||||
s.craftGrid[i] = held
|
||||
return cur
|
||||
}
|
||||
}
|
||||
|
||||
// craftResult 按网格匹配配方(有序优先,再无序),返回产物。
|
||||
func (s *Session) craftResult() (item.Stack, bool) {
|
||||
var g [3][3]string
|
||||
counts := make(map[string]int)
|
||||
for i := 0; i < 9; i++ {
|
||||
st := s.craftGrid[i]
|
||||
if st.Empty() {
|
||||
continue
|
||||
}
|
||||
g[i/3][i%3] = st.Name
|
||||
counts[st.Name]++
|
||||
}
|
||||
if len(counts) == 0 {
|
||||
return item.Stack{}, false
|
||||
}
|
||||
// 有序配方优先(避免无序规则误吞有序网格,工作台与合成表.md §4)
|
||||
if rc, ok := s.Recipes.MatchShaped(g); ok {
|
||||
return item.Stack{Name: rc.Result.Item, Count: uint8(min(rc.Result.Count, 127))}, true
|
||||
}
|
||||
if rc, ok := s.Recipes.MatchShapeless(counts); ok {
|
||||
return item.Stack{Name: rc.Result.Item, Count: uint8(min(rc.Result.Count, 127))}, true
|
||||
}
|
||||
return item.Stack{}, false
|
||||
}
|
||||
|
||||
// CraftingResult 当前网格的合成产物(无则 ok=false,UI 输出格用)。
|
||||
func (s *Session) CraftingResult() (item.Stack, bool) { return s.craftResult() }
|
||||
|
||||
// CraftingTakeResult 执行一次合成:消耗网格材料并返回产物。
|
||||
// 网格匹配失败返回 false(材料不动)。
|
||||
func (s *Session) CraftingTakeResult() (item.Stack, bool) {
|
||||
res, ok := s.craftResult()
|
||||
if !ok {
|
||||
return item.Stack{}, false
|
||||
}
|
||||
for i := range s.craftGrid {
|
||||
if s.craftGrid[i].Empty() {
|
||||
continue
|
||||
}
|
||||
if s.craftGrid[i].Count <= 1 {
|
||||
s.craftGrid[i] = item.Stack{}
|
||||
} else {
|
||||
s.craftGrid[i].Count--
|
||||
}
|
||||
}
|
||||
return res, true
|
||||
}
|
||||
|
||||
// CloseCrafting 关闭合成界面:网格剩余材料退回背包(放不下掉落地面)。
|
||||
func (s *Session) CloseCrafting() {
|
||||
if !s.craftOpen {
|
||||
return
|
||||
}
|
||||
for i := range s.craftGrid {
|
||||
if s.craftGrid[i].Empty() {
|
||||
continue
|
||||
}
|
||||
if rem := s.Inv.Add(s.craftGrid[i]); !rem.Empty() {
|
||||
s.Entities.SpawnDrop(rem.Name, int(rem.Count), s.Player.Pos[0], s.Player.Pos[1]+1, s.Player.Pos[2])
|
||||
}
|
||||
s.craftGrid[i] = item.Stack{}
|
||||
}
|
||||
s.craftOpen = false
|
||||
}
|
||||
|
||||
// tickBlockEntities 熔炉烧炼并产出(物品与背包.md §6)。
|
||||
func (s *Session) tickBlockEntities() {
|
||||
for _, be := range s.BlockEntities {
|
||||
|
||||
@@ -169,3 +169,107 @@ func mustID(t *testing.T, reg *block.Registry, name string) uint16 {
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
// fillGrid 往合成网格第 i 槽放入 1 个物品(测试辅助)。
|
||||
func fillGrid(s *Session, i int, name string) {
|
||||
s.CraftingSwap(i, item.Stack{Name: name, Count: 1})
|
||||
}
|
||||
|
||||
// TestCraftingShaped 工作台有序合成:2×2 木板 → 工作台(工作台与合成表.md §4.1/§5)。
|
||||
func TestCraftingShaped(t *testing.T) {
|
||||
s := newTestSession(t)
|
||||
s.OpenCrafting()
|
||||
if !s.CraftingOpen() {
|
||||
t.Fatal("应已打开合成界面")
|
||||
}
|
||||
// 空网格无产物
|
||||
if _, ok := s.CraftingResult(); ok {
|
||||
t.Fatal("空网格不应有产物")
|
||||
}
|
||||
// 2×2 木板
|
||||
fillGrid(s, 0, "oak_planks")
|
||||
fillGrid(s, 1, "oak_planks")
|
||||
fillGrid(s, 3, "oak_planks")
|
||||
fillGrid(s, 4, "oak_planks")
|
||||
res, ok := s.CraftingResult()
|
||||
if !ok || res.Name != "crafting_table" || res.Count != 1 {
|
||||
t.Fatalf("2×2 木板应产出工作台×1,实际 %+v ok=%v", res, ok)
|
||||
}
|
||||
// 执行合成:材料消耗、产物返回
|
||||
taken, ok := s.CraftingTakeResult()
|
||||
if !ok || taken.Name != "crafting_table" {
|
||||
t.Fatalf("TakeResult 应返回工作台,实际 %+v", taken)
|
||||
}
|
||||
for _, i := range []int{0, 1, 3, 4} {
|
||||
if !s.CraftSlot(i).Empty() {
|
||||
t.Fatalf("合成后网格槽 %d 应清空,实际 %+v", i, s.CraftSlot(i))
|
||||
}
|
||||
}
|
||||
if _, ok := s.CraftingResult(); ok {
|
||||
t.Fatal("材料耗尽后不应再有产物")
|
||||
}
|
||||
}
|
||||
|
||||
// TestCraftingShapeless 无序合成:1 原木 → 4 木板(工作台与合成表.md §4.2)。
|
||||
func TestCraftingShapeless(t *testing.T) {
|
||||
s := newTestSession(t)
|
||||
s.OpenCrafting()
|
||||
fillGrid(s, 4, "oak_log")
|
||||
res, ok := s.CraftingResult()
|
||||
if !ok || res.Name != "oak_planks" || res.Count != 4 {
|
||||
t.Fatalf("原木应产出 4 木板,实际 %+v ok=%v", res, ok)
|
||||
}
|
||||
taken, ok := s.CraftingTakeResult()
|
||||
if !ok || taken.Count != 4 {
|
||||
t.Fatalf("应产出 4 木板,实际 %+v", taken)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCraftingTools 工具配方:3 木板 + 2 木棍 → 木镐(工作台与合成表.md §4.1 多符号)。
|
||||
func TestCraftingTools(t *testing.T) {
|
||||
s := newTestSession(t)
|
||||
s.OpenCrafting()
|
||||
// 顶行 3 木板,中/下行第 2 列木棍
|
||||
for i := 0; i < 3; i++ {
|
||||
fillGrid(s, i, "oak_planks")
|
||||
}
|
||||
fillGrid(s, 4, "stick")
|
||||
fillGrid(s, 7, "stick")
|
||||
res, ok := s.CraftingResult()
|
||||
if !ok || res.Name != "wooden_pickaxe" {
|
||||
t.Fatalf("木板+木棍应产出木镐,实际 %+v ok=%v", res, ok)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCraftingCloseReturn 关闭界面:网格剩余材料退回背包,放不下掉落(工作台与合成表.md §5)。
|
||||
func TestCraftingCloseReturn(t *testing.T) {
|
||||
s := newTestSession(t)
|
||||
s.Inv.Slots[1] = item.Stack{} // 清空 1 号槽方便计数
|
||||
s.OpenCrafting()
|
||||
fillGrid(s, 0, "oak_planks")
|
||||
fillGrid(s, 0, "oak_planks") // 同槽合并 → 2 个
|
||||
fillGrid(s, 8, "stick")
|
||||
s.CloseCrafting()
|
||||
if s.CraftingOpen() {
|
||||
t.Fatal("关闭后不应仍处于打开状态")
|
||||
}
|
||||
// 网格清空
|
||||
for i := 0; i < 9; i++ {
|
||||
if !s.CraftSlot(i).Empty() {
|
||||
t.Fatalf("关闭后网格槽 %d 应清空", i)
|
||||
}
|
||||
}
|
||||
// 背包收回
|
||||
havePlanks, haveStick := 0, 0
|
||||
for _, st := range s.Inv.Slots {
|
||||
if st.Name == "oak_planks" {
|
||||
havePlanks += int(st.Count)
|
||||
}
|
||||
if st.Name == "stick" {
|
||||
haveStick += int(st.Count)
|
||||
}
|
||||
}
|
||||
if havePlanks < 2 || haveStick < 1 {
|
||||
t.Fatalf("材料应退回背包,实际 planks=%d stick=%d", havePlanks, haveStick)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user