Files
ngzz-mc/internal/entity/entity_test.go
2026-09-19 14:21:08 +08:00

216 lines
6.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}) // 世界实现了 WorldViewSolid/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)
}
}