feat(blockentity): 方块实体——熔炉烧炼(燃料/输入/产出)与箱子容器(含测试)

This commit is contained in:
NianGao Dev
2026-08-15 23:23:34 +08:00
parent 093b3df8ec
commit 6bca2bea74
4 changed files with 256 additions and 4 deletions

View File

@@ -0,0 +1,159 @@
// Package blockentity 方块实体:箱子容器与熔炉烧炼(物品与背包.md §6
//
// 接口约定(设计.md §6.3Tick / 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 每炉烧炼所需 tick10 秒)。
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)
}

View 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("燃料槽应接受煤炭")
}
}

View File

@@ -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 渲染器。

Binary file not shown.