Skip to content

Commit 0b89c7f

Browse files
committed
Refactor river zone draw
1 parent 7e64a2e commit 0b89c7f

File tree

7 files changed

+227
-99
lines changed

7 files changed

+227
-99
lines changed

src/lib/geometry/point/set.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ export class PointSet {
108108
forEach(callback) {
109109
this.#indexSet.forEach(index => {
110110
const point = this.rect.indexToPoint(index)
111-
callback(point)
111+
callback(point, index)
112112
})
113113
}
114114
}

src/model/tilemap/worldmap/world/basin/index.js

Lines changed: 35 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { Grid } from '/src/lib/grid'
33
import { Direction } from '/src/lib/direction'
44
import { DirectionBitMaskGrid } from '/src/model/tilemap/lib/bitmask'
55

6-
import { buildBasinGrid } from './model'
6+
import {
7+
buildBasinModel,
8+
buildMidpointGrid
9+
} from './model'
710
import {
811
Basin,
12+
WaterBasin,
913
EMPTY,
1014
DiffuseLandBasin,
11-
WaterBasin,
1215
} from './type'
1316

1417

@@ -19,6 +22,8 @@ export class BasinLayer {
1922
// grid of basin ids
2023
#basinGrid
2124

25+
#midpointGrid
26+
2227
// the walk distance of each basin starting from shore
2328
// initial value is 0, used to determine river stretch
2429
#distanceGrid
@@ -30,21 +35,22 @@ export class BasinLayer {
3035
#typeMap = new Map()
3136

3237
// map a point to a basin zone direction bitmask
33-
#erosionMaskGrid
38+
#directionBitmask
3439

3540
constructor(context) {
3641
const {rect, world, zoneRect} = context
3742
this.#world = world
3843
this.#zoneRect = zoneRect
3944
this.#distanceGrid = Grid.fromRect(rect, () => 0)
4045
this.#erosionGrid = Grid.fromRect(rect, () => null)
41-
this.#erosionMaskGrid = new DirectionBitMaskGrid(rect)
42-
this.#basinGrid = buildBasinGrid({
46+
this.#midpointGrid = buildMidpointGrid(context)
47+
this.#directionBitmask = new DirectionBitMaskGrid(rect)
48+
this.#basinGrid = buildBasinModel({
4349
...context,
4450
typeMap: this.#typeMap,
4551
erosionGrid: this.#erosionGrid,
4652
distanceGrid: this.#distanceGrid,
47-
erosionMaskGrid: this.#erosionMaskGrid,
53+
directionBitmask: this.#directionBitmask,
4854
})
4955
}
5056

@@ -53,35 +59,38 @@ export class BasinLayer {
5359
}
5460

5561
get(point) {
56-
if (! this.has(point)) return null
5762
const id = this.#basinGrid.get(point)
5863
const typeId = this.#typeMap.get(id)
5964
const directionId = this.#erosionGrid.get(point)
65+
const midpointIndex = this.#midpointGrid.get(point)
6066
return {
6167
id,
62-
type: Basin.parse(typeId),
63-
distance: this.#distanceGrid.get(point),
68+
type: Basin.parse(typeId) || WaterBasin,
69+
distance: this.#distanceGrid.get(point) || 0,
70+
midpoint: this.#zoneRect.indexToPoint(midpointIndex),
6471
erosion: Direction.fromId(directionId),
6572
}
6673
}
6774

6875
canCreateRiver(point) {
69-
if (! this.has(point))
70-
return false
7176
const id = this.#basinGrid.get(point)
7277
const typeId = this.#typeMap.get(id)
7378
const basinType = Basin.parse(typeId)
74-
const isDivide = this.#erosionMaskGrid.get(point).length === 1
79+
if (! basinType) {
80+
console.log(point);
81+
82+
return false
83+
}
84+
const isDivide = this.#directionBitmask.get(point).length === 1
7585
return basinType.hasRivers && isDivide
7686
}
7787

7888
getText(point) {
79-
if (! this.has(point)) return ''
8089
const basin = this.get(point)
8190
const attrs = [
8291
`id=${basin.id}`,
8392
`type=${basin.type ? basin.type.name : ''}`,
84-
`erosion=${basin.erosion.name}`,
93+
`erosion=${basin.erosion ? basin.erosion.name : 'NADA'}`,
8594
`distance=${basin.distance}`,
8695
].join(',')
8796
return `Basin(${attrs})`
@@ -94,14 +103,19 @@ export class BasinLayer {
94103
if (this.#world.surface.isLand(tilePoint)) {
95104
color = basin ? basin.type.color : DiffuseLandBasin.color
96105
} else {
97-
color = WaterBasin.color
106+
color = WaterBasin.color
98107
}
99108
canvas.rect(canvasPoint, tileSize, color.toHex())
100109
if (basin && params.get('showErosion')) {
101-
const text = basin.erosion.symbol
102-
const textColor = color.invert().toHex()
103-
canvas.text(canvasPoint, tileSize, text, textColor)
104-
// this.#drawErosionLines(props, basin)
110+
if (basin.erosion) {
111+
const text = basin.erosion.symbol
112+
const textColor = color.invert().toHex()
113+
canvas.text(canvasPoint, tileSize, text, textColor)
114+
} else {
115+
console.log(tilePoint);
116+
117+
}
118+
this.#drawErosionLines(props, basin)
105119
}
106120
}
107121

@@ -111,16 +125,11 @@ export class BasinLayer {
111125
const lineWidth = Math.round(props.tileSize / 20)
112126
// calc midpoint point on canvas
113127
const pixelsPerZonePoint = tileSize / this.#zoneRect.width
114-
const zoneSize = this.#zoneRect.width
115-
const midpoint = [
116-
Math.floor(zoneSize / 2),
117-
Math.floor(zoneSize / 2),
118-
]
119-
const canvasMidpoint = Point.multiplyScalar(midpoint, pixelsPerZonePoint)
128+
const canvasMidpoint = Point.multiplyScalar(basin.midpoint, pixelsPerZonePoint)
120129
const meanderPoint = Point.plus(canvasPoint, canvasMidpoint)
121130

122131
// draw line for each neighbor with a basin connection
123-
const directions = this.#erosionMaskGrid.get(tilePoint)
132+
const directions = this.#directionBitmask.get(tilePoint)
124133
for(let direction of directions) {
125134
// map each axis coordinate to random value in zone's rect edge
126135
// summing values from origin [0, 0] bottom-right oriented

src/model/tilemap/worldmap/world/basin/model.js

Lines changed: 102 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { ConcurrentFill } from '/src/lib/floodfill/concurrent'
22
import { Grid } from '/src/lib/grid'
3+
import { Direction } from '/src/lib/direction'
4+
import { Random } from '/src/lib/random'
35
import { Point } from '/src/lib/geometry/point'
46

57
import {
@@ -9,54 +11,62 @@ import {
911
DiffuseLandBasin,
1012
Basin,
1113
EMPTY,
14+
WaterBasin,
1215
} from './type'
1316

1417

1518
const FILL_CHANCE = .1 // chance of fill growing
1619
const FILL_GROWTH = 4 // make fill basins grow bigger than others
20+
const MIDPOINT_RATE = .6
1721

1822

19-
export function buildBasinGrid(baseContext) {
23+
export function buildMidpointGrid({rect, zoneRect}) {
24+
const centerIndex = Math.floor(zoneRect.width / 2)
25+
// 60% around center point
26+
const offset = Math.floor(centerIndex * MIDPOINT_RATE)
27+
28+
return Grid.fromRect(rect, () => {
29+
const x = centerIndex + Random.int(-offset, offset)
30+
const y = centerIndex + Random.int(-offset, offset)
31+
return zoneRect.pointToIndex([x, y])
32+
})
33+
}
34+
35+
36+
export function buildBasinModel(baseContext) {
2037
const {rect, world, typeMap} = baseContext
2138
// init basin id counter
2239
let basinId = 0
2340
// get surface border points and setup basin types and fill
24-
const fillMap = new Map()
41+
const landFillMap = new Map()
42+
const waterFillMap = new Map()
2543
const surveyMap = new Map()
2644
const basinGrid = Grid.fromRect(rect, point => {
2745
// reuse the process of basinGrid creation to determine river mouths
2846
// is this point an erosion path (possible river mouth)?
29-
const survey = surveyNeighbors(baseContext, point)
30-
if (world.surface.isLand(point)) {
31-
if (world.surface.isBorder(point)) {
32-
// set type on init
47+
if (world.surface.isBorder(point)) {
48+
if (world.surface.isLand(point)) {
49+
const survey = surveyNeighbors(baseContext, point)
50+
surveyMap.set(basinId, survey)
3351
const type = buildBasinType(world, survey)
3452
typeMap.set(basinId, type.id)
35-
surveyMap.set(basinId, survey)
36-
fillMap.set(basinId, {origin: point})
37-
basinId++
53+
landFillMap.set(basinId, {origin: point})
54+
} else {
55+
typeMap.set(basinId, WaterBasin.id)
56+
waterFillMap.set(basinId, {origin: point})
3857
}
58+
basinId++
3959
}
4060
return EMPTY
4161
})
4262
const context = {...baseContext, basinGrid, surveyMap}
43-
// returns a grid storing basin ids
44-
// ocean and lake/sea borders must grow at same time
45-
// but lakes/seas are delayed to grow less
46-
new BasinGridFill(fillMap, context).complete()
63+
new LandBasinFill(landFillMap, context).complete()
64+
new WaterBasinFill(waterFillMap, context).complete()
4765
return basinGrid
4866
}
4967

5068

51-
class BasinGridFill extends ConcurrentFill {
52-
onInitFill(fill, fillPoint) {
53-
const {surveyMap} = fill.context
54-
const survey = surveyMap.get(fill.id)
55-
// the basin opposite border is the parentPoint
56-
// update erosion path
57-
this._fillBasin(fill, fillPoint, survey.oppositeBorder)
58-
}
59-
69+
class LandBasinFill extends ConcurrentFill {
6070
getChance(fill) { return FILL_CHANCE }
6171

6272
getGrowth(fill) {
@@ -65,31 +75,29 @@ class BasinGridFill extends ConcurrentFill {
6575
return basin.isEndorheic ? 1 : FILL_GROWTH
6676
}
6777

78+
onInitFill(fill, fillPoint) {
79+
const {surveyMap} = fill.context
80+
const survey = surveyMap.get(fill.id)
81+
// the basin opposite border is the parentPoint
82+
// update erosion path
83+
this._fillBasin(fill, fillPoint, survey.oppositeBorder)
84+
}
85+
6886
onFill(fill, fillPoint, parentPoint) {
69-
const {distanceGrid, erosionMaskGrid} = fill.context
87+
const {distanceGrid, directionBitmask} = fill.context
7088
// distance to source by point
71-
const currentDistance = distanceGrid.wrapGet(parentPoint)
89+
const currentDistance = distanceGrid.get(parentPoint)
7290
distanceGrid.wrapSet(fillPoint, currentDistance + 1)
7391
// update parent point erosion path
7492
const upstream = Point.directionBetween(parentPoint, fillPoint)
75-
erosionMaskGrid.add(parentPoint, upstream)
93+
directionBitmask.add(parentPoint, upstream)
7694
this._fillBasin(fill, fillPoint, parentPoint)
7795
}
7896

7997
getNeighbors(fill, parentPoint) {
8098
return Point.around(parentPoint)
8199
}
82100

83-
_fillBasin(fill, fillPoint, parentPoint) {
84-
const {erosionGrid, erosionMaskGrid, basinGrid} = fill.context
85-
// basin id is the same as fill id
86-
basinGrid.set(fillPoint, fill.id)
87-
// set erosion flow to parent
88-
const direction = Point.directionBetween(fillPoint, parentPoint)
89-
erosionGrid.wrapSet(fillPoint, direction.id)
90-
erosionMaskGrid.add(fillPoint, direction)
91-
}
92-
93101
isEmpty(fill, fillPoint, parentPoint) {
94102
const {world, basinGrid, typeMap} = fill.context
95103
const basin = Basin.parse(typeMap.get(fill.id))
@@ -102,6 +110,16 @@ class BasinGridFill extends ConcurrentFill {
102110
// avoid fill if different types
103111
return target.isWater == parent.isWater
104112
}
113+
114+
_fillBasin(fill, fillPoint, parentPoint) {
115+
const {erosionGrid, directionBitmask, basinGrid} = fill.context
116+
// basin id is the same as fill id
117+
basinGrid.set(fillPoint, fill.id)
118+
// set erosion flow to parent
119+
const direction = Point.directionBetween(fillPoint, parentPoint)
120+
erosionGrid.wrapSet(fillPoint, direction.id)
121+
directionBitmask.add(fillPoint, direction)
122+
}
105123
}
106124

107125

@@ -139,3 +157,52 @@ function buildBasinType(world, survey) {
139157
}
140158
return DiffuseLandBasin
141159
}
160+
161+
162+
class WaterBasinFill extends ConcurrentFill {
163+
getChance(fill) { return FILL_CHANCE }
164+
getGrowth(fill) { return FILL_GROWTH }
165+
166+
getNeighbors(fill, parentPoint) {
167+
return Point.around(parentPoint)
168+
}
169+
170+
onInitFill(fill, fillPoint) {
171+
const { world, erosionGrid, directionBitmask, basinGrid, typeMap } = fill.context
172+
// basin id is the same as fill id
173+
let riverDirection
174+
basinGrid.set(fillPoint, fill.id)
175+
Point.adjacents(fillPoint, (sidePoint, direction) => {
176+
if (world.surface.isLand(sidePoint)) {
177+
const sideId = basinGrid.get(sidePoint)
178+
const sideType = Basin.parse(typeMap.get(sideId))
179+
if (sideType && sideType.hasRivers) {
180+
riverDirection = Point.directionBetween(sidePoint, fillPoint)
181+
directionBitmask.add(fillPoint, direction)
182+
}
183+
} else {
184+
directionBitmask.add(fillPoint, direction)
185+
}
186+
})
187+
const erosionDirection = riverDirection || Direction.randomCardinal()
188+
erosionGrid.set(fillPoint, erosionDirection.id)
189+
}
190+
191+
isEmpty(fill, point) {
192+
const {world, basinGrid} = fill.context
193+
const basinIsEmpty = basinGrid.get(point) === EMPTY
194+
const isWater = world.surface.isWater(point)
195+
return isWater && basinIsEmpty
196+
}
197+
198+
onFill(fill, fillPoint, parentPoint) {
199+
const {basinGrid, erosionGrid, directionBitmask} = fill.context
200+
// update parent point erosion path
201+
const upstream = Point.directionBetween(fillPoint, parentPoint)
202+
const downstream = Point.directionBetween(parentPoint, fillPoint)
203+
directionBitmask.add(fillPoint, upstream)
204+
directionBitmask.add(parentPoint, downstream)
205+
erosionGrid.set(fillPoint, upstream.id)
206+
basinGrid.set(fillPoint, fill.id)
207+
}
208+
}

src/model/tilemap/worldmap/world/river/index.js

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,19 @@ export class RiverLayer {
2525
#midpointGrid
2626
// map a river point to its river type
2727
#stretchMap
28-
#riverMouths
29-
#estuaryPointSet
3028

3129
constructor(context) {
3230
const {rect, world, zoneRect} = context
3331
this.#zoneRect = zoneRect
3432
this.world = world
3533
this.#midpointGrid = buildMidpointGrid(context)
3634
this.#directionMaskGrid = new DirectionBitMaskGrid(rect)
37-
this.#riverMouths = new PointSet(rect)
38-
this.#estuaryPointSet = new PointSet(rect)
3935
this.#stretchMap = new PointMap(rect)
4036
const _context = {
4137
...context,
4238
riverNames: this.#riverNames,
43-
riverMouths: this.#riverMouths,
44-
estuaryPointSet: this.#estuaryPointSet,
4539
directionMaskGrid: this.#directionMaskGrid,
46-
stretchMap: this.#stretchMap,
40+
stretchMap: this.#stretchMap
4741
}
4842
this.#riverPointGrid = buildRiverModel(_context)
4943
}
@@ -83,8 +77,8 @@ export class RiverLayer {
8377
return ''
8478
const river = this.get(point)
8579
const attrs = [
86-
`${river.id}`,
87-
`${river.name}${river.mouth ? ' mouth' : ''}`,
80+
`id=${river.id}`,
81+
`name=${river.name}`,
8882
`stretch=${river.stretch.name}`,
8983
`midpoint=${river.midpoint}`,
9084
].filter(x=>x).join(',')

0 commit comments

Comments
 (0)