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