package game import ( "math" "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} return NewSession(w, reg, items, recipes, p, inv, cam) } // 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("破坏后应为空气") } // 放置木板(切换到槽 1):先抬升玩家脱离坑位(否则放置位与玩家 AABB 重叠会被正确拒绝) s.Inv.Selected = 1 s.Player.Pos[1] += 2 s.Tick(1.0 / 20) // 相机跟随 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) } } // 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 }