208 lines
6.1 KiB
Go
208 lines
6.1 KiB
Go
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)
|
||
}
|
||
}
|