2026-08-15 22:47:21 +08:00
|
|
|
|
package game
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"math"
|
2026-08-15 23:48:58 +08:00
|
|
|
|
"mc/internal/blockentity"
|
2026-08-15 22:47:21 +08:00
|
|
|
|
"path/filepath"
|
|
|
|
|
|
"testing"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"mc/internal/block"
|
|
|
|
|
|
"mc/internal/camera"
|
|
|
|
|
|
"mc/internal/inventory"
|
|
|
|
|
|
"mc/internal/item"
|
|
|
|
|
|
"mc/internal/physics"
|
|
|
|
|
|
"mc/internal/recipe"
|
|
|
|
|
|
"mc/internal/world"
|
|
|
|
|
|
"mc/internal/worldgen"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// newTestSession 构造真实世界上的游戏会话(等待区块生成)。
|
|
|
|
|
|
func newTestSession(t *testing.T) *Session {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json"))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("加载方块注册表失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
items, err := item.Load(filepath.Join("..", "..", "assets", "config", "items.json"))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("加载物品注册表失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
recipes, err := recipe.Load(filepath.Join("..", "..", "assets", "config", "recipes.json"))
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("加载配方失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
gen, err := worldgen.New(reg, 42, 0.005, 0.01, 0.1)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建生成器失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
w, err := world.New(reg, gen, 3)
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
|
t.Fatalf("创建世界失败: %v", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
t.Cleanup(w.Close)
|
|
|
|
|
|
|
|
|
|
|
|
// 等待 (0,0) 区块
|
|
|
|
|
|
deadline := time.Now().Add(15 * time.Second)
|
|
|
|
|
|
for time.Now().Before(deadline) {
|
|
|
|
|
|
w.Update(8, 64, 8, 8*time.Millisecond)
|
|
|
|
|
|
if w.Block(8, 0, 8) != block.Air {
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
time.Sleep(2 * time.Millisecond)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 找地表
|
|
|
|
|
|
var surf int32
|
|
|
|
|
|
for y := int32(255); y >= 0; y-- {
|
|
|
|
|
|
if w.Block(8, y, 8) != block.Air {
|
|
|
|
|
|
surf = y
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
p := physics.NewPlayer(8.5, float64(surf+1), 8.5)
|
|
|
|
|
|
cam := camera.New(p.Eye(), 0, -math.Pi/2, 70)
|
|
|
|
|
|
inv := inventory.New(items)
|
|
|
|
|
|
inv.Slots[0] = item.Stack{Name: "diamond_pickaxe", Count: 1} // 选中的工具
|
|
|
|
|
|
inv.Slots[1] = item.Stack{Name: "oak_planks", Count: 10}
|
2026-08-15 23:49:59 +08:00
|
|
|
|
sess := NewSession(w, reg, items, recipes, p, inv, cam)
|
|
|
|
|
|
// 测试环境禁用自然刷怪(僵尸生成依赖区块加载时序,会造成 flake)
|
|
|
|
|
|
sess.spawnTimer = 1e9
|
|
|
|
|
|
return sess
|
2026-08-15 22:47:21 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestBreakAndPlace 完整闭环:破坏地表 → 放木板(方块交互与动画.md §3、§6)。
|
|
|
|
|
|
func TestBreakAndPlace(t *testing.T) {
|
|
|
|
|
|
s := newTestSession(t)
|
|
|
|
|
|
|
|
|
|
|
|
// 破坏脚下地表的草方块(向下看)
|
|
|
|
|
|
s.BeginBreak()
|
|
|
|
|
|
if !s.breaking {
|
|
|
|
|
|
t.Fatal("应锁定破坏目标")
|
|
|
|
|
|
}
|
|
|
|
|
|
tx, ty, tz := s.BreakTarget().X, s.BreakTarget().Y, s.BreakTarget().Z
|
|
|
|
|
|
steps := 0
|
|
|
|
|
|
for s.breaking && steps < 600 {
|
|
|
|
|
|
s.Tick(1.0 / 20)
|
|
|
|
|
|
steps++
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.breaking {
|
|
|
|
|
|
t.Fatal("钻石镐 600 tick 未破坏草方块")
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.World.Block(tx, ty, tz) != block.Air {
|
|
|
|
|
|
t.Fatal("破坏后应为空气")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 22:47:54 +08:00
|
|
|
|
// 放置木板(切换到槽 1):先抬升玩家脱离坑位(否则放置位与玩家 AABB 重叠会被正确拒绝)
|
2026-08-15 22:47:21 +08:00
|
|
|
|
s.Inv.Selected = 1
|
2026-08-15 22:47:54 +08:00
|
|
|
|
s.Player.Pos[1] += 2
|
|
|
|
|
|
s.Tick(1.0 / 20) // 相机跟随
|
2026-08-15 22:47:21 +08:00
|
|
|
|
s.PlaceSelected()
|
|
|
|
|
|
if name := s.Reg.Name(s.World.Block(tx, ty, tz).ID()); name != "oak_planks" {
|
|
|
|
|
|
t.Fatalf("放置后应为橡木板,实际 %s", name)
|
|
|
|
|
|
}
|
|
|
|
|
|
if s.Inv.Slots[1].Count != 9 {
|
|
|
|
|
|
t.Fatalf("木板应剩 9,实际 %d", s.Inv.Slots[1].Count)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// TestBreakSpeed 验证工具速度公式(挖矿与矿物.md §3)。
|
|
|
|
|
|
func TestBreakSpeed(t *testing.T) {
|
|
|
|
|
|
s := newTestSession(t)
|
|
|
|
|
|
stone := s.Reg.Get(mustID(t, s.Reg, "stone"))
|
|
|
|
|
|
// 空手
|
|
|
|
|
|
if got := s.breakSpeed(stone, item.Stack{}); got < 1/1.5-0.01 || got > 1/1.5+0.01 {
|
|
|
|
|
|
t.Fatalf("空手速度期望约 1/1.5,实际 %v", got)
|
|
|
|
|
|
}
|
|
|
|
|
|
// 钻石镐(speed 8;Count 必须 ≥1 才是非空栈)
|
|
|
|
|
|
withTool := item.Stack{Name: "diamond_pickaxe", Count: 1}
|
|
|
|
|
|
if got := s.breakSpeed(stone, withTool); got < 8/1.5-0.01 || got > 8/1.5+0.01 {
|
|
|
|
|
|
t.Fatalf("钻石镐速度期望约 8/1.5,实际 %v", got)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 23:48:58 +08:00
|
|
|
|
// TestFurnaceFlow 放置熔炉 → 投料 → 烧炼产出(物品与背包.md §6 会话接线)。
|
|
|
|
|
|
func TestFurnaceFlow(t *testing.T) {
|
|
|
|
|
|
s := newTestSession(t)
|
|
|
|
|
|
// 给玩家熔炉 + 煤炭 + 铁矿石
|
|
|
|
|
|
s.Inv.Slots[1] = item.Stack{Name: "furnace", Count: 1}
|
|
|
|
|
|
s.Inv.Slots[2] = item.Stack{Name: "coal", Count: 2}
|
|
|
|
|
|
s.Inv.Slots[3] = item.Stack{Name: "iron_ore", Count: 2}
|
|
|
|
|
|
s.Inv.Selected = 1
|
|
|
|
|
|
|
|
|
|
|
|
// 放置熔炉(向下看,站在地表)
|
|
|
|
|
|
s.BeginBreak()
|
|
|
|
|
|
for s.breaking {
|
|
|
|
|
|
s.Tick(1.0 / 20)
|
|
|
|
|
|
}
|
|
|
|
|
|
s.Player.Pos[1] += 2
|
|
|
|
|
|
s.Tick(1.0 / 20)
|
|
|
|
|
|
s.PlaceSelected()
|
|
|
|
|
|
|
|
|
|
|
|
// 找到熔炉方块实体
|
|
|
|
|
|
var furnace *blockentity.Entity
|
|
|
|
|
|
for _, be := range s.BlockEntities {
|
|
|
|
|
|
if be.Kind == blockentity.KindFurnace {
|
|
|
|
|
|
furnace = be
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if furnace == nil {
|
|
|
|
|
|
t.Fatal("未创建熔炉方块实体")
|
|
|
|
|
|
}
|
|
|
|
|
|
// 投料
|
|
|
|
|
|
furnace.Insert(blockentity.SlotFuel, item.Stack{Name: "coal", Count: 1}, s.Items)
|
|
|
|
|
|
furnace.Insert(blockentity.SlotInput, item.Stack{Name: "iron_ore", Count: 1}, s.Items)
|
|
|
|
|
|
// 烧炼 200 tick(10 秒)
|
|
|
|
|
|
for i := 0; i < 300; i++ {
|
|
|
|
|
|
s.Tick(1.0 / 20)
|
|
|
|
|
|
}
|
|
|
|
|
|
if furnace.Slots[blockentity.SlotOutput].Name != "iron_ingot" {
|
|
|
|
|
|
t.Fatalf("输出应为铁锭,实际 %+v", furnace.Slots[blockentity.SlotOutput])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-08-15 22:47:21 +08:00
|
|
|
|
// mustID 取方块 ID(测试辅助)。
|
|
|
|
|
|
func mustID(t *testing.T, reg *block.Registry, name string) uint16 {
|
|
|
|
|
|
t.Helper()
|
|
|
|
|
|
id, ok := reg.ID(name)
|
|
|
|
|
|
if !ok {
|
|
|
|
|
|
t.Fatalf("方块 %q 未注册", name)
|
|
|
|
|
|
}
|
|
|
|
|
|
return id
|
|
|
|
|
|
}
|