Files
ngzz-mc/internal/entity/entity_test.go

158 lines
4.4 KiB
Go
Raw Normal View History

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