feat(blockentity): 方块实体——熔炉烧炼(燃料/输入/产出)与箱子容器(含测试)
This commit is contained in:
159
internal/blockentity/blockentity.go
Normal file
159
internal/blockentity/blockentity.go
Normal file
@@ -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)
|
||||
}
|
||||
93
internal/blockentity/blockentity_test.go
Normal file
93
internal/blockentity/blockentity_test.go
Normal file
@@ -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("燃料槽应接受煤炭")
|
||||
}
|
||||
}
|
||||
@@ -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 渲染器。
|
||||
|
||||
BIN
tools/zig.zip
BIN
tools/zig.zip
Binary file not shown.
Reference in New Issue
Block a user