Add structures (ores, flora, desert features, rocks) and wire up generation
This commit is contained in:
parent
65a7445a78
commit
7e285e8068
3 changed files with 840 additions and 2 deletions
223
internal/world/features.go
Normal file
223
internal/world/features.go
Normal file
|
|
@ -0,0 +1,223 @@
|
|||
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++
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue