feat(worldgen): 村庄结构生成——网格撒点+群系白名单+水井+跨区块房屋(含测试)

This commit is contained in:
NianGao Dev
2026-08-15 23:27:50 +08:00
parent f6f89be32f
commit 7cf12ce751
1016 changed files with 444142 additions and 0 deletions

View File

@@ -46,6 +46,7 @@ type Generator struct {
// 常用方块状态(启动时从注册表解析一次)
stone, grass, dirt, bedrock, sand, water, snowB, log, leaves block.State
coal, iron, gold, diamond, redstone block.State
planks, cobble, torch block.State // 村庄建筑用
}
// New 创建生成器;必须的方块缺注册时报错。
@@ -105,6 +106,15 @@ func New(reg *block.Registry, seed int64, biomeScale, heightScale, caveDensity f
if g.redstone, err = must(reg, "redstone_ore"); err != nil {
return nil, err
}
if g.planks, err = must(reg, "oak_planks"); err != nil {
return nil, err
}
if g.cobble, err = must(reg, "cobblestone"); err != nil {
return nil, err
}
if g.torch, err = must(reg, "torch"); err != nil {
return nil, err
}
return g, nil
}
@@ -237,6 +247,8 @@ func (g *Generator) Generate(cx, cz int32) *chunk.Chunk {
}
// 树后处理 pass(世界生态.md §3.1:边缘留白避免跨区块,树冠不出界)
g.placeTrees(c)
// 村庄结构 pass(村民系统.md §2:网格撒点 + 水井 + 房屋)
g.placeVillage(c, cx, cz)
return c
}

View File

@@ -84,6 +84,48 @@ func TestTerrainStructure(t *testing.T) {
}
}
// TestVillage 村庄生成:存在村庄中心的区块应有木板/圆石建筑(村民系统.md §2)。
func TestVillage(t *testing.T) {
reg := loadReg(t)
planks, _ := reg.ID("oak_planks")
cobble, _ := reg.ID("cobblestone")
g, err := New(reg, 424242, 0.005, 0.01, 0.1)
if err != nil {
t.Fatalf("创建生成器失败: %v", err)
}
// 扫描网格点找村庄
found := 0
for gx := int32(-2); gx <= 2; gx++ {
for gz := int32(-2); gz <= 2; gz++ {
vx, vz := g.villageCenter(gx, gz)
b := g.biomeAt(vx, vz)
if b != biomePlains && b != biomeForest {
continue
}
cx, cz := floorDiv32(vx, 16), floorDiv32(vz, 16)
ch := g.Generate(cx, cz)
plankCnt, cobbleCnt := 0, 0
snap := ch.Snapshot()
for i := 0; i < 16*16*256; i++ {
switch snap[i].ID() {
case planks:
plankCnt++
case cobble:
cobbleCnt++
}
}
// 建筑跨区块:中心区块至少包含部分房屋/水井
if plankCnt < 20 || cobbleCnt == 0 {
t.Fatalf("村庄区块 (%d,%d) 建筑过少: 木板 %d 圆石 %d", cx, cz, plankCnt, cobbleCnt)
}
found++
}
}
if found == 0 {
t.Fatal("5×5 网格点内未发现村庄")
}
}
// TestOres 验证矿物只出现在规定深度区间(挖矿与矿物.md §2:y=100 只允许石头/煤矿)。
func TestOres(t *testing.T) {
reg := loadReg(t)

View File

@@ -0,0 +1,130 @@
// 村庄生成:结构 pass(村民系统.md §2、世界生态.md §3)。
//
// 规则:村庄按 384 格网格撒点(噪声抖动选位,仅平原/森林群系),
// 每个村庄水井 + 3 间房屋;建筑按绝对坐标定义,跨区块落位(世界生成.md §4)。
package worldgen
import (
"mc/internal/block"
"mc/internal/chunk"
)
// 村庄参数(村民系统.md §2)。
const (
villageSpacing = 384 // 村庄网格间距(格)
villageJitter = 96 // 中心抖动半径
villageRadius = 32 // 村庄建筑最大半径(区块跨越判定)
)
// villageCenter 返回网格点 (gx, gz) 派生出的村庄中心绝对坐标。
func (g *Generator) villageCenter(gx, gz int32) (int32, int32) {
jx := int32(g.ore.Eval2(float64(gx)*7.7+91, float64(gz)*3.1+17) * villageJitter)
jz := int32(g.ore.Eval2(float64(gx)*5.3+57, float64(gz)*8.9+29) * villageJitter)
return gx*villageSpacing + villageSpacing/2 + jx, gz*villageSpacing + villageSpacing/2 + jz
}
// placeVillage 生成跨越本区块的村庄建筑(世界生成.md §4 跨区块安全)。
func (g *Generator) placeVillage(c *chunk.Chunk, cx, cz int32) {
minX, maxX := cx*16, cx*16+15
minZ, maxZ := cz*16, cz*16+15
// 附近的网格点(±1 个网格内即可覆盖)
for dgx := int32(-1); dgx <= 1; dgx++ {
for dgz := int32(-1); dgz <= 1; dgz++ {
gx := floorDiv32(cx*16, villageSpacing) + dgx
gz := floorDiv32(cz*16, villageSpacing) + dgz
vx, vz := g.villageCenter(gx, gz)
// 村庄建筑范围与本区块相交?
if vx < minX-villageRadius || vx > maxX+villageRadius || vz < minZ-villageRadius || vz > maxZ+villageRadius {
continue
}
// 群系白名单(沙漠/雪原不建村)
b := g.biomeAt(vx, vz)
if b != biomePlains && b != biomeForest {
continue
}
// 水井 + 房屋(绝对坐标,逐块过滤到当前区块)
h := int32(g.heightAt(vx, vz))
g.placeWellAbs(c, cx, cz, vx, h, vz)
offsets := [][2]int32{{-12, -8}, {8, -10}, {-4, 12}}
for i, o := range offsets {
hx, hz := vx+o[0], vz+o[1]
hh := int32(g.heightAt(hx, hz))
g.placeHouseAbs(c, cx, cz, hx, hh, hz, uint64(gx)*73856093^uint64(gz)*19349663^uint64(i))
}
}
}
}
// inChunk 判定绝对坐标是否属于区块 (cx, cz)。
func inChunk(x, z, cx, cz int32) bool {
return floorDiv32(x, 16) == cx && floorDiv32(z, 16) == cz
}
// setAbs 绝对坐标写入(跨区块过滤,世界生成.md §4)。
func (g *Generator) setAbs(c *chunk.Chunk, cx, cz, x, y, z int32, s block.State) {
if y < 0 || y >= chunk.Height {
return
}
if !inChunk(x, z, cx, cz) {
return
}
c.Fill(int(x-cx*16), int(y), int(z-cz*16), s)
}
// placeWellAbs 水井:圆石环 + 中心水(3×3,村民系统.md §2 布局)。
func (g *Generator) placeWellAbs(c *chunk.Chunk, cx, cz, wx, h, wz int32) {
for dx := int32(-1); dx <= 1; dx++ {
for dz := int32(-1); dz <= 1; dz++ {
if dx == 0 && dz == 0 {
g.setAbs(c, cx, cz, wx+dx, h+1, wz+dz, g.water) // 井水
} else {
g.setAbs(c, cx, cz, wx+dx, h+1, wz+dz, g.cobble) // 井沿
}
}
}
}
// placeHouseAbs 村民房屋:木板地基 + 墙高 3(门洞)+ 平顶外沿 + 四角火把。
// 布局与 entity.Blueprint 一致(村民系统.md §6.2)。
func (g *Generator) placeHouseAbs(c *chunk.Chunk, cx, cz, hx, h, hz int32, seed uint64) {
w, d := int32(5+seed%2), int32(5) // 5–6 × 5
for x := int32(0); x < w; x++ {
for z := int32(0); z < d; z++ {
g.setAbs(c, cx, cz, hx+x, h+1, hz+z, g.planks)
}
}
// 墙(2 层,南侧门洞)
for y := int32(2); y <= 3; y++ {
for x := int32(0); x < w; x++ {
for z := int32(0); z < d; z++ {
edge := x == 0 || x == w-1 || z == 0 || z == d-1
if !edge {
continue
}
if y == 2 && z == d-1 && x == w/2 {
continue // 门洞
}
g.setAbs(c, cx, cz, hx+x, h+y, hz+z, g.planks)
}
}
}
// 屋顶外沿
for x := int32(-1); x <= w; x++ {
for z := int32(-1); z <= d; z++ {
g.setAbs(c, cx, cz, hx+x, h+4, hz+z, g.planks)
}
}
// 四角火把
for _, cc := range [4][2]int32{{0, 0}, {w - 1, 0}, {0, d - 1}, {w - 1, d - 1}} {
g.setAbs(c, cx, cz, hx+cc[0], h+3, hz+cc[1], g.torch)
}
}
// floorDiv32 负安全除法(villageCenter 网格定位)。
func floorDiv32(a, b int32) int32 {
q := a / b
if a%b < 0 {
q--
}
return q
}