Files
ngzz-mc/internal/entity/entity_test.go
NianGao Dev 9e2f5a6186 fix(game): 崩溃修复(卸载区块实体冻结)+ 立面光照、掉落可见、打击感、水下效果、空手拳头、血条左/饥饿右
- 崩溃(index out of range):实体所在区块卸载后失去碰撞支撑无限下坠到
  负坐标,光查询越界 panic——WorldView 增 ChunkLoaded,未加载区块实体冻结;
  chunk 光查询加越界防御(双保险)
- 立面光照:mesh 每面取**邻格**光照(原版光照模型)——此前用方块自身光照
  (sky=0)导致悬崖/矿洞立面全黑(泥土竖向黑色根因)
- 掉落可见:生成后 1 秒拾取延迟 + 图标加大到 0.45
- 打击感:攻击命中音(block.hit)+ 实体受击白闪(uTint)+ 手持挥动动画
- 水下效果:相机入水蓝色叠加(渲染.md §8 水体)
- 空手显示肤色拳头(白羊毛 × 肤色 tint),持方块正常显示
- HUD 布局:血条左下、饥饿右下(经典布局)
2026-08-16 20:59:28 +08:00

161 lines
4.6 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)
}
}
// 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
}