RegionIO/internal/world/geodes.go

242 lines
8.7 KiB
Go

package world
import (
"math"
"strings"
"regionio/internal/worldgen"
)
const geodesStage = 2
type geodePoint struct {
x, y, z int
offset int
}
// placeScheduledGeodes replays the stage-2 amethyst geode feature. The layer
// calculation follows GeodeFeature.place: sampled distance points are combined
// with vanilla normal-noise perturbation, then the nearest layer threshold
// selects filling, inner, middle, or outer material.
func (r *decorationRegion) placeScheduledGeodes(seed int64) error {
set, err := worldgen.LoadFeatureSet()
if err != nil {
return err
}
if err := r.ensureSourceNeighborhood(); err != nil {
return err
}
schedule, err := set.FeatureSchedule(possibleBiomeOrder(), r.sourceBiomes(), geodesStage)
if err != nil {
return err
}
random, decorationSeed := worldgen.DecorationRandom(seed, int(r.sourceX), int(r.sourceZ))
origin := worldgen.FeaturePosition{X: int(r.sourceX) << 4, Y: MinY, Z: int(r.sourceZ) << 4}
for _, scheduled := range schedule {
placed, ok := set.Placed[scheduled.Name]
if !ok {
continue
}
configured, ok := set.Configured[placed.Feature]
if !ok || configured.Type != "minecraft:geode" {
continue
}
config, err := set.Geode(placed.Feature)
if err != nil {
return err
}
random.SetFeatureSeed(decorationSeed, scheduled.Index, geodesStage)
context := r.placementContext(func(position worldgen.FeaturePosition) bool {
return r.biomeAllowsFeature(set, scheduled.Name, geodesStage, position)
})
if err := set.ForEachPlacementPosition(scheduled.Name, random, origin, context, func(position worldgen.FeaturePosition) error {
r.placeGeode(random, seed, position, config, set)
return nil
}); err != nil {
return err
}
}
return nil
}
func (r *decorationRegion) placeGeode(random worldgen.RandomSource, seed int64, origin worldgen.FeaturePosition, config worldgen.GeodeFeatureConfig, set *worldgen.FeatureSet) bool {
distributionPoints := sampleGeodeInt(random, config.DistributionMin, config.DistributionMax)
pointScale := float64(distributionPoints) / float64(config.OuterWallMax)
fillingThreshold := 1 / math.Sqrt(config.FillingLayer)
innerThreshold := 1 / math.Sqrt(config.InnerLayer+pointScale)
middleThreshold := 1 / math.Sqrt(config.MiddleLayer+pointScale)
outerThreshold := 1 / math.Sqrt(config.OuterLayer+pointScale)
crackSize := config.BaseCrackSize + random.NextDouble()/2
if distributionPoints > 3 {
crackSize += pointScale
}
crackThreshold := 1 / math.Sqrt(crackSize)
generateCrack := random.NextFloat() < float32(config.CrackChance)
points := make([]geodePoint, 0, distributionPoints)
invalid := 0
invalidBlocks := geodeTagIDs(set, config.InvalidBlocksTag)
for i := 0; i < distributionPoints; i++ {
point := geodePoint{
x: origin.X + sampleGeodeInt(random, config.OuterWallMin, config.OuterWallMax),
y: origin.Y + sampleGeodeInt(random, config.OuterWallMin, config.OuterWallMax),
z: origin.Z + sampleGeodeInt(random, config.OuterWallMin, config.OuterWallMax),
offset: sampleGeodeInt(random, config.PointOffsetMin, config.PointOffsetMax),
}
state := r.getBlock(point.x, point.y, point.z)
if state == StateAir || invalidBlocks[state] {
invalid++
if invalid > config.InvalidBlocksThreshold {
return false
}
}
points = append(points, point)
}
crackPoints := geodeCrackPoints(origin, random, distributionPoints, generateCrack)
noise := worldgen.NewNormalNoise(worldgen.NewLegacy(seed), -4, []float64{1})
cannotReplace := geodeTagIDs(set, config.CannotReplaceTag)
placed := false
var potentialPlacements [][3]int
for z := origin.Z + config.MinGenOffset; z <= origin.Z+config.MaxGenOffset; z++ {
for y := origin.Y + config.MinGenOffset; y <= origin.Y+config.MaxGenOffset; y++ {
for x := origin.X + config.MinGenOffset; x <= origin.X+config.MaxGenOffset; x++ {
perturbation := noise.GetValue(float64(x), float64(y), float64(z)) * config.NoiseMultiplier
innerDistance := geodeDistance(x, y, z, points, perturbation)
if innerDistance < outerThreshold {
continue
}
current := r.getBlock(x, y, z)
if cannotReplace[current] {
continue
}
crackDistance := 0.0
if generateCrack {
crackDistance = geodeDistanceWithOffset(x, y, z, crackPoints, config.CrackPointOffset, perturbation)
}
var state uint16
switch {
case generateCrack && crackDistance >= crackThreshold && innerDistance < fillingThreshold:
state = StateAir
case innerDistance >= fillingThreshold:
state, _ = nameToStateID(config.Filling.Name, config.Filling.Properties)
case innerDistance >= innerThreshold:
alternate := random.NextFloat() < float32(config.UseAlternateLayerChance)
provider := config.Inner
if alternate && config.AlternateInner.Name != "" {
provider = config.AlternateInner
}
state, _ = nameToStateID(provider.Name, provider.Properties)
if (!config.PlacementsRequireAlternate || alternate) &&
random.NextFloat() < float32(config.UsePotentialPlacementsChance) {
potentialPlacements = append(potentialPlacements, [3]int{x, y, z})
}
case innerDistance >= middleThreshold:
state, _ = nameToStateID(config.Middle.Name, config.Middle.Properties)
case innerDistance >= outerThreshold:
state, _ = nameToStateID(config.Outer.Name, config.Outer.Properties)
default:
continue
}
if r.setBlock(x, y, z, state) {
placed = true
}
}
}
}
r.placeGeodeInnerPlacements(random, potentialPlacements, config, cannotReplace)
return placed
}
func (r *decorationRegion) placeGeodeInnerPlacements(random worldgen.RandomSource, positions [][3]int, config worldgen.GeodeFeatureConfig, cannotReplace map[uint16]bool) {
if len(config.InnerPlacements) == 0 {
return
}
directions := [...]struct {
dx, dy, dz int
name string
}{
{0, -1, 0, "down"}, {0, 1, 0, "up"}, {0, 0, -1, "north"},
{0, 0, 1, "south"}, {-1, 0, 0, "west"}, {1, 0, 0, "east"},
}
for _, position := range positions {
placement := config.InnerPlacements[int(random.NextIntN(int32(len(config.InnerPlacements))))]
for _, direction := range directions {
x, y, z := position[0]+direction.dx, position[1]+direction.dy, position[2]+direction.dz
current := r.getBlock(x, y, z)
if current != StateAir && !isWaterState(current) {
continue
}
props := make(map[string]string, len(placement.Properties))
for key, value := range placement.Properties {
props[key] = value
}
props["facing"] = direction.name
props["waterlogged"] = "false"
if isWaterState(current) {
props["waterlogged"] = "true"
}
state, ok := nameToStateID(placement.Name, props)
if ok && !cannotReplace[current] {
if r.setBlock(x, y, z, state) {
break
}
}
}
}
}
func sampleGeodeInt(random worldgen.RandomSource, min, max int) int {
if max <= min {
return min
}
return min + int(random.NextIntN(int32(max-min+1)))
}
func geodeTagIDs(set *worldgen.FeatureSet, tag string) map[uint16]bool {
if strings.HasPrefix(tag, "#") {
tag = tag[1:]
}
ids := make(map[uint16]bool)
for _, name := range flattenBlockTag(set, tag, nil) {
if id, ok := nameToStateID(name, nil); ok {
ids[id] = true
}
}
return ids
}
func geodeDistance(x, y, z int, points []geodePoint, perturbation float64) float64 {
distance := 0.0
for _, point := range points {
dx, dy, dz := x-point.x, y-point.y, z-point.z
distance += 1/math.Sqrt(float64(dx*dx+dy*dy+dz*dz+point.offset)) + perturbation
}
return distance
}
func geodeDistanceWithOffset(x, y, z int, points []geodePoint, offset int, perturbation float64) float64 {
distance := 0.0
for _, point := range points {
dx, dy, dz := x-point.x, y-point.y, z-point.z
distance += 1/math.Sqrt(float64(dx*dx+dy*dy+dz*dz+offset)) + perturbation
}
return distance
}
func geodeCrackPoints(origin worldgen.FeaturePosition, random worldgen.RandomSource, distributionPoints int, enabled bool) []geodePoint {
if !enabled {
return nil
}
offset := distributionPoints*2 + 1
switch random.NextIntN(4) {
case 0:
return []geodePoint{{x: origin.X + offset, y: origin.Y + 7, z: origin.Z}, {x: origin.X + offset, y: origin.Y + 5, z: origin.Z}, {x: origin.X + offset, y: origin.Y + 1, z: origin.Z}}
case 1:
return []geodePoint{{x: origin.X, y: origin.Y + 7, z: origin.Z + offset}, {x: origin.X, y: origin.Y + 5, z: origin.Z + offset}, {x: origin.X, y: origin.Y + 1, z: origin.Z + offset}}
case 2:
return []geodePoint{{x: origin.X + offset, y: origin.Y + 7, z: origin.Z + offset}, {x: origin.X + offset, y: origin.Y + 5, z: origin.Z + offset}, {x: origin.X + offset, y: origin.Y + 1, z: origin.Z + offset}}
default:
return []geodePoint{{x: origin.X, y: origin.Y + 7, z: origin.Z}, {x: origin.X, y: origin.Y + 5, z: origin.Z}, {x: origin.X, y: origin.Y + 1, z: origin.Z}}
}
}