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 } // 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) { 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) } } // 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) }