Files
ngzz-mc/internal/light/light_test.go

208 lines
6.1 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 light
import (
"path/filepath"
"testing"
"mc/internal/block"
"mc/internal/chunk"
"mc/internal/coord"
)
// chunkWorld 测试用 World 实现:多区块映射(验证跨区块光照溢出,光照.md §6)。
type chunkWorld struct {
reg *block.Registry
chunks map[[2]int32]*chunk.Chunk
}
// newChunkWorld 构造含注册表的多区块世界。
func newChunkWorld(t *testing.T) *chunkWorld {
t.Helper()
reg, err := block.Load(filepath.Join("..", "..", "assets", "config", "blocks.json"))
if err != nil {
t.Fatalf("加载注册表失败: %v", err)
}
return &chunkWorld{reg: reg, chunks: map[[2]int32]*chunk.Chunk{}}
}
// addChunk 添加一个区块(初始全空气)。
func (w *chunkWorld) addChunk(cx, cz int32) *chunk.Chunk {
c := chunk.New(cx, cz)
w.chunks[[2]int32{cx, cz}] = c
return c
}
// chunkAt 按世界坐标定位区块;未加载返回 nil。
func (w *chunkWorld) chunkAt(x, z int32) *chunk.Chunk {
return w.chunks[[2]int32{coord.ChunkCoord(x), coord.ChunkCoord(z)}]
}
// Opaque 遮光判定:未加载区块与世界边界视为阻隔(光照.md §6 延迟更新语义)。
func (w *chunkWorld) Opaque(x, y, z int32) bool {
if y < 0 || y >= chunk.Height {
return true
}
c := w.chunkAt(x, z)
if c == nil {
return true
}
return w.reg.IsOpaque(c.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))))
}
// BlockLight 读方块光。
func (w *chunkWorld) BlockLight(x, y, z int32) uint8 {
c := w.chunkAt(x, z)
if c == nil {
return 0
}
return c.BlockLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))
}
// SetBlockLight 写方块光。
func (w *chunkWorld) SetBlockLight(x, y, z int32, v uint8) {
if c := w.chunkAt(x, z); c != nil {
c.SetBlockLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), v)
}
}
// SourceLight 方块自身发光等级。
func (w *chunkWorld) SourceLight(x, y, z int32) uint8 {
c := w.chunkAt(x, z)
if c == nil {
return 0
}
return w.reg.Light(c.Block(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z))))
}
// SkyLight 读天空光。
func (w *chunkWorld) SkyLight(x, y, z int32) uint8 {
c := w.chunkAt(x, z)
if c == nil {
return 15
}
return c.SkyLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)))
}
// SetSkyLight 写天空光。
func (w *chunkWorld) SetSkyLight(x, y, z int32, v uint8) {
if c := w.chunkAt(x, z); c != nil {
c.SetSkyLight(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), v)
}
}
// placeTorch 放置火把并初始化其方块光(等效游戏内放火把流程)。
func (w *chunkWorld) placeTorch(x, y, z int32) Pos {
torch, _ := w.reg.ID("torch")
c := w.chunkAt(x, z)
c.Fill(int(coord.LocalCoord(x)), int(y), int(coord.LocalCoord(z)), block.NewState(torch, 0))
p := Pos{X: x, Y: y, Z: z}
w.SetBlockLight(x, y, z, w.SourceLight(x, y, z)) // 14
return p
}
// TestSkyFill 验证天空光按列灌入与衰减(光照.md §3)。
// 语义:光从列顶 15 开始,每穿过一个方块减 1,空气不减;最低 0。
func TestSkyFill(t *testing.T) {
w := newChunkWorld(t)
c := w.addChunk(0, 0)
stone, _ := w.reg.ID("stone")
// 造一个 y=30..70 的石柱(表面 y=70)
for y := 30; y <= 70; y++ {
c.Fill(8, y, 8, block.NewState(stone, 0))
}
SkyFill(w, 0, 15, 0, 15)
if got := w.SkyLight(8, 80, 8); got != 15 {
t.Fatalf("表面上方天空光期望 15,实际 %d", got)
}
if got := w.SkyLight(8, 70, 8); got != 15 {
t.Fatalf("表面方块天空光期望 15,实际 %d", got)
}
if got := w.SkyLight(8, 69, 8); got != 14 {
t.Fatalf("表面下方 1 格期望 14,实际 %d", got)
}
if got := w.SkyLight(8, 60, 8); got != 5 {
t.Fatalf("表面下方 10 格期望 5,实际 %d", got)
}
if got := w.SkyLight(8, 55, 8); got != 0 {
t.Fatalf("表面下方 15 格期望 0,实际 %d", got)
}
if got := w.SkyLight(8, 40, 8); got != 0 {
t.Fatalf("更深处方块光期望 0,实际 %d", got)
}
}
// TestPropagate 验证方块光 BFS 衰减与遮挡(光照.md §4)。
func TestPropagate(t *testing.T) {
w := newChunkWorld(t)
c := w.addChunk(0, 0)
seed := w.placeTorch(8, 65, 8)
// 遮挡墙:火把东侧放石头
stone, _ := w.reg.ID("stone")
c.Fill(9, 65, 8, block.NewState(stone, 0))
Propagate(w, []Pos{seed})
if got := w.BlockLight(8, 64, 8); got != 13 {
t.Fatalf("火把下方期望 13,实际 %d", got)
}
if got := w.BlockLight(8, 66, 8); got != 13 {
t.Fatalf("火把上方期望 13,实际 %d", got)
}
if got := w.BlockLight(9, 65, 8); got != 0 {
t.Fatalf("石头遮挡处期望 0,实际 %d", got)
}
if got := w.BlockLight(7, 65, 8); got != 13 {
t.Fatalf("火把西侧期望 13,实际 %d", got)
}
if got := w.BlockLight(8, 51, 8); got != 0 {
t.Fatalf("14 格外期望 0,实际 %d", got)
}
}
// TestCrossChunk 验证火把贴墙跨区块溢出(光照.md §6 踩坑)。
func TestCrossChunk(t *testing.T) {
w := newChunkWorld(t)
w.addChunk(0, 0)
w.addChunk(1, 0)
seed := w.placeTorch(15, 65, 8) // 区块 (0,0) 最东侧
Propagate(w, []Pos{seed})
// (16,65,8) 属于区块 (1,0) 局部 (0,65,8)
if got := w.BlockLight(16, 65, 8); got != 13 {
t.Fatalf("跨区块溢出期望 13,实际 %d", got)
}
}
// TestRemoveLight 验证光源移除重算:另一光源的光仍保留(光照.md §5)。
func TestRemoveLight(t *testing.T) {
w := newChunkWorld(t)
w.addChunk(0, 0)
w.addChunk(1, 0)
a := w.placeTorch(8, 65, 8)
b := w.placeTorch(20, 65, 8) // 第二个火把(区块 (1,0))
Propagate(w, []Pos{a, b})
// 先移除火把 A 的方块(变回空气),再做 relight
w.chunkAt(8, 8).Fill(8, 65, 8, block.Air)
RemoveLight(w, []Pos{a}, func(minX, minY, minZ, maxX, maxY, maxZ int32) []Pos {
var seeds []Pos
for x := minX; x <= maxX; x++ {
for y := minY; y <= maxY; y++ {
for z := minZ; z <= maxZ; z++ {
if w.SourceLight(x, y, z) > 0 {
seeds = append(seeds, Pos{x, y, z})
}
}
}
}
return seeds
})
// (9,65,8) 距 B 11 格 → 光 14-11 = 3
if got := w.BlockLight(9, 65, 8); got != 3 {
t.Fatalf("移除 A 后 (9,65,8) 期望 3(B 的光),实际 %d", got)
}
// A 原位置距 B 12 格 → 2
if got := w.BlockLight(8, 65, 8); got != 2 {
t.Fatalf("A 原位置期望 2,实际 %d", got)
}
}