@datamoc/mw_games 0.31.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/api-surface.json +312 -0
  3. package/dist/assets/index.d.ts +2 -0
  4. package/dist/assets/index.d.ts.map +1 -1
  5. package/dist/assets/index.js +1 -0
  6. package/dist/assets/index.js.map +1 -1
  7. package/dist/assets/svg.d.ts +87 -0
  8. package/dist/assets/svg.d.ts.map +1 -0
  9. package/dist/assets/svg.js +162 -0
  10. package/dist/assets/svg.js.map +1 -0
  11. package/dist/index.d.ts +1 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +1 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/mw_games.global.js +60 -60
  16. package/dist/mwl/fengari.d.ts +12 -6
  17. package/dist/mwl/fengari.d.ts.map +1 -1
  18. package/dist/mwl/fengari.js +69 -85
  19. package/dist/mwl/fengari.js.map +1 -1
  20. package/dist/mwl/runtime.d.ts +13 -0
  21. package/dist/mwl/runtime.d.ts.map +1 -1
  22. package/dist/mwl/runtime.js +72 -4
  23. package/dist/mwl/runtime.js.map +1 -1
  24. package/dist/mwl/schema.d.ts.map +1 -1
  25. package/dist/mwl/schema.js +4 -1
  26. package/dist/mwl/schema.js.map +1 -1
  27. package/dist/roguelike/Cave.d.ts +124 -0
  28. package/dist/roguelike/Cave.d.ts.map +1 -0
  29. package/dist/roguelike/Cave.js +216 -0
  30. package/dist/roguelike/Cave.js.map +1 -0
  31. package/dist/roguelike/Overworld.d.ts +89 -0
  32. package/dist/roguelike/Overworld.d.ts.map +1 -0
  33. package/dist/roguelike/Overworld.js +325 -0
  34. package/dist/roguelike/Overworld.js.map +1 -0
  35. package/dist/roguelike/generate.d.ts +8 -2
  36. package/dist/roguelike/generate.d.ts.map +1 -1
  37. package/dist/roguelike/generate.js +13 -3
  38. package/dist/roguelike/generate.js.map +1 -1
  39. package/dist/roguelike/index.d.ts +4 -0
  40. package/dist/roguelike/index.d.ts.map +1 -1
  41. package/dist/roguelike/index.js +2 -0
  42. package/dist/roguelike/index.js.map +1 -1
  43. package/dist/skeleton/index.d.ts +131 -0
  44. package/dist/skeleton/index.d.ts.map +1 -0
  45. package/dist/skeleton/index.js +206 -0
  46. package/dist/skeleton/index.js.map +1 -0
  47. package/dist/testing/SceneControls.d.ts +35 -0
  48. package/dist/testing/SceneControls.d.ts.map +1 -0
  49. package/dist/testing/SceneControls.js +49 -0
  50. package/dist/testing/SceneControls.js.map +1 -0
  51. package/dist/testing/index.d.ts +2 -0
  52. package/dist/testing/index.d.ts.map +1 -1
  53. package/dist/testing/index.js +1 -0
  54. package/dist/testing/index.js.map +1 -1
  55. package/dist/two-d/render/SkeletalSprite.d.ts +118 -0
  56. package/dist/two-d/render/SkeletalSprite.d.ts.map +1 -0
  57. package/dist/two-d/render/SkeletalSprite.js +189 -0
  58. package/dist/two-d/render/SkeletalSprite.js.map +1 -0
  59. package/dist/two-d/render/index.d.ts +2 -0
  60. package/dist/two-d/render/index.d.ts.map +1 -1
  61. package/dist/two-d/render/index.js +1 -0
  62. package/dist/two-d/render/index.js.map +1 -1
  63. package/dist/version.d.ts +1 -1
  64. package/dist/version.js +1 -1
  65. package/package.json +7 -2
  66. package/sbom.cdx.json +222 -222
@@ -0,0 +1,216 @@
1
+ import { hexDistance, hexNeighbors, hexToPixel } from '../core/Hex.js';
2
+ import * as Random from '../core/Random.js';
3
+ import { DUNGEON_KINDS } from './generate.js';
4
+ import { Level } from './Level.js';
5
+ /** the four orthogonal neighbours, the square-grid cave's whole step set */
6
+ function squareNeighbours(x, y) {
7
+ return [
8
+ [x, y - 1],
9
+ [x, y + 1],
10
+ [x - 1, y],
11
+ [x + 1, y],
12
+ ];
13
+ }
14
+ function hexNeighbours(x, y) {
15
+ return hexNeighbors(x, y).map((cell) => [cell.x, cell.y]);
16
+ }
17
+ /** a regular flat-top hex is 2/sqrt(3) wider than tall, so pixel distance is true distance */
18
+ const HEX_TILE_WIDTH = 2 / Math.sqrt(3);
19
+ function hexPixelDistanceSquared(ax, ay, bx, by) {
20
+ const a = hexToPixel(ax, ay, HEX_TILE_WIDTH, 1);
21
+ const b = hexToPixel(bx, by, HEX_TILE_WIDTH, 1);
22
+ return (a.x - b.x) ** 2 + (a.y - b.y) ** 2;
23
+ }
24
+ export function generateCave(options) {
25
+ const { width, height } = options;
26
+ if (width < 5 || height < 5)
27
+ throw new Error('a cave needs at least 5x5 cells');
28
+ if (options.chambers.length < 1)
29
+ throw new Error('a cave needs at least one chamber');
30
+ const wall = options.wall ?? 0;
31
+ const floor = options.floor ?? 1;
32
+ const kinds = options.kinds ?? [];
33
+ const shape = options.shape ?? 'square';
34
+ const hex = shape === 'hex';
35
+ const neighbours = hex ? hexNeighbours : squareNeighbours;
36
+ //how far a cell is from a target: growth wants rings around the centre, a walk wants the
37
+ //step that closes in on the target in the picture the player sees
38
+ const growthDistance = (ax, ay, bx, by) => hex ? hexDistance({ x: ax, y: ay }, { x: bx, y: by }) : Math.abs(ax - bx) + Math.abs(ay - by);
39
+ const walkDistance = (ax, ay, bx, by) => hex ? hexPixelDistanceSquared(ax, ay, bx, by) : (ax - bx) ** 2 + (ay - by) ** 2;
40
+ if (options.random === undefined && options.seed === undefined)
41
+ throw new Error('a cave needs a seed or a random source');
42
+ const source = options.random;
43
+ const roll = (bound) => Random.int(bound, undefined, source);
44
+ const level = new Level(width, height, [...DUNGEON_KINDS, ...kinds], wall, shape);
45
+ const at = (x, y) => y * width + x;
46
+ const inner = (x, y) => level.insideWithBorder(x, y);
47
+ const open = new Uint8Array(width * height);
48
+ const carve = (x, y) => {
49
+ if (inner(x, y))
50
+ open[at(x, y)] = 1;
51
+ };
52
+ if (source === undefined)
53
+ Random.push(options.seed);
54
+ try {
55
+ const centres = new Map();
56
+ const cellsById = new Map();
57
+ for (const chamber of options.chambers) {
58
+ if (chamber.size < 1)
59
+ throw new Error(`cave chamber ${chamber.id} needs at least one cell`);
60
+ const cx = Random.range(Math.max(1, chamber.xRange[0]), Math.min(width - 2, chamber.xRange[1]), source);
61
+ const cy = Random.range(Math.max(1, chamber.yRange[0]), Math.min(height - 2, chamber.yRange[1]), source);
62
+ centres.set(chamber.id, { x: cx, y: cy });
63
+ // random growth: absorb frontier cells until `size` are carved; `jagged` is the
64
+ // chance a step picks any frontier cell instead of the one nearest the centre
65
+ const cells = [{ x: cx, y: cy }];
66
+ const seen = new Set([at(cx, cy)]);
67
+ const frontier = [];
68
+ const push = (x, y) => {
69
+ for (const [nx, ny] of neighbours(x, y)) {
70
+ if (!inner(nx, ny) || seen.has(at(nx, ny)))
71
+ continue;
72
+ seen.add(at(nx, ny));
73
+ frontier.push([nx, ny]);
74
+ }
75
+ };
76
+ push(cx, cy);
77
+ while (cells.length < chamber.size && frontier.length) {
78
+ let pick = 0;
79
+ if (roll(100) < (chamber.jagged ?? 0)) {
80
+ pick = roll(frontier.length);
81
+ }
82
+ else {
83
+ let best = Infinity;
84
+ for (let index = 0; index < frontier.length; index++) {
85
+ const distance = growthDistance(frontier[index][0], frontier[index][1], cx, cy);
86
+ if (distance < best) {
87
+ best = distance;
88
+ pick = index;
89
+ }
90
+ }
91
+ }
92
+ const [nx, ny] = frontier.splice(pick, 1)[0];
93
+ cells.push({ x: nx, y: ny });
94
+ push(nx, ny);
95
+ }
96
+ for (const cell of cells)
97
+ carve(cell.x, cell.y);
98
+ cellsById.set(chamber.id, cells);
99
+ }
100
+ // a passage is a biased random walk from one chamber centre to another
101
+ const walk = (from, to, widthHexes, windiness) => {
102
+ let x = from.x;
103
+ let y = from.y;
104
+ for (let step = 0; step < width * height && (x !== to.x || y !== to.y); step++) {
105
+ carve(x, y);
106
+ if (widthHexes > 1)
107
+ for (const [nx, ny] of neighbours(x, y).slice(0, widthHexes - 1))
108
+ carve(nx, ny);
109
+ const steps = neighbours(x, y).filter(([nx, ny]) => inner(nx, ny));
110
+ let next = steps[0];
111
+ if (roll(100) < windiness) {
112
+ next = steps[roll(steps.length)];
113
+ }
114
+ else {
115
+ let best = Infinity;
116
+ for (const option of steps) {
117
+ const distance = walkDistance(option[0], option[1], to.x, to.y);
118
+ if (distance < best) {
119
+ best = distance;
120
+ next = option;
121
+ }
122
+ }
123
+ }
124
+ [x, y] = next;
125
+ }
126
+ carve(to.x, to.y);
127
+ };
128
+ for (const chamber of options.chambers) {
129
+ for (const passage of chamber.passages) {
130
+ const from = centres.get(chamber.id);
131
+ const to = centres.get(passage.to);
132
+ if (!from || !to)
133
+ throw new Error(`cave passage ${chamber.id} -> ${passage.to} names no chamber`);
134
+ if (roll(100) >= (passage.chance ?? 100))
135
+ continue;
136
+ walk(from, to, Math.max(1, passage.width ?? 1), passage.windiness ?? 0);
137
+ }
138
+ }
139
+ // everything the game named must be walkable, whatever the rolls did
140
+ const mustBeClear = options.mustBeClear ?? [];
141
+ for (const point of mustBeClear) {
142
+ carve(point.x, point.y);
143
+ for (const [nx, ny] of neighbours(point.x, point.y))
144
+ carve(nx, ny);
145
+ }
146
+ // force-join every chamber centre and mustBeClear cell to the first chamber, so the
147
+ // cave is always connected even where `chance` left a passage out
148
+ const first = centres.get(options.chambers[0].id);
149
+ if (first) {
150
+ const reachable = () => {
151
+ const found = new Set([at(first.x, first.y)]);
152
+ const queue = [[first.x, first.y]];
153
+ while (queue.length) {
154
+ const [x, y] = queue.pop();
155
+ for (const [nx, ny] of neighbours(x, y)) {
156
+ if (!inner(nx, ny) || !open[at(nx, ny)] || found.has(at(nx, ny)))
157
+ continue;
158
+ found.add(at(nx, ny));
159
+ queue.push([nx, ny]);
160
+ }
161
+ }
162
+ return found;
163
+ };
164
+ for (const target of [...centres.values(), ...mustBeClear]) {
165
+ if (!inner(target.x, target.y) || reachable().has(at(target.x, target.y)))
166
+ continue;
167
+ walk(target, first, 1, 15);
168
+ }
169
+ }
170
+ const reserved = new Set();
171
+ const starts = [];
172
+ for (const start of options.starts ?? []) {
173
+ const centre = centres.get(start.chamber);
174
+ if (!centre)
175
+ throw new Error(`cave start ${start.side} names no chamber`);
176
+ carve(centre.x, centre.y);
177
+ reserved.add(at(centre.x, centre.y));
178
+ if (start.ring)
179
+ for (const [nx, ny] of neighbours(centre.x, centre.y)) {
180
+ if (!inner(nx, ny))
181
+ continue;
182
+ carve(nx, ny);
183
+ reserved.add(at(nx, ny));
184
+ }
185
+ starts.push({ side: start.side, x: centre.x, y: centre.y });
186
+ }
187
+ const villages = [];
188
+ const density = options.villageDensity ?? 0;
189
+ if (density > 0) {
190
+ const candidates = [];
191
+ for (let index = 0; index < open.length; index++)
192
+ if (open[index] && !reserved.has(index))
193
+ candidates.push(index);
194
+ const wanted = Math.floor((candidates.length * density) / 1000);
195
+ for (let placed = 0; placed < wanted && candidates.length; placed++) {
196
+ const pick = roll(candidates.length);
197
+ const index = candidates.splice(pick, 1)[0];
198
+ villages.push({ x: index % width, y: Math.floor(index / width) });
199
+ }
200
+ }
201
+ for (let index = 0; index < open.length; index++)
202
+ if (open[index])
203
+ level.set(index % width, Math.floor(index / width), floor);
204
+ return {
205
+ level,
206
+ chambers: [...cellsById.entries()].map(([id, cells]) => ({ id, centre: centres.get(id), cells })),
207
+ starts,
208
+ villages,
209
+ };
210
+ }
211
+ finally {
212
+ if (source === undefined)
213
+ Random.pop();
214
+ }
215
+ }
216
+ //# sourceMappingURL=Cave.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Cave.js","sourceRoot":"","sources":["../../src/roguelike/Cave.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,89 @@
1
+ import type { LevelShape } from './Level.ts';
2
+ /**
3
+ * An overworld map: a whole realm as plain tile data.
4
+ *
5
+ * The world-map counterpart to `generateDungeon` and `generateCave`: elevation and moisture
6
+ * fields from seeded value noise classify each cell into water, plains, forest, hills,
7
+ * mountains, desert or swamp (the `seaLevel` knob decides how much is sea); rivers run from
8
+ * high ground down to the sea or a lake, merging when they meet and never crossing a
9
+ * mountain ridge (a ridge stops a river as a lake instead); settlements place themselves by
10
+ * suitability (near water, on buildable ground, spaced apart) and join into a road network
11
+ * that prefers flat ground and fords a river where it must cross one; points of interest
12
+ * carry a `leadsTo` id a game feeds its `Overworld` locations with; and every cell carries a
13
+ * coarse region id so encounter tables and names can hang off a region instead of a cell.
14
+ *
15
+ * `shape: 'hex'` generates the same realm on a hex grid (odd-q offset coordinates, the `Level`
16
+ * convention): the noise is sampled where each hex is drawn so landmasses are not stretched,
17
+ * rivers and roads step across the six edges, spacing and "near water" count hex distance.
18
+ *
19
+ * Deterministic: the same seed gives the same realm on every platform, drawn from the
20
+ * framework `Random` stack. Sizes up to a few hundred tiles a side generate on load; a
21
+ * bigger realm is a `threads.spawn` task like any other long job.
22
+ *
23
+ * @example
24
+ * ```ts
25
+ * import { generateOverworld } from '@datamoc/mw_games/roguelike';
26
+ *
27
+ * const realm = generateOverworld({ width: 96, height: 64, seed: 42 });
28
+ * console.log(realm.terrain[0]); // 'water' | 'plains' | 'forest' | ...
29
+ * console.log(realm.settlements.length); // roads join every one of them
30
+ * ```
31
+ */
32
+ export type OverworldTerrain = 'water' | 'plains' | 'forest' | 'hills' | 'mountains' | 'desert' | 'swamp';
33
+ export interface OverworldOptions {
34
+ width: number;
35
+ height: number;
36
+ seed: number;
37
+ /** elevation below which a cell is sea; 0..1, defaults to 0.4 */
38
+ seaLevel?: number;
39
+ /** river sources attempted on high ground; defaults to `width * height / 600` */
40
+ rivers?: number;
41
+ /** settlements to place by suitability; defaults to `width * height / 500` */
42
+ settlements?: number;
43
+ /** points of interest to place; defaults to `width * height / 900` */
44
+ pointsOfInterest?: number;
45
+ /** the coarse region grid's cell side, in tiles; defaults to 12 */
46
+ regionSize?: number;
47
+ /** how stretched the noise is: larger values give broader landmasses; defaults to 6 */
48
+ noiseScale?: number;
49
+ /** the grid the realm is laid out on; defaults to `'square'` */
50
+ shape?: LevelShape;
51
+ }
52
+ export interface OverworldSettlement {
53
+ x: number;
54
+ y: number;
55
+ kind: 'village' | 'town' | 'castle';
56
+ }
57
+ export interface OverworldInterest {
58
+ x: number;
59
+ y: number;
60
+ kind: 'ruin' | 'shrine' | 'dungeon';
61
+ /** the id a game's `Overworld` location this point leads to carries */
62
+ leadsTo: string;
63
+ }
64
+ export interface OverworldResult {
65
+ /** row-major terrain ids, `width * height` entries */
66
+ terrain: OverworldTerrain[];
67
+ /** row-major elevation, 0..1, the field `terrain` was classified from */
68
+ elevation: number[];
69
+ /** row-major moisture, 0..1 */
70
+ moisture: number[];
71
+ /** each river's cells from its source to its mouth or lake */
72
+ rivers: Array<Array<{
73
+ x: number;
74
+ y: number;
75
+ }>>;
76
+ /** each road's cells, settlement to settlement; river crossings are the ford sites */
77
+ roads: Array<Array<{
78
+ x: number;
79
+ y: number;
80
+ }>>;
81
+ settlements: OverworldSettlement[];
82
+ pointsOfInterest: OverworldInterest[];
83
+ /** coarse region id per cell, row-major; region r's cells share `(y / regionSize) * cols + (x / regionSize)` */
84
+ regions: number[];
85
+ /** how many columns the coarse region grid has */
86
+ regionColumns: number;
87
+ }
88
+ export declare function generateOverworld(options: OverworldOptions): OverworldResult;
89
+ //# sourceMappingURL=Overworld.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Overworld.d.ts","sourceRoot":"","sources":["../../src/roguelike/Overworld.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,WAAW,GAAG,QAAQ,GAAG,OAAO,CAAC;AAE1G,MAAM,WAAW,gBAAgB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IAEb,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,mEAAmE;IACnE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uFAAuF;IACvF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gEAAgE;IAChE,KAAK,CAAC,EAAE,UAAU,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IACnC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;CACpC;AAED,MAAM,WAAW,iBAAiB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,IAAI,EAAE,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;IACpC,uEAAuE;IACvE,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC/B,sDAAsD;IACtD,OAAO,EAAE,gBAAgB,EAAE,CAAC;IAC5B,yEAAyE;IACzE,SAAS,EAAE,MAAM,EAAE,CAAC;IACpB,+BAA+B;IAC/B,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,8DAA8D;IAC9D,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC/C,sFAAsF;IACtF,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC;QAAE,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,CAAC;IAC9C,WAAW,EAAE,mBAAmB,EAAE,CAAC;IACnC,gBAAgB,EAAE,iBAAiB,EAAE,CAAC;IACtC,gHAAgH;IAChH,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,kDAAkD;IAClD,aAAa,EAAE,MAAM,CAAC;CACtB;AA+ED,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAkK5E"}
@@ -0,0 +1,325 @@
1
+ import { hexDistance, hexNeighbors, hexRange } from '../core/Hex.js';
2
+ import * as Random from '../core/Random.js';
3
+ /** seeded value noise: a lattice of random values, smoothly interpolated, a few octaves */
4
+ function valueNoise(random, width, height, scale, hex) {
5
+ const latticeSide = (side) => Math.max(2, Math.ceil(side / scale) + 2);
6
+ const columns = latticeSide(width);
7
+ const rows = latticeSide(height);
8
+ const lattice = new Float64Array(columns * rows);
9
+ for (let index = 0; index < lattice.length; index++)
10
+ lattice[index] = random();
11
+ const smooth = (t) => t * t * (3 - 2 * t);
12
+ const field = new Array(width * height).fill(0);
13
+ for (let octave = 0; octave < 3; octave++) {
14
+ const factor = 1 << octave;
15
+ const weight = 1 / (factor * factor);
16
+ for (let y = 0; y < height; y++) {
17
+ for (let x = 0; x < width; x++) {
18
+ //on a hex grid sample where the cell is drawn: columns are 0.866 apart and
19
+ //odd columns sit half a row lower
20
+ const gx = ((hex ? x * HEX_COLUMN_PITCH : x) / scale) * factor;
21
+ const gy = ((hex ? y + (x & 1) / 2 : y) / scale) * factor;
22
+ const x0 = Math.floor(gx) % (columns - 1);
23
+ const y0 = Math.floor(gy) % (rows - 1);
24
+ const fx = smooth(gx - Math.floor(gx));
25
+ const fy = smooth(gy - Math.floor(gy));
26
+ const at = (lx, ly) => lattice[((y0 + ly) % rows) * columns + ((x0 + lx) % columns)];
27
+ const top = at(0, 0) * (1 - fx) + at(1, 0) * fx;
28
+ const bottom = at(0, 1) * (1 - fx) + at(1, 1) * fx;
29
+ field[y * width + x] += (top * (1 - fy) + bottom * fy) * weight;
30
+ }
31
+ }
32
+ }
33
+ const total = 1 + 1 / 4 + 1 / 16;
34
+ for (let index = 0; index < field.length; index++)
35
+ field[index] /= total;
36
+ return field;
37
+ }
38
+ /** distance between hex columns in units of the row height: 3/4 of a flat-top hex's width */
39
+ const HEX_COLUMN_PITCH = Math.sqrt(3) / 2;
40
+ const EIGHT = [
41
+ [0, -1],
42
+ [0, 1],
43
+ [-1, 0],
44
+ [1, 0],
45
+ [-1, -1],
46
+ [1, -1],
47
+ [-1, 1],
48
+ [1, 1],
49
+ ];
50
+ /**
51
+ * Visit the cells a river or road may step to from `(x, y)`: the eight surrounding squares
52
+ * (flagged diagonal for the corners), or the six hexes, every one an edge neighbour.
53
+ */
54
+ function forEachStep(hex, x, y, visit) {
55
+ if (hex) {
56
+ for (const cell of hexNeighbors(x, y))
57
+ visit(cell.x, cell.y, false);
58
+ return;
59
+ }
60
+ for (const [dx, dy] of EIGHT)
61
+ visit(x + dx, y + dy, dx !== 0 && dy !== 0);
62
+ }
63
+ /** how much a road dislikes crossing each terrain; water is only crossable at a ford */
64
+ const ROAD_COST = {
65
+ plains: 1,
66
+ forest: 3,
67
+ hills: 5,
68
+ desert: 2,
69
+ swamp: 6,
70
+ water: 24,
71
+ mountains: 40,
72
+ };
73
+ export function generateOverworld(options) {
74
+ const { width, height } = options;
75
+ if (width < 16 || height < 16)
76
+ throw new Error('an overworld needs at least 16x16 tiles');
77
+ const seaLevel = options.seaLevel ?? 0.4;
78
+ if (!(seaLevel > 0 && seaLevel < 1))
79
+ throw new Error('seaLevel must be between 0 and 1');
80
+ const scale = options.noiseScale ?? 6;
81
+ const hex = options.shape === 'hex';
82
+ const at = (x, y) => y * width + x;
83
+ const inside = (x, y) => x >= 0 && y >= 0 && x < width && y < height;
84
+ Random.push(options.seed);
85
+ try {
86
+ //a terrain field from elevation and moisture, both seeded; landmasses read as
87
+ //coherent shapes rather than scattered peaks because the noise octaves overlap
88
+ const elevation = valueNoise(() => Random.float(), width, height, scale, hex);
89
+ const moisture = valueNoise(() => Random.float(), width, height, scale * 1.5, hex);
90
+ const terrain = new Array(width * height);
91
+ const classify = (x, y) => {
92
+ const index = at(x, y);
93
+ const up = elevation[index];
94
+ const wet = moisture[index];
95
+ if (up < seaLevel)
96
+ return 'water';
97
+ if (up > 0.78)
98
+ return 'mountains';
99
+ if (up > 0.62)
100
+ return 'hills';
101
+ if (wet < 0.3)
102
+ return 'desert';
103
+ if (wet > 0.72)
104
+ return up < seaLevel + 0.08 ? 'swamp' : 'forest';
105
+ return 'plains';
106
+ };
107
+ for (let y = 0; y < height; y++)
108
+ for (let x = 0; x < width; x++)
109
+ terrain[at(x, y)] = classify(x, y);
110
+ //rivers: walk downhill from high ground to the sea or a lake; two rivers that
111
+ //meet merge (the second stops on the first), and a ridge that stops a walk
112
+ //drowns its basin cell into a lake instead of crossing the mountains
113
+ const riverCells = new Set();
114
+ const rivers = [];
115
+ const riverTarget = options.rivers ?? Math.max(1, Math.floor((width * height) / 600));
116
+ const highGround = [];
117
+ for (let index = 0; index < terrain.length; index++)
118
+ if (elevation[index] > 0.7 && terrain[index] !== 'water')
119
+ highGround.push(index);
120
+ for (let attempt = 0; attempt < riverTarget * 4 && rivers.length < riverTarget; attempt++) {
121
+ if (highGround.length === 0)
122
+ break;
123
+ const sourceIndex = highGround[Random.int(highGround.length)];
124
+ const path = [];
125
+ let x = sourceIndex % width;
126
+ let y = Math.floor(sourceIndex / width);
127
+ const walked = new Set();
128
+ for (let step = 0; step < width + height; step++) {
129
+ const index = at(x, y);
130
+ if (riverCells.has(index) || terrain[index] === 'water') {
131
+ if (path.length > 0)
132
+ path.push({ x, y });
133
+ break;
134
+ }
135
+ path.push({ x, y });
136
+ walked.add(index);
137
+ let best;
138
+ forEachStep(hex, x, y, (nx, ny) => {
139
+ if (!inside(nx, ny))
140
+ return;
141
+ const candidate = { x: nx, y: ny, up: elevation[at(nx, ny)] };
142
+ if (!best || candidate.up < best.up)
143
+ best = candidate;
144
+ });
145
+ if (!best || best.up >= elevation[at(x, y)]) {
146
+ //no way down: a ridge or a basin. Drown the cell as a lake and stop here
147
+ terrain[at(x, y)] = 'water';
148
+ break;
149
+ }
150
+ [x, y] = [best.x, best.y];
151
+ }
152
+ if (path.length >= 2) {
153
+ for (const cell of path)
154
+ riverCells.add(at(cell.x, cell.y));
155
+ rivers.push(path);
156
+ }
157
+ }
158
+ //settlements: buildable ground near water, spaced apart; a castle prefers hills,
159
+ //a town the plains, and everything else reads as a village
160
+ const settlements = [];
161
+ const nearWater = (x, y) => {
162
+ if (hex)
163
+ return hexRange({ x, y }, 3).some((cell) => inside(cell.x, cell.y) && terrain[at(cell.x, cell.y)] === 'water');
164
+ for (let dy = -3; dy <= 3; dy++)
165
+ for (let dx = -3; dx <= 3; dx++) {
166
+ const nx = x + dx;
167
+ const ny = y + dy;
168
+ if (inside(nx, ny) && terrain[at(nx, ny)] === 'water')
169
+ return true;
170
+ }
171
+ return false;
172
+ };
173
+ const spaced = (x, y, distance) => settlements.every((settlement) => (hex
174
+ ? hexDistance(settlement, { x, y })
175
+ : Math.abs(settlement.x - x) + Math.abs(settlement.y - y)) >= distance);
176
+ const wanted = options.settlements ?? Math.max(2, Math.floor((width * height) / 500));
177
+ for (let attempt = 0; attempt < wanted * 60 && settlements.length < wanted; attempt++) {
178
+ const x = Random.int(width);
179
+ const y = Random.int(height);
180
+ const kind = terrain[at(x, y)];
181
+ if ((kind !== 'plains' && kind !== 'hills' && kind !== 'forest') || !nearWater(x, y))
182
+ continue;
183
+ if (!spaced(x, y, 8))
184
+ continue;
185
+ settlements.push({ x, y, kind: kind === 'hills' ? 'castle' : 'town' });
186
+ }
187
+ for (const settlement of settlements)
188
+ if (settlement.kind === 'town' && Random.int(2) === 0)
189
+ settlement.kind = 'village';
190
+ //roads: a Dijkstra tree over the settlements, preferring flat ground; a water or
191
+ //river cell on a route is a ford or bridge site, which is why water is expensive
192
+ //rather than impassable here
193
+ const roads = [];
194
+ const from = settlements[0];
195
+ if (from) {
196
+ const connected = [from];
197
+ const pending = settlements.slice(1);
198
+ while (pending.length) {
199
+ let bestRoute;
200
+ for (const to of pending) {
201
+ for (const start of connected) {
202
+ const path = roadBetween(terrain, width, height, start, to, hex);
203
+ if (path && (!bestRoute || path.length < bestRoute.path.length))
204
+ bestRoute = { path, to };
205
+ }
206
+ }
207
+ if (!bestRoute)
208
+ break;
209
+ roads.push(bestRoute.path);
210
+ connected.push(bestRoute.to);
211
+ pending.splice(pending.indexOf(bestRoute.to), 1);
212
+ }
213
+ }
214
+ //points of interest away from settlements, each leading somewhere
215
+ const pointsOfInterest = [];
216
+ const wantedInterest = options.pointsOfInterest ?? Math.max(1, Math.floor((width * height) / 900));
217
+ for (let attempt = 0; attempt < wantedInterest * 40 && pointsOfInterest.length < wantedInterest; attempt++) {
218
+ const x = Random.int(width);
219
+ const y = Random.int(height);
220
+ const kind = terrain[at(x, y)];
221
+ if (kind === 'water' || kind === 'mountains')
222
+ continue;
223
+ if (!spaced(x, y, 6))
224
+ continue;
225
+ const kinds = ['ruin', 'shrine', 'dungeon'];
226
+ const interest = kinds[Random.int(kinds.length)];
227
+ pointsOfInterest.push({
228
+ x,
229
+ y,
230
+ kind: interest,
231
+ leadsTo: `overworld-${interest}-${pointsOfInterest.length}`,
232
+ });
233
+ }
234
+ //regions: a coarse id per cell, so a game hangs encounters and names off a region
235
+ const regionSize = Math.max(2, options.regionSize ?? 12);
236
+ const regionColumns = Math.ceil(width / regionSize);
237
+ const regions = new Array(width * height);
238
+ for (let y = 0; y < height; y++)
239
+ for (let x = 0; x < width; x++)
240
+ regions[at(x, y)] = Math.floor(y / regionSize) * regionColumns + Math.floor(x / regionSize);
241
+ return { terrain, elevation, moisture, rivers, roads, settlements, pointsOfInterest, regions, regionColumns };
242
+ }
243
+ finally {
244
+ Random.pop();
245
+ }
246
+ }
247
+ /** one road: Dijkstra from one settlement to another over per-terrain costs */
248
+ function roadBetween(terrain, width, height, from, to, hex) {
249
+ const at = (x, y) => y * width + x;
250
+ const inside = (x, y) => x >= 0 && y >= 0 && x < width && y < height;
251
+ const total = width * height;
252
+ const distance = new Float64Array(total).fill(Infinity);
253
+ const previous = new Int32Array(total).fill(-1);
254
+ const start = at(from.x, from.y);
255
+ const goal = at(to.x, to.y);
256
+ distance[start] = 0;
257
+ //a binary heap keyed by distance: with per-terrain costs this stays fast at the
258
+ //'few hundred tiles a side' sizes an overworld is allowed to load with
259
+ const heap = [start];
260
+ const inHeap = new Uint8Array(total);
261
+ const swapDown = (position) => {
262
+ while (true) {
263
+ const left = position * 2 + 1;
264
+ const right = left + 1;
265
+ let smallest = position;
266
+ if (left < heap.length && distance[heap[left]] < distance[heap[smallest]])
267
+ smallest = left;
268
+ if (right < heap.length && distance[heap[right]] < distance[heap[smallest]])
269
+ smallest = right;
270
+ if (smallest === position)
271
+ return;
272
+ [heap[position], heap[smallest]] = [heap[smallest], heap[position]];
273
+ position = smallest;
274
+ }
275
+ };
276
+ const pop = () => {
277
+ const top = heap[0];
278
+ const last = heap.pop();
279
+ if (heap.length) {
280
+ heap[0] = last;
281
+ swapDown(0);
282
+ }
283
+ inHeap[top] = 0;
284
+ return top;
285
+ };
286
+ const push = (cell) => {
287
+ heap.push(cell);
288
+ inHeap[cell] = 1;
289
+ let position = heap.length - 1;
290
+ while (position > 0) {
291
+ const parent = (position - 1) >> 1;
292
+ if (distance[heap[parent]] <= distance[heap[position]])
293
+ break;
294
+ [heap[position], heap[parent]] = [heap[parent], heap[position]];
295
+ position = parent;
296
+ }
297
+ };
298
+ while (heap.length) {
299
+ const current = pop();
300
+ if (current === goal) {
301
+ const path = [];
302
+ for (let cell = goal; cell !== -1; cell = previous[cell])
303
+ path.push({ x: cell % width, y: Math.floor(cell / width) });
304
+ return path.reverse();
305
+ }
306
+ const cx = current % width;
307
+ const cy = Math.floor(current / width);
308
+ forEachStep(hex, cx, cy, (nx, ny, diagonal) => {
309
+ if (!inside(nx, ny))
310
+ return;
311
+ const next = at(nx, ny);
312
+ if (distance[current] === Infinity)
313
+ return;
314
+ const step = ROAD_COST[terrain[next]] * (diagonal ? 1.5 : 1);
315
+ if (distance[current] + step < distance[next]) {
316
+ distance[next] = distance[current] + step;
317
+ previous[next] = current;
318
+ if (!inHeap[next])
319
+ push(next);
320
+ }
321
+ });
322
+ }
323
+ return undefined;
324
+ }
325
+ //# sourceMappingURL=Overworld.js.map