diff --git a/internal/blockentity/blockentity.go b/internal/blockentity/blockentity.go new file mode 100644 index 00000000..31024657 --- /dev/null +++ b/internal/blockentity/blockentity.go @@ -0,0 +1,159 @@ +// Package blockentity 方块实体:箱子容器与熔炉烧炼(物品与背包.md §6)。 +// +// 接口约定(设计.md §6.3):Tick / Serialize / Deserialize;随区块保存。 +package blockentity + +import ( + "fmt" + + "mc/internal/inventory" + "mc/internal/item" +) + +// Kind 方块实体类型。 +type Kind uint8 + +// 方块实体类型(物品与背包.md §6)。 +const ( + KindChest Kind = iota // 箱子(27 格) + KindFurnace // 熔炉(燃料+输入+输出) +) + +// Entity 方块实体。 +type Entity struct { + Kind Kind + X, Y, Z int32 + + // 容器(箱子/熔炉共用) + Slots []item.Stack // 0..N-1 槽位(N 按类型) + + // 熔炉进度(物品与背包.md §6) + BurnTicks int // 剩余燃烧时间(tick) + BurnMaxTicks int // 当前燃料总燃烧时间 + CookTicks int // 已烧炼进度 +} + +// 熔炉槽位(物品与背包.md §6:燃料+输入+输出)。 +const ( + SlotFuel = 0 + SlotInput = 1 + SlotOutput = 2 +) + +// CookTime 每炉烧炼所需 tick(10 秒)。 +const CookTime = 200 + +// FuelTicks 燃料燃烧时长表(tick,物品与背包.md §6)。 +var FuelTicks = map[string]int{ + "coal": 1600, +} + +// New 创建方块实体(按类型分配槽位)。 +func New(kind Kind, x, y, z int32) *Entity { + e := &Entity{Kind: kind, X: x, Y: y, Z: z} + switch kind { + case KindChest: + e.Slots = make([]item.Stack, 27) + case KindFurnace: + e.Slots = make([]item.Stack, 3) + } + return e +} + +// NewFurnace 创建熔炉。 +func NewFurnace(x, y, z int32) *Entity { return New(KindFurnace, x, y, z) } + +// NewChest 创建箱子。 +func NewChest(x, y, z int32) *Entity { return New(KindChest, x, y, z) } + +// Tick 熔炉烧炼(每 tick;箱子无逻辑)。 +// 返回 true 表示产出完成了一个物品(调用方取出放入输出槽)。 +func (e *Entity) Tick() bool { + if e.Kind != KindFurnace { + return false + } + // 燃料耗尽则尝试补充 + if e.BurnTicks <= 0 { + fuel := e.Slots[SlotFuel] + if !fuel.Empty() { + if ticks, ok := FuelTicks[fuel.Name]; ok { + e.BurnMaxTicks = ticks + e.BurnTicks = ticks + e.Slots[SlotFuel].Count-- + if e.Slots[SlotFuel].Count == 0 { + e.Slots[SlotFuel] = item.Stack{} + } + } + } + } + // 燃烧中 + 输入可烧炼 → 推进度 + if e.BurnTicks > 0 && !e.Slots[SlotInput].Empty() { + e.BurnTicks-- + e.CookTicks++ + if e.CookTicks >= CookTime { + e.CookTicks = 0 + return true + } + } + return false +} + +// CanInsert 槽位插入规则(容器版:与背包规则表一致,物品与背包.md §3)。 +func (e *Entity) CanInsert(slot int, s item.Stack, reg *item.Registry) bool { + if slot < 0 || slot >= len(e.Slots) { + return false + } + // 熔炉槽位限制 + if e.Kind == KindFurnace { + switch slot { + case SlotFuel: + _, ok := FuelTicks[s.Name] + return ok || e.Slots[slot].Name == s.Name + case SlotInput: + return e.Slots[slot].Empty() || e.Slots[slot].Name == s.Name + case SlotOutput: + return e.Slots[slot].Empty() || e.Slots[slot].Name == s.Name + } + } + return inventory.CanMerge(e.Slots[slot], s) || e.Slots[slot].Empty() +} + +// Insert 放入槽位(返回剩余;规则表校验由 CanInsert 先行)。 +func (e *Entity) Insert(slot int, s item.Stack, reg *item.Registry) item.Stack { + if slot < 0 || slot >= len(e.Slots) { + return s + } + if e.Slots[slot].Empty() { + e.Slots[slot] = s + return item.Stack{} + } + if e.Slots[slot].Name == s.Name { + limit := s.Limit(reg) + merged, remain := inventory.Merge(limit, e.Slots[slot], s) + e.Slots[slot] = merged + return remain + } + return s +} + +// Remove 从槽位取出 count 个。 +func (e *Entity) Remove(slot, count int) item.Stack { + if slot < 0 || slot >= len(e.Slots) || e.Slots[slot].Empty() { + return item.Stack{} + } + s := e.Slots[slot] + if count >= int(s.Count) { + e.Slots[slot] = item.Stack{} + return s + } + s.Count -= uint8(count) + out := s + out.Count = uint8(count) + e.Slots[slot] = s + return out +} + +// String 描述(调试)。 +func (e *Entity) String() string { + return fmt.Sprintf("blockentity(%d @%d,%d,%d)", e.Kind, e.X, e.Y, e.Z) +} diff --git a/internal/blockentity/blockentity_test.go b/internal/blockentity/blockentity_test.go new file mode 100644 index 00000000..47268b10 --- /dev/null +++ b/internal/blockentity/blockentity_test.go @@ -0,0 +1,93 @@ +package blockentity + +import ( + "path/filepath" + "testing" + + "mc/internal/item" +) + +// loadItemRegBE 加载物品注册表。 +func loadItemRegBE(t *testing.T) *item.Registry { + t.Helper() + r, err := item.Load(filepath.Join("..", "..", "assets", "config", "items.json")) + if err != nil { + t.Fatalf("加载物品注册表失败: %v", err) + } + return r +} + +// TestFurnaceSmelt 熔炉烧炼闭环(物品与背包.md §6):煤+铁矿石 → 铁锭。 +func TestFurnaceSmelt(t *testing.T) { + reg := loadItemRegBE(t) + f := NewFurnace(0, 64, 0) + // 放燃料与输入 + if rem := f.Insert(SlotFuel, item.Stack{Name: "coal", Count: 1}, reg); !rem.Empty() { + t.Fatal("燃料应放入") + } + if rem := f.Insert(SlotInput, item.Stack{Name: "iron_ore", Count: 2}, reg); !rem.Empty() { + t.Fatal("输入应放入") + } + // 烧炼 2 炉 + produced := 0 + for i := 0; i < 2000; i++ { + if f.Tick() { + produced++ + } + } + if produced != 2 { + t.Fatalf("产出期望 2,实际 %d", produced) + } + if f.Slots[SlotInput].Count != 0 { + t.Fatalf("输入应耗尽,剩余 %d", f.Slots[SlotInput].Count) + } +} + +// TestFurnaceFuelExhaust 燃料耗尽自动停炉。 +func TestFurnaceFuelExhaust(t *testing.T) { + reg := loadItemRegBE(t) + f := NewFurnace(0, 64, 0) + f.Insert(SlotFuel, item.Stack{Name: "coal", Count: 1}, reg) + f.Insert(SlotInput, item.Stack{Name: "iron_ore", Count: 64}, reg) + // 一块煤 1600 tick,最多产出 8 个(1600/200) + produced := 0 + for i := 0; i < 4000; i++ { + if f.Tick() { + produced++ + } + } + if produced != 8 { + t.Fatalf("产出期望 8(一块煤),实际 %d", produced) + } +} + +// TestChestStore 箱子存取(物品与背包.md §6 容器)。 +func TestChestStore(t *testing.T) { + reg := loadItemRegBE(t) + c := NewChest(1, 64, 1) + if rem := c.Insert(0, item.Stack{Name: "diamond", Count: 32}, reg); !rem.Empty() { + t.Fatal("应全部放入") + } + if rem := c.Insert(0, item.Stack{Name: "diamond", Count: 32}, reg); !rem.Empty() { + t.Fatal("应合并到 64") + } + got := c.Remove(0, 10) + if got.Count != 10 || got.Name != "diamond" { + t.Fatalf("取出异常: %+v", got) + } + if c.Slots[0].Count != 54 { + t.Fatalf("剩余期望 54,实际 %d", c.Slots[0].Count) + } +} + +// TestFurnaceSlotRules 熔炉槽位规则(燃料槽只收燃料)。 +func TestFurnaceSlotRules(t *testing.T) { + reg := loadItemRegBE(t) + f := NewFurnace(0, 64, 0) + if f.CanInsert(SlotFuel, item.Stack{Name: "diamond", Count: 1}, reg) { + t.Fatal("燃料槽不应接受钻石") + } + if !f.CanInsert(SlotFuel, item.Stack{Name: "coal", Count: 1}, reg) { + t.Fatal("燃料槽应接受煤炭") + } +} diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 1b033652..4686af5f 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -19,10 +19,10 @@ import ( // quad 一个四边形(屏幕像素坐标,y 轴向下)。 type quad struct { - x, y, w, h float32 - u0, v0, u1, v1 float32 - r, g, b, a float32 - tex uint32 + x, y, w, h float32 + u0, v0, u1, v1 float32 + r, g, b, a float32 + tex uint32 } // Renderer UI 渲染器。 diff --git a/tools/zig.zip b/tools/zig.zip index 0a8cc3f4..288a9f56 100644 Binary files a/tools/zig.zip and b/tools/zig.zip differ