216 lines
6.2 KiB
Go
216 lines
6.2 KiB
Go
package entity
|
||
|
||
import (
|
||
"path/filepath"
|
||
"testing"
|
||
"time"
|
||
|
||
"mc/internal/block"
|
||
"mc/internal/world"
|
||
"mc/internal/worldgen"
|
||
)
|
||
|
||
// flatWorld 测试世界:y<63 固体,其余空气;无光。
|
||
type flatWorld struct{}
|
||
|
||
func (flatWorld) Solid(x, y, z int32) bool { return y < 63 }
|
||
func (flatWorld) Block(x, y, z int32) block.State { return block.Air }
|
||
func (flatWorld) SkyLight(x, y, z int32) uint8 { return 15 }
|
||
func (flatWorld) BlockLight(x, y, z int32) uint8 { return 0 }
|
||
func (flatWorld) ChunkLoaded(x, z int32) bool { return true }
|
||
|
||
// TestZombieChase 僵尸追玩家(僵尸与敌对生物.md §2)。
|
||
func TestZombieChase(t *testing.T) {
|
||
m := NewManager()
|
||
z := m.Spawn(KindZombie, 0.5, 63, 0.5)
|
||
w := flatWorld{}
|
||
for i := 0; i < 200; i++ {
|
||
m.Tick(0.05, w, [3]float64{10, 63, 0.5})
|
||
}
|
||
// 僵尸应向玩家移动(方块光 0 ≤ 7 满足感知条件;天空光 15 → 燃烧掉血但不影响移动)
|
||
if z.Pos[0] < 1.0 {
|
||
t.Fatalf("僵尸未向玩家移动: %+v", z.Pos)
|
||
}
|
||
}
|
||
|
||
// TestZombieBurn 白天暴露燃烧致死(僵尸与敌对生物.md §4 逐 tick 伤害)。
|
||
func TestZombieBurn(t *testing.T) {
|
||
m := NewManager()
|
||
z := m.Spawn(KindZombie, 0.5, 63, 0.5)
|
||
w := flatWorld{} // sky=15
|
||
deadline := time.Now().Add(2 * time.Second)
|
||
for !z.Dead && time.Now().Before(deadline) {
|
||
m.Tick(0.05, w, [3]float64{100, 63, 100})
|
||
}
|
||
if !z.Dead {
|
||
t.Fatal("暴露僵尸应在白天被烧死")
|
||
}
|
||
if z.Health > 0 {
|
||
t.Fatalf("死亡实体血量应 ≤0: %v", z.Health)
|
||
}
|
||
}
|
||
|
||
// TestAnimalWander 动物游荡(确定性方向,实体系统.md §3 限频决策)。
|
||
func TestAnimalWander(t *testing.T) {
|
||
m := NewManager()
|
||
a := m.Spawn(KindAnimal, 0.5, 63, 0.5)
|
||
w := flatWorld{}
|
||
for i := 0; i < 100; i++ {
|
||
m.Tick(0.05, w, [3]float64{100, 63, 100})
|
||
}
|
||
if a.Pos[1] < 63 {
|
||
t.Fatalf("动物掉到地面以下: %+v", a.Pos)
|
||
}
|
||
}
|
||
|
||
// TestDropMerge 掉落物合并(实体系统.md §5)。
|
||
func TestDropMerge(t *testing.T) {
|
||
dropPickupDelay = 0 // 测试跳过拾取延迟
|
||
defer func() { dropPickupDelay = time.Second }()
|
||
m := NewManager()
|
||
m.SpawnDrop("cobblestone", 5, 0.5, 64, 0.5)
|
||
m.SpawnDrop("cobblestone", 5, 0.6, 64, 0.5) // 距离 <1 → 合并
|
||
m.SpawnDrop("cobblestone", 5, 10, 64, 10) // 距离远 → 新实体
|
||
cnt := 0
|
||
for _, e := range m.List() {
|
||
if e.Kind == KindItemDrop {
|
||
cnt++
|
||
}
|
||
}
|
||
if cnt != 2 {
|
||
t.Fatalf("掉落物实体数期望 2,实际 %d", cnt)
|
||
}
|
||
// 拾取
|
||
drops := m.PickupNearby(0.5, 64, 0.5)
|
||
if len(drops) != 1 || drops[0].Count != 10 {
|
||
t.Fatalf("拾取期望 1 组 10 个,实际 %+v", drops)
|
||
}
|
||
}
|
||
|
||
// TestDropStackCap 单堆叠上限 9999:合并到上限后,多余部分生成新堆(实体系统.md §5)。
|
||
func TestDropStackCap(t *testing.T) {
|
||
m := NewManager()
|
||
m.SpawnDrop("cobblestone", 9999, 0.5, 64, 0.5)
|
||
m.SpawnDrop("cobblestone", 5, 0.6, 64, 0.5) // 9999+5 超上限 → 不合并,生成新堆
|
||
cnt, total := 0, 0
|
||
for _, e := range m.List() {
|
||
if e.Kind != KindItemDrop {
|
||
continue
|
||
}
|
||
d, ok := e.Data.(*Drop)
|
||
if !ok {
|
||
t.Fatalf("掉落物数据缺失: %+v", e.Data)
|
||
}
|
||
cnt++
|
||
total += d.Count
|
||
if d.Count > dropStackMax {
|
||
t.Fatalf("单堆超上限: %d > %d", d.Count, dropStackMax)
|
||
}
|
||
}
|
||
if cnt != 2 || total != 10004 {
|
||
t.Fatalf("期望 2 堆共 10004,实际 %d 堆共 %d", cnt, total)
|
||
}
|
||
}
|
||
|
||
// TestBlueprint 房屋蓝图确定性 + 建造执行顺序(村民系统.md §6)。
|
||
func TestBlueprint(t *testing.T) {
|
||
a := Blueprint([3]int32{10, 64, 10}, 42)
|
||
b := Blueprint([3]int32{10, 64, 10}, 42)
|
||
if len(a) != len(b) {
|
||
t.Fatalf("蓝图确定性失败: %d != %d", len(a), len(b))
|
||
}
|
||
for i := range a {
|
||
if a[i] != b[i] {
|
||
t.Fatalf("蓝图第 %d 项不一致", i)
|
||
}
|
||
}
|
||
// 顺序约束:地基(Order 0)先于墙与屋顶
|
||
firstNonFoundation := -1
|
||
for i, task := range a {
|
||
if task.Order != 0 {
|
||
firstNonFoundation = i
|
||
break
|
||
}
|
||
}
|
||
if firstNonFoundation <= 0 {
|
||
t.Fatal("蓝图缺少地基")
|
||
}
|
||
|
||
// 建造执行:每 tick 2 块,全部放置成功
|
||
v := &Villager{Profession: 1, BuildQueue: append([]BuildTask{}, a...)}
|
||
placed := 0
|
||
place := func(x, y, z int32, id uint16, meta uint8) bool { placed++; return true }
|
||
for len(v.BuildQueue) > 0 {
|
||
v.BuildTick(place)
|
||
}
|
||
if placed != len(a) {
|
||
t.Fatalf("放置数量期望 %d,实际 %d", len(a), placed)
|
||
}
|
||
}
|
||
|
||
// TestEntityWorldIntegration 实体跑在真实世界上(世界接入 smoke)。
|
||
func TestEntityWorldIntegration(t *testing.T) {
|
||
reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json"))
|
||
if err != nil {
|
||
t.Fatalf("加载注册表失败: %v", err)
|
||
}
|
||
gen, err := worldgen.New(reg, 1, 0.005, 0.01, 1.0) // 无洞穴
|
||
if err != nil {
|
||
t.Fatalf("创建生成器失败: %v", err)
|
||
}
|
||
w, err := world.New(reg, gen, 2)
|
||
if err != nil {
|
||
t.Fatalf("创建世界失败: %v", err)
|
||
}
|
||
defer w.Close()
|
||
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
|
||
}
|
||
}
|
||
m := NewManager()
|
||
m.Spawn(KindZombie, 8.5, float64(surf+1), 8.5)
|
||
m.Tick(0.05, w, [3]float64{12, float64(surf + 1), 8.5}) // 世界实现了 WorldView(Solid/Block/Sky/BlockLight)
|
||
}
|
||
|
||
// frozenWorld 全部区块未加载的世界视图(实体冻结测试)。
|
||
type frozenWorld struct{ flatWorld }
|
||
|
||
func (frozenWorld) ChunkLoaded(x, z int32) bool { return false }
|
||
|
||
// TestEntityFreeze 未加载区块实体冻结:不 tick、不下坠(崩溃根因回归测试)。
|
||
func TestEntityFreeze(t *testing.T) {
|
||
m := NewManager()
|
||
v := m.Spawn(KindVillager, 1000, 100, 1000)
|
||
v.Data = &Villager{}
|
||
w := frozenWorld{}
|
||
for i := 0; i < 10; i++ {
|
||
m.Tick(0.05, w, [3]float64{0, 64, 0})
|
||
}
|
||
if v.Pos[1] != 100 {
|
||
t.Fatalf("未加载区块实体应冻结,实际 y=%v", v.Pos[1])
|
||
}
|
||
if v.Pos[0] != 1000 {
|
||
t.Fatalf("未加载区块实体应冻结,实际 x=%v", v.Pos[0])
|
||
}
|
||
}
|
||
|
||
func TestFaceVelYaw(t *testing.T) {
|
||
// 速度 +Z → yaw=π,模型 −Z 指向世界 +Z(渲染.md §5.3)
|
||
y := faceVelYaw(0, 1)
|
||
if y < 3.14 || y > 3.15 {
|
||
t.Fatalf("朝 +Z 时期望 π,实际 %v", y)
|
||
}
|
||
}
|