feat(worldgen): 村庄结构生成——网格撒点+群系白名单+水井+跨区块房屋(含测试)
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
130
internal/worldgen/village.go
Normal file
130
internal/worldgen/village.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user