223 lines
7.3 KiB
Go
223 lines
7.3 KiB
Go
package world
|
|
|
|
// features.go places procedural worldgen features beyond trees: ore veins
|
|
// underground, biome-gated surface flora (flowers, dead bushes, cacti), and
|
|
// scattered rocks/boulders. Each placement is deterministic per chunk via the
|
|
// shared chunkRand PRNG, and block-state IDs are resolved at runtime through the
|
|
// embedded blocks.json table (nameToStateID), so no IDs are hardcoded.
|
|
//
|
|
// These run after terrain+surface+biomes are filled, called from decorate.
|
|
|
|
// oreSpec describes one ore type's placement envelope.
|
|
type oreSpec struct {
|
|
name string // block name, e.g. "minecraft:coal_ore"
|
|
minY int // lowest Y (absolute) for this ore
|
|
maxY int // highest Y (absolute)
|
|
attempts int // placement attempts per chunk
|
|
blobSize int // blocks in each vein
|
|
rarity float64 // 0..1 chance an attempt places its vein
|
|
}
|
|
|
|
// oreSpecs mirrors the vanilla overworld ore distribution (Y bands + relative
|
|
// frequency). Deepslate variants are omitted for simplicity; surface ore uses
|
|
// the stone-form IDs.
|
|
var oreSpecs = []oreSpec{
|
|
{"minecraft:coal_ore", MinY, MinY + 128, 20, 4, 0.5},
|
|
{"minecraft:iron_ore", MinY, MinY + 72, 14, 4, 0.4},
|
|
{"minecraft:copper_ore", MinY, MinY + 48, 6, 4, 0.3},
|
|
{"minecraft:gold_ore", MinY, MinY + 32, 4, 4, 0.25},
|
|
{"minecraft:redstone_ore", MinY, MinY + 16, 4, 4, 0.3},
|
|
{"minecraft:lapis_ore", MinY, MinY + 32, 3, 4, 0.25},
|
|
{"minecraft:diamond_ore", MinY, MinY - 16 + 16, 3, 3, 0.2}, // -64..-16-ish
|
|
{"minecraft:emerald_ore", MinY + 16, MinY + 48, 1, 1, 0.15},
|
|
}
|
|
|
|
// placeOres embeds ore veins in solid stone across the chunk. For each oreSpec
|
|
// it makes `attempts` tries; a successful attempt picks a column and a Y within
|
|
// the ore's band and writes a small blob, overwriting only stone so caves/surface
|
|
// are untouched.
|
|
func placeOres(c *Chunk, r *chunkRand) {
|
|
for _, spec := range oreSpecs {
|
|
ore := nameToStateID(spec.name, nil)
|
|
if ore == StateAir {
|
|
continue // unknown block name; skip defensively
|
|
}
|
|
for a := 0; a < spec.attempts; a++ {
|
|
if r.nextFloat() > spec.rarity {
|
|
continue
|
|
}
|
|
lx := int(r.next() % 16)
|
|
lz := int(r.next() % 16)
|
|
span := spec.maxY - spec.minY
|
|
if span <= 0 {
|
|
span = 1
|
|
}
|
|
y := spec.minY + int(r.next()%uint32(span))
|
|
placeOreBlob(c, ore, spec.blobSize, lx, y, lz, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// placeOreBlob writes a small vein of `n` ore blocks around (lx,y,lz), each
|
|
// replacing only stone. The blob is a short random walk so it reads as a vein
|
|
// rather than a cube.
|
|
func placeOreBlob(c *Chunk, ore uint16, n int, lx, y, lz int, r *chunkRand) {
|
|
x, yy, z := lx, y, lz
|
|
for i := 0; i < n; i++ {
|
|
if c.GetBlock(x, yy, z) == StateStone {
|
|
c.SetBlock(x, yy, z, ore)
|
|
}
|
|
// Step to a random orthogonal neighbour to grow the vein.
|
|
switch r.next() % 6 {
|
|
case 0:
|
|
x++
|
|
case 1:
|
|
x--
|
|
case 2:
|
|
yy++
|
|
case 3:
|
|
yy--
|
|
case 4:
|
|
z++
|
|
case 5:
|
|
z--
|
|
}
|
|
}
|
|
}
|
|
|
|
// biomeFlowers maps a biome name to the flower blocks that can spawn on its
|
|
// grassy surface. Empty/absent = no flowers. Names resolve to IDs at runtime.
|
|
var biomeFlowers = map[string][]string{
|
|
"minecraft:plains": {"minecraft:dandelion", "minecraft:poppy", "minecraft:azure_bluet", "minecraft:cornflower", "minecraft:oxeye_daisy"},
|
|
"minecraft:sunflower_plains": {"minecraft:dandelion", "minecraft:poppy", "minecraft:sunflower"},
|
|
"minecraft:forest": {"minecraft:dandelion", "minecraft:poppy", "minecraft:lily_of_the_valley"},
|
|
"minecraft:flower_forest": {"minecraft:dandelion", "minecraft:poppy", "minecraft:allium", "minecraft:azure_bluet", "minecraft:red_tulip", "minecraft:white_tulip", "minecraft:oxeye_daisy", "minecraft:cornflower"},
|
|
"minecraft:birch_forest": {"minecraft:dandelion", "minecraft:poppy"},
|
|
"minecraft:meadow": {"minecraft:dandelion", "minecraft:poppy", "minecraft:cornflower", "minecraft:allium"},
|
|
}
|
|
|
|
// placeFlora scatters biome-appropriate small plants on grassy surface columns.
|
|
// Grass columns (grass[lx][lz]==true) receive a flower with low probability; the
|
|
// per-column biome gates which flower set applies.
|
|
func placeFlora(c *Chunk, r *chunkRand, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) {
|
|
for lx := 0; lx < 16; lx++ {
|
|
for lz := 0; lz < 16; lz++ {
|
|
if !grass[lx][lz] {
|
|
continue
|
|
}
|
|
// ~4% chance per grass column — sparse, like vanilla plains.
|
|
if r.next()%100 >= 4 {
|
|
continue
|
|
}
|
|
flowers := biomeFlowers[biomeName[lx][lz]]
|
|
if len(flowers) == 0 {
|
|
continue
|
|
}
|
|
y := MinY + surfTop[lx][lz] + 1
|
|
if c.GetBlock(lx, y, lz) != StateAir {
|
|
continue
|
|
}
|
|
flower := nameToStateID(flowers[int(r.next())%len(flowers)], nil)
|
|
if flower != StateAir {
|
|
c.SetBlock(lx, y, lz, flower)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// placeDesertFeatures places cacti and dead bushes in arid biomes. Cacti are
|
|
// 1-3 tall columns; dead bushes are single blocks. Both go on sand, so they use
|
|
// the surface block check rather than the grass flag.
|
|
func placeDesertFeatures(c *Chunk, r *chunkRand, surfTop *[16][16]int, biomeName *[16][16]string) {
|
|
for lx := 1; lx < 15; lx++ {
|
|
for lz := 1; lz < 15; lz++ {
|
|
b := biomeName[lx][lz]
|
|
if b != "minecraft:desert" && b != "minecraft:desert_lakes" && b != "minecraft:badlands" {
|
|
continue
|
|
}
|
|
topY := MinY + surfTop[lx][lz]
|
|
if c.GetBlock(lx, topY, lz) != StateSand {
|
|
continue
|
|
}
|
|
// ~3% chance per eligible column.
|
|
if r.next()%100 >= 3 {
|
|
continue
|
|
}
|
|
if b == "minecraft:desert" || b == "minecraft:desert_lakes" {
|
|
placeCactus(c, lx, topY+1, lz, r)
|
|
}
|
|
if b == "minecraft:badlands" {
|
|
placeDeadBush(c, lx, topY+1, lz)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// placeCactus writes a 1-3 tall cactus column on top of the surface.
|
|
func placeCactus(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
|
cactus := nameToStateID("minecraft:cactus", nil)
|
|
if cactus == StateAir {
|
|
return
|
|
}
|
|
h := 1 + int(r.next()%3)
|
|
for i := 0; i < h; i++ {
|
|
c.SetBlock(lx, baseY+i, lz, cactus)
|
|
}
|
|
}
|
|
|
|
// placeDeadBush writes a single dead_bush on the surface.
|
|
func placeDeadBush(c *Chunk, lx, baseY, lz int) {
|
|
db := nameToStateID("minecraft:dead_bush", nil)
|
|
if db == StateAir {
|
|
return
|
|
}
|
|
c.SetBlock(lx, baseY, lz, db)
|
|
}
|
|
|
|
// placeRocks scatters small cobblestone/granite/diorite/andesite boulders on the
|
|
// surface in windswept/mountain biomes. A boulder is a 2-3 block cluster sitting
|
|
// on grass.
|
|
func placeRocks(c *Chunk, r *chunkRand, surfTop *[16][16]int, grass *[16][16]bool, biomeName *[16][16]string) {
|
|
for lx := 2; lx < 14; lx++ {
|
|
for lz := 2; lz < 14; lz++ {
|
|
if !grass[lx][lz] {
|
|
continue
|
|
}
|
|
b := biomeName[lx][lz]
|
|
if b != "minecraft:windswept_hills" && b != "minecraft:windswept_forest" &&
|
|
b != "minecraft:windswept_gravelly_hills" && b != "minecraft:stony_peaks" {
|
|
continue
|
|
}
|
|
// ~2% chance per eligible column.
|
|
if r.next()%100 >= 2 {
|
|
continue
|
|
}
|
|
placeBoulder(c, lx, MinY+surfTop[lx][lz]+1, lz, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// placeBoulder writes a small 2-3 block cluster of stone-family blocks.
|
|
func placeBoulder(c *Chunk, lx, baseY, lz int, r *chunkRand) {
|
|
rocks := []string{"minecraft:cobblestone", "minecraft:granite", "minecraft:diorite", "minecraft:andesite"}
|
|
block := nameToStateID(rocks[int(r.next())%len(rocks)], nil)
|
|
if block == StateAir {
|
|
return
|
|
}
|
|
n := 2 + int(r.next()%2)
|
|
x, yy, z := lx, baseY, lz
|
|
for i := 0; i < n; i++ {
|
|
if c.GetBlock(x, yy, z) == StateAir {
|
|
c.SetBlock(x, yy, z, block)
|
|
}
|
|
// Grow up/sideways into air.
|
|
switch r.next() % 3 {
|
|
case 0:
|
|
yy++
|
|
case 1:
|
|
z++
|
|
case 2:
|
|
x++
|
|
}
|
|
}
|
|
}
|