Files
2026-09-25 17:02:24 +03:00

868 lines
30 KiB
Go

package overlay
import (
"fmt"
"math"
"sort"
"salty/terrain/internal/field"
"salty/terrain/internal/noise"
)
// Filling an overlay in from a baked world, so an author starts from something rather than from nothing.
//
// The annotation layer is hand-painted and it starts blank, which is the right default and a bad starting
// point. Where a forest can grow, where a town would actually stand and what a road between two towns would
// follow are all *consequences of the terrain* - of slope, of where the rivers run, of how far the sea is -
// and the terrain is the one thing an author cannot see while painting, because the solve has not happened
// yet when they are painting classes and the heightmap is 29 million pixels when it has. So the generator
// reads a finished bake and proposes the marks the terrain implies. The author then moves them.
//
// Four rules, and they are the whole design:
//
// - **A painted pixel is never touched.** Generation fills blank pixels only. An author who has drawn the
// capital exactly where they want it can regenerate everything else around it as often as they like, and
// the two halves compose rather than competing. This is what makes the feature safe to re-run.
// - **It is opt-in per mark.** A mark generates only if it carries a `generate` block. A legend written
// before this existed produces exactly the blank sheet it always did, and a mark the author wants to own
// completely simply says nothing.
// - **It runs at the template's resolution**, which is the overlay's own. Generating on the 8 m geology
// grid and downsampling would smear a road across two colours, and the classifier reads exact colours -
// a blended pixel is dropped or becomes a different mark. Nothing here antialiases anything, for the
// same reason the studio's brush does not.
// - **It proposes, it does not decide.** These are starting points. The numbers below are chosen to put
// something plausible on the sheet, not to be a settlement model.
//
// What it is emphatically not: a simulation. There is no economy, no history and no climate here - the Go
// generator has no climate model at all - so "where would a city be" is answered with drainage, slope and
// distance to the sea, which is the part of the question the terrain can actually answer.
// Generate kinds. A mark's `generate.kind` picks one.
const (
// GenForest fills ground that could carry trees: shallow enough, below the treeline, and broken up by a
// noise field so it reads as woodland rather than as a contour band.
GenForest = "forest"
// GenSettlement places discs at scored sites - rivers, flat ground, the coast - with a minimum spacing,
// largest tier first. Several marks may use it; they share one spacing rule, so a village never lands
// inside a city.
GenSettlement = "settlement"
// GenRoad joins the settlements that were placed, along least-cost paths over the terrain. Water is
// impassable, so roads never swim: an island group comes out as one road network per island.
GenRoad = "road"
// GenCoast bands the waterline. It is the one kind whose mark usually carries `coast_jitter`, which is
// the only overlay property any pass reads.
GenCoast = "coast"
)
// GenSpec is a mark's `generate` block: what to put where, and the few numbers worth varying. Every zero
// field takes a default that is derived from the world being generated rather than from a constant, because
// a treeline in metres means nothing until you know how high the land got.
type GenSpec struct {
Kind string `json:"kind"`
// MaxSlopeDeg is the steepest ground this mark will be put on. Forests stop at cliffs, towns stand on
// flat ground, and roads climb but grudgingly.
MaxSlopeDeg float64 `json:"max_slope_deg"`
// MinHeightM and MaxHeightM bound the elevation band. MaxHeightM zero means "derive a treeline from the
// land's own height distribution", which is the only honest default on a world whose relief is unknown
// until it is baked.
MinHeightM float64 `json:"min_height_m"`
MaxHeightM float64 `json:"max_height_m"`
// Cover is roughly the fraction of the eligible ground this mark should take, for area kinds. It is a
// quantile of the noise field rather than a count, so it means the same thing on any size of world.
Cover float64 `json:"cover"`
// WavelengthKm is how big the patches are, for area kinds.
WavelengthKm float64 `json:"wavelength_km"`
// Count is how many of this mark to place, for settlements.
Count int `json:"count"`
// MinSpacingKm is how far apart settlements must stand. Shared across every settlement mark, taken from
// the largest that sets one.
MinSpacingKm float64 `json:"min_spacing_km"`
// RadiusM is how big the painted blob is. Zero derives one from the mark's own min_area_px, so the blob
// this writes is never one the feature reducer would then discard as a speck.
RadiusM float64 `json:"radius_m"`
// WidthM is how wide a band or a road is painted. For a road the legend's own width_m is used when this
// is zero, because that is the same number said once.
WidthM float64 `json:"width_m"`
// CoastKm is how far inland a coast band reaches, and how close to the sea a settlement wants to be for
// its coastal bonus.
CoastKm float64 `json:"coast_km"`
// OnlyClasses and NotClasses restrict a mark to, or bar it from, ground painted with named classes from
// the *class* legend.
//
// They exist because height and slope cannot tell an ice cap from a meadow. The first run of this
// generator grew woodland across both polar caps: the caps are flat, they are below the treeline, and
// nothing the terrain knows says otherwise - the only thing that does is the colour the author painted
// there. A class name that is not in the legend is an error rather than an empty filter, because a
// misspelt exclusion is a forest on an ice cap that nobody notices.
OnlyClasses []string `json:"only_classes"`
NotClasses []string `json:"not_classes"`
// Resolved forms of the two lists above, as class indices. Filled in by Generate.
onlyIdx map[int]bool
notIdx map[int]bool
}
// resolveClasses turns the class names into indices against the class legend that was actually loaded.
func (g *GenSpec) resolveClasses(markName string, names []string) error {
find := func(list []string) (map[int]bool, error) {
if len(list) == 0 {
return nil, nil
}
if len(names) == 0 {
return nil, fmt.Errorf("mark %q names classes, but no class legend was handed to the generator",
markName)
}
out := map[int]bool{}
for _, want := range list {
found := -1
for i, n := range names {
if n == want {
found = i
break
}
}
if found < 0 {
return nil, fmt.Errorf("mark %q names the class %q, which is not in the class legend",
markName, want)
}
out[found] = true
}
return out, nil
}
var err error
if g.onlyIdx, err = find(g.OnlyClasses); err != nil {
return err
}
g.notIdx, err = find(g.NotClasses)
return err
}
func (g *GenSpec) validate(markName string) error {
switch g.Kind {
case GenForest, GenSettlement, GenRoad, GenCoast:
default:
return fmt.Errorf("mark %q: generate.kind %q is not one of %q, %q, %q, %q",
markName, g.Kind, GenForest, GenSettlement, GenRoad, GenCoast)
}
if g.Cover < 0 || g.Cover > 1 {
return fmt.Errorf("mark %q: generate.cover is %v, outside 0..1", markName, g.Cover)
}
if g.Count < 0 {
return fmt.Errorf("mark %q: generate.count is %d", markName, g.Count)
}
return nil
}
// GenInputs is the baked world the marks are read off, at the overlay's own resolution.
type GenInputs struct {
W, H int
CellM float64 // metres per overlay pixel
// HeightM is the surface in metres and Sea is which cells are under water, both over the painted rows
// only - the polar pad is scaffolding and has no marks on it.
HeightM []float32
Sea []bool
// FlowM2 is drainage area in square metres. Nil is allowed: rivers then contribute nothing to a
// settlement's score, which is worth saying out loud rather than silently scoring zero everywhere.
FlowM2 []float32
// ClassAt is the class legend's index per cell, and ClassNames the names those indices mean. Both are
// optional together: without them only_classes and not_classes cannot be honoured, and asking for one is
// then an error rather than a filter that quietly does nothing.
ClassAt []uint8
ClassNames []string
Seed int64
// Existing is the overlay as it stands. Its painted pixels are preserved exactly and generation fills
// around them. Nil is a blank sheet.
Existing *Raster
}
// GenReport is what was placed, for the run summary.
type GenReport struct {
Marks []GenMarkReport
Kept int // pixels that were already painted and were left alone
Painted int // pixels this generation filled
TreelineM float64
Settlement []Placed
}
// GenMarkReport is one mark's share of a generation.
type GenMarkReport struct {
Name string
Kind string
Cells int
Pieces int // settlements placed, or roads traced
// Wanted is how many were asked for, when that is a number the legend gave. Reported separately from
// Pieces so a run that could not fit them all says so: the spacing and the amount of flat ground are
// what ration settlements, and an author who asked for forty and got eighteen needs to be told, not left
// to count the dots.
Wanted int
}
// Placed is one settlement, kept so the roads can be run between them and so the summary can say where they
// went.
type Placed struct {
Mark int // raster index
X, Y int
Score float64
RadPx int
Region int // which connected landmass, so roads never try to cross open water
}
// wantedFor is how many of a mark the legend asked for, or zero when it is not a counted kind.
func wantedFor(m *Mark) int {
if m.Generate == nil {
return 0
}
return m.Generate.Count
}
// Generate fills the blank parts of an overlay from a baked world.
func (l *Legend) Generate(in GenInputs) (*Raster, GenReport, error) {
var rep GenReport
if in.W <= 0 || in.H <= 0 {
return nil, rep, fmt.Errorf("overlay generation needs a size, got %dx%d", in.W, in.H)
}
if len(in.HeightM) != in.W*in.H || len(in.Sea) != in.W*in.H {
return nil, rep, fmt.Errorf("overlay generation: height and sea must be %d cells", in.W*in.H)
}
for i := range l.Marks {
if g := l.Marks[i].Generate; g != nil {
if err := g.validate(l.Marks[i].Name); err != nil {
return nil, rep, err
}
if err := g.resolveClasses(l.Marks[i].Name, in.ClassNames); err != nil {
return nil, rep, err
}
}
}
if in.ClassAt != nil && len(in.ClassAt) != in.W*in.H {
return nil, rep, fmt.Errorf("overlay generation: the class raster is %d cells and the grid is %d",
len(in.ClassAt), in.W*in.H)
}
out := &Raster{W: in.W, H: in.H, Mark: make([]uint8, in.W*in.H)}
// What was on the sheet before this run, kept separately from what is on it now. The distinction is the
// whole layering rule: a hand-painted pixel is never touched, while a mark this run has just put down
// may be built over by a later one - a road through generated woodland is a road, and a town on it is a
// town. Without the two being different, whichever kind painted first would block every kind after it,
// which is exactly what happened on the first run: a coastal band claimed a fifth of the world and the
// settlements and roads placed inside it painted nothing at all.
protectedPx := make([]bool, in.W*in.H)
if in.Existing != nil {
if in.Existing.W != in.W || in.Existing.H != in.H {
return nil, rep, fmt.Errorf("the overlay on disk is %dx%d and the generator is working at %dx%d",
in.Existing.W, in.Existing.H, in.W, in.H)
}
copy(out.Mark, in.Existing.Mark)
for i, m := range out.Mark {
if m != Blank {
protectedPx[i] = true
rep.Kept++
}
}
}
d := newGenData(in)
d.protected = protectedPx
rep.TreelineM = d.treelineM
// Painting order is coarse to fine: the coastal band, then woodland, then the roads across it, then the
// settlements the roads run between.
//
// Two area marks never overwrite each other - the first in legend order claims the overlap, because
// deciding that a forest beats a coastline or the reverse is an authoring judgement and not one a
// generator should make silently. Roads and settlements do overwrite generated areas, because they are
// the thing being placed and the area is the ground it stands on.
order := []string{GenCoast, GenForest, GenRoad, GenSettlement}
byKind := map[string][]int{}
for i := range l.Marks {
if g := l.Marks[i].Generate; g != nil {
byKind[g.Kind] = append(byKind[g.Kind], i)
}
}
// Settlements are placed before the roads are drawn even though they are painted after, because the
// roads are the paths between them and cannot be traced until they exist.
if len(byKind[GenSettlement]) > 0 {
rep.Settlement = l.placeSettlements(byKind[GenSettlement], d)
}
for _, kind := range order {
for _, mi := range byKind[kind] {
m := &l.Marks[mi]
idx := uint8(mi + 1)
var cells, pieces int
switch kind {
case GenCoast:
cells = l.paintCoastBand(m, d, out, idx)
case GenForest:
cells = l.paintForest(m, d, out, idx)
case GenRoad:
cells, pieces = l.paintRoads(m, d, out, idx, rep.Settlement)
case GenSettlement:
cells, pieces = paintSettlements(d, out, idx, rep.Settlement, l.MinArea(m))
}
rep.Marks = append(rep.Marks, GenMarkReport{
Name: m.Name, Kind: kind, Cells: cells, Pieces: pieces, Wanted: wantedFor(m),
})
rep.Painted += cells
}
}
return out, rep, nil
}
// genData is everything derived once and shared by the kinds: slope, distance to the sea, the treeline and
// the landmass labels.
type genData struct {
in GenInputs
// protected marks the pixels that were already painted when this run started. Nothing here may write to
// one, whatever kind it is.
protected []bool
slopeDeg []float32
coastKm []float32 // distance to the nearest sea cell, kilometres; land only
landID []int32 // connected landmass, -1 at sea
treelineM float64
landMaxM float64
flowLog []float32 // log10 of drainage area, normalised 0..1 over the land
}
func newGenData(in GenInputs) *genData {
d := &genData{in: in}
d.slopeDeg = slopeField(in.HeightM, in.W, in.H, in.CellM)
d.coastKm = coastDistanceKm(in.Sea, in.W, in.H, in.CellM)
d.landID = labelLandmasses(in.Sea, in.W, in.H)
// The treeline is a quantile of the land's own heights rather than a number in metres, because a metre
// means nothing until the world is baked: the same legend over a 47 m plain and a 2800 m range has to
// put trees on both. Two thirds of the way up leaves the summits bare on a world that has summits and
// takes almost nothing off a world that does not - which is correct, a lowland has no treeline.
var hs []float32
for i, s := range in.Sea {
if !s {
hs = append(hs, in.HeightM[i])
}
}
if len(hs) > 0 {
sort.Slice(hs, func(a, b int) bool { return hs[a] < hs[b] })
d.landMaxM = float64(hs[len(hs)-1])
d.treelineM = float64(hs[int(float64(len(hs)-1)*0.94)])
}
if in.FlowM2 != nil && len(in.FlowM2) == in.W*in.H {
d.flowLog = make([]float32, in.W*in.H)
cell := in.CellM * in.CellM
// Normalised against a trunk river's catchment rather than the map's largest, so one enormous basin
// cannot flatten every other river to nothing.
hi := math.Log10(math.Max(cell*4, 5e7))
lo := math.Log10(math.Max(cell, 1))
for i, f := range in.FlowM2 {
if in.Sea[i] || f <= 0 {
continue
}
t := (math.Log10(float64(f)) - lo) / (hi - lo)
d.flowLog[i] = float32(math.Max(0, math.Min(1, t)))
}
}
return d
}
// slopeField is the surface gradient in degrees, central differences, X wrapped because the world is a
// cylinder and Y clamped because it is not a sphere.
func slopeField(h []float32, w, hgt int, cellM float64) []float32 {
out := make([]float32, w*hgt)
field.Rows(hgt, func(y0, y1 int) {
for y := y0; y < y1; y++ {
ym := y - 1
if ym < 0 {
ym = 0
}
yp := y + 1
if yp >= hgt {
yp = hgt - 1
}
for x := 0; x < w; x++ {
xm := (x - 1 + w) % w
xp := (x + 1) % w
dzdx := float64(h[y*w+xp]-h[y*w+xm]) / (2 * cellM)
dzdy := float64(h[yp*w+x]-h[ym*w+x]) / (2 * cellM)
out[y*w+x] = float32(math.Atan(math.Hypot(dzdx, dzdy)) * 180 / math.Pi)
}
}
})
return out
}
// coastDistanceKm is how far each land cell is from the sea, by a multi-source breadth-first walk over the
// eight neighbours with X wrapped. Hop distance rather than Euclidean: it is a score input, and a BFS over
// 29 million cells costs one pass where a distance transform costs several.
func coastDistanceKm(sea []bool, w, h int, cellM float64) []float32 {
out := make([]float32, w*h)
for i := range out {
out[i] = -1
}
queue := make([]int32, 0, w*8)
for i, s := range sea {
if s {
continue
}
x, y := i%w, i/w
if touchesSea(sea, w, h, x, y) {
out[i] = 0
queue = append(queue, int32(i))
}
}
hop := float32(cellM / 1000)
for head := 0; head < len(queue); head++ {
c := int(queue[head])
cx, cy := c%w, c/w
d := out[c] + hop
for _, o := range neighbours8 {
nx := (cx + o[0] + w) % w
ny := cy + o[1]
if ny < 0 || ny >= h {
continue
}
n := ny*w + nx
if sea[n] || out[n] >= 0 {
continue
}
out[n] = d
queue = append(queue, int32(n))
}
}
return out
}
var neighbours8 = [8][2]int{{-1, -1}, {0, -1}, {1, -1}, {-1, 0}, {1, 0}, {-1, 1}, {0, 1}, {1, 1}}
func touchesSea(sea []bool, w, h, x, y int) bool {
for _, o := range neighbours8 {
nx := (x + o[0] + w) % w
ny := y + o[1]
if ny < 0 || ny >= h {
continue
}
if sea[ny*w+nx] {
return true
}
}
return false
}
// labelLandmasses numbers the connected land components, X wrapped, so a landmass across the seam is one
// landmass. Roads are built per component, which is what stops them crossing open water.
func labelLandmasses(sea []bool, w, h int) []int32 {
out := make([]int32, w*h)
for i := range out {
out[i] = -1
}
var stack []int32
next := int32(0)
for start := range sea {
if sea[start] || out[start] >= 0 {
continue
}
id := next
next++
out[start] = id
stack = append(stack[:0], int32(start))
for len(stack) > 0 {
c := int(stack[len(stack)-1])
stack = stack[:len(stack)-1]
cx, cy := c%w, c/w
for _, o := range neighbours8 {
nx := (cx + o[0] + w) % w
ny := cy + o[1]
if ny < 0 || ny >= h {
continue
}
n := ny*w + nx
if sea[n] || out[n] >= 0 {
continue
}
out[n] = id
stack = append(stack, int32(n))
}
}
}
return out
}
// eligible is the shared test every kind starts from: on land, not too steep, inside the height band.
func (d *genData) eligible(i int, g *GenSpec, maxDefault float64) bool {
if d.in.Sea[i] {
return false
}
maxSlope := g.MaxSlopeDeg
if maxSlope <= 0 {
maxSlope = maxDefault
}
if float64(d.slopeDeg[i]) > maxSlope {
return false
}
if !d.classAllows(i, g) {
return false
}
hm := float64(d.in.HeightM[i])
if hm < g.MinHeightM {
return false
}
top := g.MaxHeightM
if top <= 0 {
top = d.treelineM
}
return top <= 0 || hm <= top
}
// classAllows applies a mark's only_classes and not_classes to one cell.
func (d *genData) classAllows(i int, g *GenSpec) bool {
if d.in.ClassAt == nil || (g.onlyIdx == nil && g.notIdx == nil) {
return true
}
c := int(d.in.ClassAt[i])
if g.notIdx != nil && g.notIdx[c] {
return false
}
if g.onlyIdx != nil && !g.onlyIdx[c] {
return false
}
return true
}
// paintForest fills eligible ground where a noise field stands above a quantile, so woodland has an outline
// rather than a contour edge. The field is indexed by world position (cross-cutting rule 1), so the same
// ground gets the same trees whatever else changes.
func (l *Legend) paintForest(m *Mark, d *genData, out *Raster, idx uint8) int {
g := m.Generate
cover := g.Cover
if cover <= 0 {
cover = 0.45
}
wavelengthKm := g.WavelengthKm
if wavelengthKm <= 0 {
wavelengthKm = 6
}
in := d.in
circM := float64(in.W) * in.CellM
u, v := noise.WorldUV(in.W, in.H, in.CellM, 0, 0, math.Max(circM, 1))
cells := math.Max(1, math.Round(circM/(wavelengthKm*1000)))
f := noise.FBMAt(u, v, noise.NewSource(in.Seed, srcOverlayForest),
noise.Params{BaseCells: int(cells), Octaves: 4, Gain: 0.5})
// The threshold is a quantile of the noise *over the eligible ground*, so `cover` means what it says on a
// world whose eligible ground is a thin strip as much as on one where it is everything.
// The quantile is taken over the ground this mark can actually take - eligible and not already claimed -
// so `cover` means the same fraction whether or not another area mark got there first.
var vals []float32
for i := range out.Mark {
if out.Mark[i] == Blank && d.eligible(i, g, 25) {
vals = append(vals, f.Data[i])
}
}
if len(vals) == 0 {
return 0
}
sort.Slice(vals, func(a, b int) bool { return vals[a] < vals[b] })
cut := vals[int(float64(len(vals)-1)*(1-cover))]
n := 0
for i := range out.Mark {
if out.Mark[i] != Blank || f.Data[i] < cut || !d.eligible(i, g, 25) {
continue
}
out.Mark[i] = idx
n++
}
return n
}
// paintCoastBand marks a strip inland of the waterline. Its usual purpose is to carry `coast_jitter`, so it
// deliberately follows the shore rather than any other feature.
func (l *Legend) paintCoastBand(m *Mark, d *genData, out *Raster, idx uint8) int {
g := m.Generate
reachKm := g.CoastKm
if reachKm <= 0 {
if g.WidthM > 0 {
reachKm = g.WidthM / 1000
} else {
reachKm = 1.5
}
}
n := 0
for i := range out.Mark {
if out.Mark[i] != Blank || d.in.Sea[i] {
continue
}
if c := d.coastKm[i]; c >= 0 && float64(c) <= reachKm {
out.Mark[i] = idx
n++
}
}
return n
}
// placeSettlements scores the land and takes the best sites, largest tier first, with one spacing rule
// shared by every settlement mark so a village never lands inside a city.
//
// The score is the part of "where would a town be" that terrain can answer: fresh water, flat ground, and
// the sea. Everything else about a settlement - trade, history, who won a war - is the author's, which is
// why these are proposals in an editable sheet rather than a placement the bake bakes in.
func (l *Legend) placeSettlements(marks []int, d *genData) []Placed {
in := d.in
spacingKm := 0.0
for _, mi := range marks {
if s := l.Marks[mi].Generate.MinSpacingKm; s > spacingKm {
spacingKm = s
}
}
if spacingKm <= 0 {
spacingKm = 4
}
spacingPx := math.Max(2, spacingKm*1000/in.CellM)
// Tiers in the order the legend lists them, which is how an author already writes them: city, town,
// village. The first listed takes the best sites.
type tier struct {
mi int
g *GenSpec
radPx int
}
var tiers []tier
for _, mi := range marks {
g := l.Marks[mi].Generate
radM := g.RadiusM
if radM <= 0 {
// Big enough that the feature reducer will not drop it as a speck. The margin is generous on
// purpose: a disc loses area wherever it meets ground that is already painted, and a settlement
// that came out just under its own min_area_px would be placed, reported, and then silently
// dropped by the feature pass - which is what happened to a city on the first real run. 1.6
// linear is 2.6x the area, so it survives losing more than half of itself.
minArea := float64(l.MinArea(&l.Marks[mi]))
radM = math.Sqrt(minArea/math.Pi) * in.CellM * 1.6
}
radPx := int(math.Max(1, math.Round(radM/in.CellM)))
tiers = append(tiers, tier{mi: mi, g: g, radPx: radPx})
}
// Candidates are taken on a stride rather than from every cell: two sites a quarter of the spacing apart
// are the same site, and sorting 29 million scores to throw away all but fifty is work for nothing.
stride := int(math.Max(1, math.Floor(spacingPx/4)))
type cand struct {
i int
score float64
}
var cands []cand
for y := 0; y < in.H; y += stride {
for x := 0; x < in.W; x += stride {
i := y*in.W + x
s := d.settlementScore(i)
if s > 0 {
// The seed picks among the plausible sites; the terrain decides which sites are plausible at
// all. Without this the score is a pure function of the ground, so every press of the
// studio's generate button proposes exactly the same towns and a re-roll re-rolls nothing.
// A third either way reshuffles the ranking among comparable ground while still leaving a
// river mouth on a plain beating a hillside.
s *= 1 + settlementJitter*(hash01(uint64(i), uint64(in.Seed))-0.5)
cands = append(cands, cand{i: i, score: s})
}
}
}
// Sorted by score, ties broken by index so the result does not depend on the sort's stability.
sort.Slice(cands, func(a, b int) bool {
if cands[a].score != cands[b].score {
return cands[a].score > cands[b].score
}
return cands[a].i < cands[b].i
})
var placed []Placed
taken := make([][2]int, 0, 64)
sp2 := spacingPx * spacingPx
farEnough := func(x, y int) bool {
for _, t := range taken {
dx := float64(wrapDelta(x-t[0], in.W))
dy := float64(y - t[1])
if dx*dx+dy*dy < sp2 {
return false
}
}
return true
}
for _, t := range tiers {
want := t.g.Count
if want <= 0 {
continue
}
got := 0
maxSlope := t.g.MaxSlopeDeg
if maxSlope <= 0 {
maxSlope = 8
}
for _, c := range cands {
if got >= want {
break
}
if float64(d.slopeDeg[c.i]) > maxSlope {
continue
}
x, y := c.i%in.W, c.i/in.W
if !farEnough(x, y) {
continue
}
taken = append(taken, [2]int{x, y})
placed = append(placed, Placed{
Mark: t.mi + 1, X: x, Y: y, Score: c.score, RadPx: t.radPx,
Region: int(d.landID[c.i]),
})
got++
}
}
return placed
}
// settlementJitter is how far the seed may move a site's score, as a fraction. Large enough that the
// ranking among comparable ground genuinely reshuffles between presses, small enough that a site three times
// better than its neighbour still wins every time.
const settlementJitter = 0.65
// hash01 is a deterministic value in [0,1) from two integers: splitmix64 finalised. Not a stream, so it does
// not matter which order the cells are visited in, which is cross-cutting rule 12.
func hash01(a, b uint64) float64 {
x := a*0x9e3779b97f4a7c15 + b*0xbf58476d1ce4e5b9
x ^= x >> 30
x *= 0xbf58476d1ce4e5b9
x ^= x >> 27
x *= 0x94d049bb133111eb
x ^= x >> 31
return float64(x>>11) / float64(1<<53)
}
// settlementScore is 0 where nobody would build and rises with the three things the terrain knows.
//
// Ground that is already painted scores zero, which is not a judgement about the ground: a site there cannot
// be stamped, because nothing may overwrite a hand-painted pixel. Scoring it anyway is how a settlement gets
// placed, counted and reported and then paints nothing at all - measured on the shipped template, one city of
// three and six villages of eighteen came out as empty blobs that the feature pass then dropped, so the run
// summary and `terrain plan` disagreed with each other and neither was wrong.
func (d *genData) settlementScore(i int) float64 {
if d.in.Sea[i] || d.protected[i] {
return 0
}
slope := float64(d.slopeDeg[i])
if slope > 12 {
return 0
}
flat := 1 - slope/12
river := 0.0
if d.flowLog != nil {
river = float64(d.flowLog[i])
}
// A coast bonus that falls off over a few kilometres: a harbour is worth a great deal, being forty
// kilometres inland is worth nothing either way.
coast := 0.0
if c := d.coastKm[i]; c >= 0 {
coast = math.Max(0, 1-float64(c)/5)
}
// Flat ground is a precondition rather than an attraction, so it multiplies; water and the sea are the
// reasons to be here, so they add.
return flat * (0.15 + 1.5*river + 1.0*coast)
}
// paintSettlements stamps each placed site, growing the disc until the blob is big enough to survive the
// feature pass.
//
// The growth loop is not a flourish. A disc loses whatever part of itself falls on a coastline somebody has
// already painted, or on the sea, and settlements are scored *towards* the coast, so the loss is routine
// rather than rare. Without it the generator places a town, reports it, writes it, and the feature reducer
// then drops it as a speck - so `terrain plan` lists fewer settlements than the run said it made, with
// nothing anywhere to explain the difference. Measured on the shipped template: three cities placed and two
// reported, eighteen villages placed and twelve reported.
//
// It gives up after a few tries rather than growing without limit: a site hemmed in on every side is telling
// you it is a bad site, and a village the size of a county is worse than a missing one.
func paintSettlements(d *genData, out *Raster, idx uint8, placed []Placed, minArea int) (int, int) {
n, pieces := 0, 0
for _, p := range placed {
if uint8(p.Mark) != idx {
continue
}
pieces++
got, r := 0, p.RadPx
for try := 0; try < 4; try++ {
// Re-stamping a larger disc only adds the new ring, because the cells already taken carry this
// mark, so the area accumulates rather than being recounted.
got += stampDisc(out, d, p.X, p.Y, r, idx, true)
if got >= minArea {
break
}
r = int(math.Ceil(float64(r) * 1.5))
}
n += got
}
return n, pieces
}
// stampDisc paints a filled circle, wrapping in X.
//
// overArea says whether this mark may cover ground another generated mark has already taken. A hand-painted
// pixel is never covered either way, which is what keeps a drawn stroke intact underneath a generated town.
func stampDisc(out *Raster, d *genData, cx, cy, r int, idx uint8, overArea bool) int {
n := 0
r2 := r * r
for dy := -r; dy <= r; dy++ {
y := cy + dy
if y < 0 || y >= out.H {
continue
}
for dx := -r; dx <= r; dx++ {
if dx*dx+dy*dy > r2 {
continue
}
x := ((cx+dx)%out.W + out.W) % out.W
i := y*out.W + x
if d.in.Sea[i] || d.protected[i] || (!overArea && out.Mark[i] != Blank) {
continue
}
out.Mark[i] = idx
n++
}
}
return n
}
func wrapDelta(d, w int) int {
if d > w/2 {
d -= w
} else if d < -w/2 {
d += w
}
return d
}
// srcOverlayForest is this pass's seeded noise stream. It sits above the detail passes' 40s and the
// tectonic 50s so that adding one here cannot reshuffle any existing field.
const srcOverlayForest = 60