@lagless/2d-map-generator 0.0.48

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 (124) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +373 -0
  3. package/dist/index.d.ts +31 -0
  4. package/dist/index.d.ts.map +1 -0
  5. package/dist/index.js +27 -0
  6. package/dist/lib/collision/rapier-provider.d.ts +54 -0
  7. package/dist/lib/collision/rapier-provider.d.ts.map +1 -0
  8. package/dist/lib/collision/rapier-provider.js +67 -0
  9. package/dist/lib/collision/spatial-grid-provider.d.ts +19 -0
  10. package/dist/lib/collision/spatial-grid-provider.d.ts.map +1 -0
  11. package/dist/lib/collision/spatial-grid-provider.js +127 -0
  12. package/dist/lib/core/generated-map.d.ts +14 -0
  13. package/dist/lib/core/generated-map.d.ts.map +1 -0
  14. package/dist/lib/core/generated-map.js +17 -0
  15. package/dist/lib/core/map-dimensions.d.ts +8 -0
  16. package/dist/lib/core/map-dimensions.d.ts.map +1 -0
  17. package/dist/lib/core/map-dimensions.js +7 -0
  18. package/dist/lib/core/map-generator.d.ts +14 -0
  19. package/dist/lib/core/map-generator.d.ts.map +1 -0
  20. package/dist/lib/core/map-generator.js +124 -0
  21. package/dist/lib/core/terrain-query.d.ts +17 -0
  22. package/dist/lib/core/terrain-query.d.ts.map +1 -0
  23. package/dist/lib/core/terrain-query.js +54 -0
  24. package/dist/lib/features/biome-feature.d.ts +10 -0
  25. package/dist/lib/features/biome-feature.d.ts.map +1 -0
  26. package/dist/lib/features/biome-feature.js +9 -0
  27. package/dist/lib/features/bridge-feature.d.ts +10 -0
  28. package/dist/lib/features/bridge-feature.d.ts.map +1 -0
  29. package/dist/lib/features/bridge-feature.js +55 -0
  30. package/dist/lib/features/grass-feature.d.ts +10 -0
  31. package/dist/lib/features/grass-feature.d.ts.map +1 -0
  32. package/dist/lib/features/grass-feature.js +36 -0
  33. package/dist/lib/features/ground-patch-feature.d.ts +10 -0
  34. package/dist/lib/features/ground-patch-feature.d.ts.map +1 -0
  35. package/dist/lib/features/ground-patch-feature.js +57 -0
  36. package/dist/lib/features/lake-feature.d.ts +10 -0
  37. package/dist/lib/features/lake-feature.d.ts.map +1 -0
  38. package/dist/lib/features/lake-feature.js +65 -0
  39. package/dist/lib/features/object-placement-feature.d.ts +18 -0
  40. package/dist/lib/features/object-placement-feature.d.ts.map +1 -0
  41. package/dist/lib/features/object-placement-feature.js +229 -0
  42. package/dist/lib/features/places-feature.d.ts +10 -0
  43. package/dist/lib/features/places-feature.d.ts.map +1 -0
  44. package/dist/lib/features/places-feature.js +14 -0
  45. package/dist/lib/features/river-feature.d.ts +10 -0
  46. package/dist/lib/features/river-feature.d.ts.map +1 -0
  47. package/dist/lib/features/river-feature.js +122 -0
  48. package/dist/lib/features/shore-feature.d.ts +10 -0
  49. package/dist/lib/features/shore-feature.d.ts.map +1 -0
  50. package/dist/lib/features/shore-feature.js +20 -0
  51. package/dist/lib/math/catmull-rom.d.ts +12 -0
  52. package/dist/lib/math/catmull-rom.d.ts.map +1 -0
  53. package/dist/lib/math/catmull-rom.js +42 -0
  54. package/dist/lib/math/collision-test.d.ts +14 -0
  55. package/dist/lib/math/collision-test.d.ts.map +1 -0
  56. package/dist/lib/math/collision-test.js +29 -0
  57. package/dist/lib/math/jagged-aabb.d.ts +12 -0
  58. package/dist/lib/math/jagged-aabb.d.ts.map +1 -0
  59. package/dist/lib/math/jagged-aabb.js +47 -0
  60. package/dist/lib/math/polygon-utils.d.ts +19 -0
  61. package/dist/lib/math/polygon-utils.d.ts.map +1 -0
  62. package/dist/lib/math/polygon-utils.js +79 -0
  63. package/dist/lib/math/river-polygon.d.ts +12 -0
  64. package/dist/lib/math/river-polygon.d.ts.map +1 -0
  65. package/dist/lib/math/river-polygon.js +84 -0
  66. package/dist/lib/math/spline.d.ts +15 -0
  67. package/dist/lib/math/spline.d.ts.map +1 -0
  68. package/dist/lib/math/spline.js +127 -0
  69. package/dist/lib/physics/canopy-sensor-tag.d.ts +2 -0
  70. package/dist/lib/physics/canopy-sensor-tag.d.ts.map +1 -0
  71. package/dist/lib/physics/canopy-sensor-tag.js +1 -0
  72. package/dist/lib/physics/create-map-colliders.d.ts +12 -0
  73. package/dist/lib/physics/create-map-colliders.d.ts.map +1 -0
  74. package/dist/lib/physics/create-map-colliders.js +29 -0
  75. package/dist/lib/presets/standard-biome.d.ts +3 -0
  76. package/dist/lib/presets/standard-biome.d.ts.map +1 -0
  77. package/dist/lib/presets/standard-biome.js +9 -0
  78. package/dist/lib/presets/standard-map.d.ts +6 -0
  79. package/dist/lib/presets/standard-map.d.ts.map +1 -0
  80. package/dist/lib/presets/standard-map.js +61 -0
  81. package/dist/lib/presets/standard-objects.d.ts +6 -0
  82. package/dist/lib/presets/standard-objects.d.ts.map +1 -0
  83. package/dist/lib/presets/standard-objects.js +25 -0
  84. package/dist/lib/types/collision-provider.d.ts +8 -0
  85. package/dist/lib/types/collision-provider.d.ts.map +1 -0
  86. package/dist/lib/types/collision-provider.js +1 -0
  87. package/dist/lib/types/feature-configs.d.ts +139 -0
  88. package/dist/lib/types/feature-configs.d.ts.map +1 -0
  89. package/dist/lib/types/feature-configs.js +8 -0
  90. package/dist/lib/types/feature.d.ts +31 -0
  91. package/dist/lib/types/feature.d.ts.map +1 -0
  92. package/dist/lib/types/feature.js +12 -0
  93. package/dist/lib/types/generated-map.d.ts +10 -0
  94. package/dist/lib/types/generated-map.d.ts.map +1 -0
  95. package/dist/lib/types/generated-map.js +1 -0
  96. package/dist/lib/types/generated-river.d.ts +23 -0
  97. package/dist/lib/types/generated-river.d.ts.map +1 -0
  98. package/dist/lib/types/generated-river.js +1 -0
  99. package/dist/lib/types/geometry.d.ts +30 -0
  100. package/dist/lib/types/geometry.d.ts.map +1 -0
  101. package/dist/lib/types/geometry.js +5 -0
  102. package/dist/lib/types/index.d.ts +16 -0
  103. package/dist/lib/types/index.d.ts.map +1 -0
  104. package/dist/lib/types/index.js +5 -0
  105. package/dist/lib/types/map-generator-config.d.ts +8 -0
  106. package/dist/lib/types/map-generator-config.d.ts.map +1 -0
  107. package/dist/lib/types/map-generator-config.js +1 -0
  108. package/dist/lib/types/object-def.d.ts +58 -0
  109. package/dist/lib/types/object-def.d.ts.map +1 -0
  110. package/dist/lib/types/object-def.js +5 -0
  111. package/dist/lib/types/placed-object.d.ts +19 -0
  112. package/dist/lib/types/placed-object.d.ts.map +1 -0
  113. package/dist/lib/types/placed-object.js +10 -0
  114. package/dist/lib/types/prng-interface.d.ts +14 -0
  115. package/dist/lib/types/prng-interface.d.ts.map +1 -0
  116. package/dist/lib/types/prng-interface.js +1 -0
  117. package/dist/lib/utils/extract-canopy-zones.d.ts +22 -0
  118. package/dist/lib/utils/extract-canopy-zones.d.ts.map +1 -0
  119. package/dist/lib/utils/extract-canopy-zones.js +34 -0
  120. package/dist/lib/utils/sort-placed-objects.d.ts +3 -0
  121. package/dist/lib/utils/sort-placed-objects.d.ts.map +1 -0
  122. package/dist/lib/utils/sort-placed-objects.js +3 -0
  123. package/dist/tsconfig.lib.tsbuildinfo +1 -0
  124. package/package.json +42 -0
@@ -0,0 +1,65 @@
1
+ import { MathOps } from '@lagless/math';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import { generateRiverPolygon } from '../math/river-polygon.js';
4
+ const TWO_PI = Math.PI * 2;
5
+ export class LakeFeature {
6
+ static id = FeatureId.Lake;
7
+ id = FeatureId.Lake;
8
+ requires = [];
9
+ generate(ctx, config) {
10
+ const lakes = [];
11
+ for (const lakeDef of config.lakes) {
12
+ if (ctx.random.getFloat() >= lakeDef.odds) {
13
+ continue;
14
+ }
15
+ const boundCenterX = lakeDef.spawnBound.pos.x * ctx.width;
16
+ const boundCenterY = lakeDef.spawnBound.pos.y * ctx.height;
17
+ const boundRad = lakeDef.spawnBound.rad;
18
+ const angle = ctx.random.getFloat() * TWO_PI;
19
+ const dist = ctx.random.getFloat() * boundRad;
20
+ const centerX = boundCenterX + MathOps.cos(angle) * dist;
21
+ const centerY = boundCenterY + MathOps.sin(angle) * dist;
22
+ const numPoints = 20;
23
+ const smoothPasses = 3;
24
+ // Generate raw radii with random variation
25
+ const rawRadii = [];
26
+ for (let i = 0; i < numPoints; i++) {
27
+ rawRadii.push(lakeDef.innerRad + ctx.random.getFloat() * (lakeDef.outerRad - lakeDef.innerRad));
28
+ }
29
+ // Smooth radii with circular moving average to eliminate star-shaped spikes
30
+ let radii = rawRadii;
31
+ for (let pass = 0; pass < smoothPasses; pass++) {
32
+ const smoothed = [];
33
+ for (let i = 0; i < numPoints; i++) {
34
+ const prev = radii[(i - 1 + numPoints) % numPoints];
35
+ const curr = radii[i];
36
+ const next = radii[(i + 1) % numPoints];
37
+ smoothed.push(prev * 0.25 + curr * 0.5 + next * 0.25);
38
+ }
39
+ radii = smoothed;
40
+ }
41
+ const splinePoints = [];
42
+ for (let i = 0; i < numPoints; i++) {
43
+ const theta = (i / numPoints) * TWO_PI;
44
+ splinePoints.push({
45
+ x: centerX + MathOps.cos(theta) * radii[i],
46
+ y: centerY + MathOps.sin(theta) * radii[i],
47
+ });
48
+ }
49
+ splinePoints.push({ x: splinePoints[0].x, y: splinePoints[0].y });
50
+ // Control points already define the lake boundary — waterWidth is a small outward expansion
51
+ const waterWidth = 2;
52
+ const shoreWidth = Math.max(4, Math.min(8, (lakeDef.innerRad + lakeDef.outerRad) * 0.1));
53
+ const lake = generateRiverPolygon({
54
+ splinePoints,
55
+ waterWidth,
56
+ shoreWidth,
57
+ looped: true,
58
+ mapWidth: ctx.width,
59
+ mapHeight: ctx.height,
60
+ });
61
+ lakes.push(lake);
62
+ }
63
+ return { lakes };
64
+ }
65
+ }
@@ -0,0 +1,18 @@
1
+ import type { IMapFeature, GenerationContext } from '../types/feature.js';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import type { ObjectPlacementConfig, ObjectPlacementOutput } from '../types/feature-configs.js';
4
+ import type { MapObjectDef } from '../types/object-def.js';
5
+ export declare class ObjectPlacementFeature implements IMapFeature<ObjectPlacementConfig, ObjectPlacementOutput> {
6
+ static readonly id = FeatureId.ObjectPlacement;
7
+ readonly id = FeatureId.ObjectPlacement;
8
+ readonly requires: readonly FeatureId[];
9
+ generate(ctx: GenerationContext, config: ObjectPlacementConfig): ObjectPlacementOutput;
10
+ }
11
+ export interface PlacementBounds {
12
+ readonly halfWidth: number;
13
+ readonly halfHeight: number;
14
+ readonly centerX: number;
15
+ readonly centerY: number;
16
+ }
17
+ export declare function computePlacementBounds(def: MapObjectDef): PlacementBounds | undefined;
18
+ //# sourceMappingURL=object-placement-feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"object-placement-feature.d.ts","sourceRoot":"","sources":["../../../src/lib/features/object-placement-feature.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,qBAAqB,EAAE,qBAAqB,EAAE,MAAM,6BAA6B,CAAC;AAShG,OAAO,KAAK,EAAE,YAAY,EAAqB,MAAM,wBAAwB,CAAC;AAU9E,qBAAa,sBAAuB,YAAW,WAAW,CAAC,qBAAqB,EAAE,qBAAqB,CAAC;IACtG,MAAM,CAAC,QAAQ,CAAC,EAAE,6BAA6B;IAC/C,QAAQ,CAAC,EAAE,6BAA6B;IACxC,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAsC;IAE7E,QAAQ,CAAC,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,qBAAqB,GAAG,qBAAqB;CA6BvF;AAsBD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,YAAY,GAAG,eAAe,GAAG,SAAS,CAsCrF"}
@@ -0,0 +1,229 @@
1
+ import { MathOps } from '@lagless/math';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import { PlacementKind } from '../types/feature-configs.js';
4
+ import { ShoreFeature } from './shore-feature.js';
5
+ import { GrassFeature } from './grass-feature.js';
6
+ import { RiverFeature } from './river-feature.js';
7
+ import { LakeFeature } from './lake-feature.js';
8
+ import { ShapeType } from '../types/geometry.js';
9
+ import { TerrainQuery } from '../core/terrain-query.js';
10
+ const MAX_ATTEMPTS_NORMAL = 500;
11
+ const MAX_ATTEMPTS_IMPORTANT = 5000;
12
+ const AREA_DENOMINATOR = 250000;
13
+ const DEFAULT_ORIENTATIONS = [0];
14
+ export class ObjectPlacementFeature {
15
+ static id = FeatureId.ObjectPlacement;
16
+ id = FeatureId.ObjectPlacement;
17
+ requires = [FeatureId.Shore, FeatureId.Grass];
18
+ generate(ctx, config) {
19
+ const objects = [];
20
+ const terrainQuery = new TerrainQuery({
21
+ shore: ctx.hasFeature(FeatureId.Shore) ? ctx.get(ShoreFeature) : undefined,
22
+ grass: ctx.hasFeature(FeatureId.Grass) ? ctx.get(GrassFeature) : undefined,
23
+ river: ctx.hasFeature(FeatureId.River) ? ctx.get(RiverFeature) : undefined,
24
+ lake: ctx.hasFeature(FeatureId.Lake) ? ctx.get(LakeFeature) : undefined,
25
+ });
26
+ for (const stage of config.stages) {
27
+ switch (stage.kind) {
28
+ case PlacementKind.Location:
29
+ placeLocation(ctx, config.registry, stage, objects, terrainQuery);
30
+ break;
31
+ case PlacementKind.Fixed:
32
+ placeFixed(ctx, config.registry, stage, objects, terrainQuery);
33
+ break;
34
+ case PlacementKind.Random:
35
+ placeRandom(ctx, config.registry, stage, objects, terrainQuery);
36
+ break;
37
+ case PlacementKind.Density:
38
+ placeDensity(ctx, config.registry, stage, objects, terrainQuery);
39
+ break;
40
+ }
41
+ }
42
+ return { objects };
43
+ }
44
+ }
45
+ function matchesZone(terrainQuery, x, y, zone) {
46
+ if (zone == null)
47
+ return true;
48
+ return terrainQuery.classify(x, y) === zone;
49
+ }
50
+ function matchesZoneCorners(terrainQuery, x, y, zone, bounds, scale) {
51
+ const hw = bounds.halfWidth * scale;
52
+ const hh = bounds.halfHeight * scale;
53
+ const cx = x + bounds.centerX * scale;
54
+ const cy = y + bounds.centerY * scale;
55
+ return terrainQuery.classify(cx - hw, cy - hh) === zone
56
+ && terrainQuery.classify(cx + hw, cy - hh) === zone
57
+ && terrainQuery.classify(cx - hw, cy + hh) === zone
58
+ && terrainQuery.classify(cx + hw, cy + hh) === zone;
59
+ }
60
+ export function computePlacementBounds(def) {
61
+ let minX = Infinity;
62
+ let minY = Infinity;
63
+ let maxX = -Infinity;
64
+ let maxY = -Infinity;
65
+ let count = 0;
66
+ for (const collider of def.colliders) {
67
+ if (collider.isSensor && !def.includeSensorsInBounds)
68
+ continue;
69
+ count++;
70
+ const ox = collider.offsetX ?? 0;
71
+ const oy = collider.offsetY ?? 0;
72
+ if (collider.shape.type === ShapeType.Circle) {
73
+ const r = collider.shape.radius;
74
+ minX = Math.min(minX, ox - r);
75
+ minY = Math.min(minY, oy - r);
76
+ maxX = Math.max(maxX, ox + r);
77
+ maxY = Math.max(maxY, oy + r);
78
+ }
79
+ else {
80
+ const hw = collider.shape.halfWidth;
81
+ const hh = collider.shape.halfHeight;
82
+ minX = Math.min(minX, ox - hw);
83
+ minY = Math.min(minY, oy - hh);
84
+ maxX = Math.max(maxX, ox + hw);
85
+ maxY = Math.max(maxY, oy + hh);
86
+ }
87
+ }
88
+ if (count === 0)
89
+ return undefined;
90
+ return {
91
+ halfWidth: (maxX - minX) / 2,
92
+ halfHeight: (maxY - minY) / 2,
93
+ centerX: (minX + maxX) / 2,
94
+ centerY: (minY + maxY) / 2,
95
+ };
96
+ }
97
+ function placeLocation(ctx, registry, stage, objects, terrainQuery) {
98
+ const def = registry.get(stage.typeId);
99
+ if (!def)
100
+ return;
101
+ const bounds = computePlacementBounds(def);
102
+ const maxAttempts = stage.maxAttempts ?? MAX_ATTEMPTS_IMPORTANT;
103
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
104
+ const angle = ctx.random.getFloat() * Math.PI * 2;
105
+ const dist = ctx.random.getFloat() * stage.rad;
106
+ const x = stage.pos.x + MathOps.cos(angle) * dist;
107
+ const y = stage.pos.y + MathOps.sin(angle) * dist;
108
+ const placed = tryPlace(ctx, def, x, y, objects, terrainQuery, bounds);
109
+ if (placed)
110
+ return;
111
+ }
112
+ if (stage.optional)
113
+ return;
114
+ tryPlace(ctx, def, stage.pos.x, stage.pos.y, objects, terrainQuery, bounds);
115
+ }
116
+ function placeFixed(ctx, registry, stage, objects, terrainQuery) {
117
+ const def = registry.get(stage.typeId);
118
+ if (!def)
119
+ return;
120
+ const bounds = computePlacementBounds(def);
121
+ const maxAttempts = stage.important ? MAX_ATTEMPTS_IMPORTANT : MAX_ATTEMPTS_NORMAL;
122
+ for (let i = 0; i < stage.count; i++) {
123
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
124
+ const x = ctx.random.getFloat() * ctx.width;
125
+ const y = ctx.random.getFloat() * ctx.height;
126
+ if (!matchesZone(terrainQuery, x, y, stage.terrainZone))
127
+ continue;
128
+ if (tryPlace(ctx, def, x, y, objects, terrainQuery, bounds, stage.terrainZone))
129
+ break;
130
+ }
131
+ }
132
+ }
133
+ function placeRandom(ctx, registry, stage, objects, terrainQuery) {
134
+ const chosen = chooseN(ctx, stage.spawns, stage.choose);
135
+ for (const typeId of chosen) {
136
+ const def = registry.get(typeId);
137
+ if (!def)
138
+ continue;
139
+ const bounds = computePlacementBounds(def);
140
+ for (let attempt = 0; attempt < MAX_ATTEMPTS_NORMAL; attempt++) {
141
+ const x = ctx.random.getFloat() * ctx.width;
142
+ const y = ctx.random.getFloat() * ctx.height;
143
+ if (!matchesZone(terrainQuery, x, y, stage.terrainZone))
144
+ continue;
145
+ if (tryPlace(ctx, def, x, y, objects, terrainQuery, bounds, stage.terrainZone))
146
+ break;
147
+ }
148
+ }
149
+ }
150
+ function placeDensity(ctx, registry, stage, objects, terrainQuery) {
151
+ const def = registry.get(stage.typeId);
152
+ if (!def)
153
+ return;
154
+ const bounds = computePlacementBounds(def);
155
+ const mapArea = ctx.width * ctx.height;
156
+ const count = Math.round(stage.density * (mapArea / AREA_DENOMINATOR));
157
+ for (let i = 0; i < count; i++) {
158
+ for (let attempt = 0; attempt < MAX_ATTEMPTS_NORMAL; attempt++) {
159
+ const x = ctx.random.getFloat() * ctx.width;
160
+ const y = ctx.random.getFloat() * ctx.height;
161
+ if (!matchesZone(terrainQuery, x, y, stage.terrainZone))
162
+ continue;
163
+ if (tryPlace(ctx, def, x, y, objects, terrainQuery, bounds, stage.terrainZone))
164
+ break;
165
+ }
166
+ }
167
+ }
168
+ function tryPlace(ctx, def, x, y, objects, terrainQuery, bounds, terrainZone) {
169
+ const scale = def.scaleRange[0] + ctx.random.getFloat() * (def.scaleRange[1] - def.scaleRange[0]);
170
+ const orientations = def.orientations ?? DEFAULT_ORIENTATIONS;
171
+ const ori = orientations[ctx.random.getRandomInt(0, orientations.length)];
172
+ if (bounds) {
173
+ const collisionX = x + bounds.centerX * scale;
174
+ const collisionY = y + bounds.centerY * scale;
175
+ const collisionShape = {
176
+ type: ShapeType.Cuboid,
177
+ halfWidth: bounds.halfWidth * scale,
178
+ halfHeight: bounds.halfHeight * scale,
179
+ };
180
+ if (ctx.collision.testShape(collisionShape, collisionX, collisionY, ori, 1)) {
181
+ return false;
182
+ }
183
+ // Corner terrain check: all 4 AABB corners must match the required zone
184
+ if (terrainZone != null && !matchesZoneCorners(terrainQuery, x, y, terrainZone, bounds, scale)) {
185
+ return false;
186
+ }
187
+ ctx.collision.addShape(objects.length, collisionShape, collisionX, collisionY, ori, 1);
188
+ }
189
+ const zone = terrainQuery.classify(x, y);
190
+ const children = [];
191
+ if (def.children) {
192
+ for (const childDef of def.children) {
193
+ const childX = x + childDef.offset.x * scale;
194
+ const childY = y + childDef.offset.y * scale;
195
+ const childOri = childDef.inheritOri ? ori + childDef.ori : childDef.ori;
196
+ children.push({
197
+ typeId: childDef.typeId,
198
+ posX: childX,
199
+ posY: childY,
200
+ rotation: childOri,
201
+ scale: childDef.scale * scale,
202
+ terrainZone: terrainQuery.classify(childX, childY),
203
+ children: [],
204
+ });
205
+ }
206
+ }
207
+ objects.push({
208
+ typeId: def.typeId,
209
+ posX: x,
210
+ posY: y,
211
+ rotation: ori,
212
+ scale,
213
+ terrainZone: zone,
214
+ children,
215
+ });
216
+ return true;
217
+ }
218
+ function chooseN(ctx, items, n) {
219
+ if (n >= items.length)
220
+ return [...items];
221
+ const available = [...items];
222
+ const result = [];
223
+ for (let i = 0; i < n; i++) {
224
+ const idx = ctx.random.getRandomInt(0, available.length);
225
+ result.push(available[idx]);
226
+ available.splice(idx, 1);
227
+ }
228
+ return result;
229
+ }
@@ -0,0 +1,10 @@
1
+ import type { IMapFeature, GenerationContext } from '../types/feature.js';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import type { PlacesConfig, PlacesOutput } from '../types/feature-configs.js';
4
+ export declare class PlacesFeature implements IMapFeature<PlacesConfig, PlacesOutput> {
5
+ static readonly id = FeatureId.Places;
6
+ readonly id = FeatureId.Places;
7
+ readonly requires: readonly FeatureId[];
8
+ generate(ctx: GenerationContext, config: PlacesConfig): PlacesOutput;
9
+ }
10
+ //# sourceMappingURL=places-feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"places-feature.d.ts","sourceRoot":"","sources":["../../../src/lib/features/places-feature.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,6BAA6B,CAAC;AAE9E,qBAAa,aAAc,YAAW,WAAW,CAAC,YAAY,EAAE,YAAY,CAAC;IAC3E,MAAM,CAAC,QAAQ,CAAC,EAAE,oBAAoB;IACtC,QAAQ,CAAC,EAAE,oBAAoB;IAC/B,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAM;IAE7C,QAAQ,CAAC,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,YAAY,GAAG,YAAY;CASrE"}
@@ -0,0 +1,14 @@
1
+ import { FeatureId } from '../types/feature.js';
2
+ export class PlacesFeature {
3
+ static id = FeatureId.Places;
4
+ id = FeatureId.Places;
5
+ requires = [];
6
+ generate(ctx, config) {
7
+ const places = config.places.map(p => ({
8
+ name: p.name,
9
+ x: p.pos.x * ctx.width,
10
+ y: p.pos.y * ctx.height,
11
+ }));
12
+ return { places };
13
+ }
14
+ }
@@ -0,0 +1,10 @@
1
+ import type { IMapFeature, GenerationContext } from '../types/feature.js';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import type { RiverConfig, RiverOutput } from '../types/feature-configs.js';
4
+ export declare class RiverFeature implements IMapFeature<RiverConfig, RiverOutput> {
5
+ static readonly id = FeatureId.River;
6
+ readonly id = FeatureId.River;
7
+ readonly requires: readonly FeatureId[];
8
+ generate(ctx: GenerationContext, config: RiverConfig): RiverOutput;
9
+ }
10
+ //# sourceMappingURL=river-feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"river-feature.d.ts","sourceRoot":"","sources":["../../../src/lib/features/river-feature.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAK5E,qBAAa,YAAa,YAAW,WAAW,CAAC,WAAW,EAAE,WAAW,CAAC;IACxE,MAAM,CAAC,QAAQ,CAAC,EAAE,mBAAmB;IACrC,QAAQ,CAAC,EAAE,mBAAmB;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAM;IAE7C,QAAQ,CAAC,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW;CAgCnE"}
@@ -0,0 +1,122 @@
1
+ import { MathOps } from '@lagless/math';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import { generateRiverPolygon } from '../math/river-polygon.js';
4
+ export class RiverFeature {
5
+ static id = FeatureId.River;
6
+ id = FeatureId.River;
7
+ requires = [];
8
+ generate(ctx, config) {
9
+ const widths = selectWeightedWidths(ctx, config.weights);
10
+ const rivers = [];
11
+ for (const waterWidth of widths) {
12
+ const splinePoints = generateRiverPoints(ctx, config.subdivisionPasses, config.masks);
13
+ if (splinePoints.length < 2)
14
+ continue;
15
+ const shoreWidth = Math.max(4, Math.min(8, waterWidth * 0.75));
16
+ const river = generateRiverPolygon({
17
+ splinePoints,
18
+ waterWidth,
19
+ shoreWidth,
20
+ looped: false,
21
+ mapWidth: ctx.width,
22
+ mapHeight: ctx.height,
23
+ });
24
+ rivers.push(river);
25
+ }
26
+ return {
27
+ rivers,
28
+ normalRivers: rivers,
29
+ };
30
+ }
31
+ }
32
+ function selectWeightedWidths(ctx, weights) {
33
+ if (weights.length === 0)
34
+ return [];
35
+ let totalWeight = 0;
36
+ for (const w of weights) {
37
+ totalWeight += w.weight;
38
+ }
39
+ let roll = ctx.random.getFloat() * totalWeight;
40
+ for (const w of weights) {
41
+ roll -= w.weight;
42
+ if (roll <= 0) {
43
+ return w.widths;
44
+ }
45
+ }
46
+ return weights[weights.length - 1].widths;
47
+ }
48
+ function generateRiverPoints(ctx, subdivisionPasses, masks) {
49
+ const { width, height, random } = ctx;
50
+ const edge = random.getRandomInt(0, 4);
51
+ let start;
52
+ let end;
53
+ switch (edge) {
54
+ case 0:
55
+ start = { x: 0, y: random.getFloat() * height };
56
+ end = { x: width, y: random.getFloat() * height };
57
+ break;
58
+ case 1:
59
+ start = { x: width, y: random.getFloat() * height };
60
+ end = { x: 0, y: random.getFloat() * height };
61
+ break;
62
+ case 2:
63
+ start = { x: random.getFloat() * width, y: 0 };
64
+ end = { x: random.getFloat() * width, y: height };
65
+ break;
66
+ default:
67
+ start = { x: random.getFloat() * width, y: height };
68
+ end = { x: random.getFloat() * width, y: 0 };
69
+ break;
70
+ }
71
+ let points = [start, end];
72
+ for (let pass = 0; pass < subdivisionPasses; pass++) {
73
+ const newPoints = [points[0]];
74
+ for (let i = 0; i < points.length - 1; i++) {
75
+ const a = points[i];
76
+ const b = points[i + 1];
77
+ const mx = (a.x + b.x) * 0.5;
78
+ const my = (a.y + b.y) * 0.5;
79
+ const dx = b.x - a.x;
80
+ const dy = b.y - a.y;
81
+ const dist = MathOps.sqrt(dx * dx + dy * dy);
82
+ const perpX = dist > 0 ? -dy / dist : 0;
83
+ const perpY = dist > 0 ? dx / dist : 0;
84
+ const offset = (random.getFloat() * 2 - 1) * (dist / 7);
85
+ const midpoint = {
86
+ x: mx + perpX * offset,
87
+ y: my + perpY * offset,
88
+ };
89
+ newPoints.push(midpoint);
90
+ newPoints.push(b);
91
+ }
92
+ points = newPoints;
93
+ }
94
+ if (masks.length > 0) {
95
+ points = filterMaskedPoints(points, masks);
96
+ }
97
+ return points;
98
+ }
99
+ function filterMaskedPoints(points, masks) {
100
+ if (points.length <= 2)
101
+ return points;
102
+ const filtered = [points[0]];
103
+ for (let i = 1; i < points.length - 1; i++) {
104
+ const pt = points[i];
105
+ let masked = false;
106
+ for (const mask of masks) {
107
+ const cx = mask.pos ? mask.pos.x : 0;
108
+ const cy = mask.pos ? mask.pos.y : 0;
109
+ const dx = pt.x - cx;
110
+ const dy = pt.y - cy;
111
+ if (dx * dx + dy * dy < mask.rad * mask.rad) {
112
+ masked = true;
113
+ break;
114
+ }
115
+ }
116
+ if (!masked) {
117
+ filtered.push(pt);
118
+ }
119
+ }
120
+ filtered.push(points[points.length - 1]);
121
+ return filtered;
122
+ }
@@ -0,0 +1,10 @@
1
+ import type { IMapFeature, GenerationContext } from '../types/feature.js';
2
+ import { FeatureId } from '../types/feature.js';
3
+ import type { ShoreConfig, ShoreOutput } from '../types/feature-configs.js';
4
+ export declare class ShoreFeature implements IMapFeature<ShoreConfig, ShoreOutput> {
5
+ static readonly id = FeatureId.Shore;
6
+ readonly id = FeatureId.Shore;
7
+ readonly requires: readonly FeatureId[];
8
+ generate(ctx: GenerationContext, config: ShoreConfig): ShoreOutput;
9
+ }
10
+ //# sourceMappingURL=shore-feature.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"shore-feature.d.ts","sourceRoot":"","sources":["../../../src/lib/features/shore-feature.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC1E,OAAO,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AAChD,OAAO,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,6BAA6B,CAAC;AAK5E,qBAAa,YAAa,YAAW,WAAW,CAAC,WAAW,EAAE,WAAW,CAAC;IACxE,MAAM,CAAC,QAAQ,CAAC,EAAE,mBAAmB;IACrC,QAAQ,CAAC,EAAE,mBAAmB;IAC9B,QAAQ,CAAC,QAAQ,EAAE,SAAS,SAAS,EAAE,CAAM;IAE7C,QAAQ,CAAC,GAAG,EAAE,iBAAiB,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW;CAqBnE"}
@@ -0,0 +1,20 @@
1
+ import { FeatureId } from '../types/feature.js';
2
+ import { generateJaggedAabbPoints } from '../math/jagged-aabb.js';
3
+ import { computePolygonBounds } from '../math/polygon-utils.js';
4
+ export class ShoreFeature {
5
+ static id = FeatureId.Shore;
6
+ id = FeatureId.Shore;
7
+ requires = [];
8
+ generate(ctx, config) {
9
+ const shoreAabb = {
10
+ min: { x: config.inset, y: config.inset },
11
+ max: { x: ctx.width - config.inset, y: ctx.height - config.inset },
12
+ };
13
+ const points = generateJaggedAabbPoints(shoreAabb, config.divisions, config.divisions, config.variation, ctx.random);
14
+ const bounds = computePolygonBounds(points);
15
+ return {
16
+ polygon: { points, count: points.length },
17
+ bounds,
18
+ };
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ import type { ReadonlyVec2 } from '../types/geometry.js';
2
+ export declare function catmullRom(t: number, p0: number, p1: number, p2: number, p3: number): number;
3
+ export declare function catmullRomDerivative(t: number, p0: number, p1: number, p2: number, p3: number): number;
4
+ export interface ControlPointResult {
5
+ pt: number;
6
+ p0: ReadonlyVec2;
7
+ p1: ReadonlyVec2;
8
+ p2: ReadonlyVec2;
9
+ p3: ReadonlyVec2;
10
+ }
11
+ export declare function getControlPoints(t: number, points: readonly ReadonlyVec2[], looped: boolean): ControlPointResult;
12
+ //# sourceMappingURL=catmull-rom.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"catmull-rom.d.ts","sourceRoot":"","sources":["../../../src/lib/math/catmull-rom.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAO5F;AAED,wBAAgB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,MAAM,CAMtG;AAED,MAAM,WAAW,kBAAkB;IACjC,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;IACjB,EAAE,EAAE,YAAY,CAAC;CAClB;AAED,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,YAAY,EAAE,EAAE,MAAM,EAAE,OAAO,GAAG,kBAAkB,CA+BhH"}
@@ -0,0 +1,42 @@
1
+ export function catmullRom(t, p0, p1, p2, p3) {
2
+ return 0.5 * (2.0 * p1 +
3
+ t * (-p0 + p2) +
4
+ t * t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) +
5
+ t * t * t * (-p0 + 3.0 * p1 - 3.0 * p2 + p3));
6
+ }
7
+ export function catmullRomDerivative(t, p0, p1, p2, p3) {
8
+ return 0.5 * (-p0 + p2 +
9
+ 2.0 * t * (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) +
10
+ 3.0 * t * t * (-p0 + 3.0 * p1 - 3.0 * p2 + p3));
11
+ }
12
+ export function getControlPoints(t, points, looped) {
13
+ const count = points.length;
14
+ let i;
15
+ let i0;
16
+ let i1;
17
+ let i2;
18
+ let i3;
19
+ if (looped) {
20
+ t = ((t % 1.0) + 1.0) % 1.0; // fmod that handles negatives
21
+ i = ~~(t * (count - 1));
22
+ i1 = i;
23
+ i2 = (i1 + 1) % (count - 1);
24
+ i0 = i1 > 0 ? i1 - 1 : count - 2;
25
+ i3 = (i2 + 1) % (count - 1);
26
+ }
27
+ else {
28
+ t = Math.max(0, Math.min(1, t));
29
+ i = ~~(t * (count - 1));
30
+ i1 = i === count - 1 ? i - 1 : i;
31
+ i2 = i1 + 1;
32
+ i0 = i1 > 0 ? i1 - 1 : i1;
33
+ i3 = i2 < count - 1 ? i2 + 1 : i2;
34
+ }
35
+ return {
36
+ pt: t * (count - 1) - i1,
37
+ p0: points[i0],
38
+ p1: points[i1],
39
+ p2: points[i2],
40
+ p3: points[i3],
41
+ };
42
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Test circle vs circle overlap.
3
+ */
4
+ export declare function testCircleCircle(ax: number, ay: number, ar: number, bx: number, by: number, br: number): boolean;
5
+ /**
6
+ * Test circle vs AABB overlap.
7
+ * AABB defined by (minX, minY) to (maxX, maxY).
8
+ */
9
+ export declare function testCircleAabb(cx: number, cy: number, cr: number, minX: number, minY: number, maxX: number, maxY: number): boolean;
10
+ /**
11
+ * Test AABB vs AABB overlap.
12
+ */
13
+ export declare function testAabbAabb(aMinX: number, aMinY: number, aMaxX: number, aMaxY: number, bMinX: number, bMinY: number, bMaxX: number, bMaxY: number): boolean;
14
+ //# sourceMappingURL=collision-test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collision-test.d.ts","sourceRoot":"","sources":["../../../src/lib/math/collision-test.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAClC,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GACjC,OAAO,CAMT;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAClC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GACrD,OAAO,CAQT;AAED;;GAEG;AACH,wBAAgB,YAAY,CAC1B,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAC1D,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GACzD,OAAO,CAGT"}
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Test circle vs circle overlap.
3
+ */
4
+ export function testCircleCircle(ax, ay, ar, bx, by, br) {
5
+ const dx = bx - ax;
6
+ const dy = by - ay;
7
+ const distSq = dx * dx + dy * dy;
8
+ const rSum = ar + br;
9
+ return distSq <= rSum * rSum;
10
+ }
11
+ /**
12
+ * Test circle vs AABB overlap.
13
+ * AABB defined by (minX, minY) to (maxX, maxY).
14
+ */
15
+ export function testCircleAabb(cx, cy, cr, minX, minY, maxX, maxY) {
16
+ // Find closest point on AABB to circle center
17
+ const closestX = Math.max(minX, Math.min(cx, maxX));
18
+ const closestY = Math.max(minY, Math.min(cy, maxY));
19
+ const dx = cx - closestX;
20
+ const dy = cy - closestY;
21
+ return (dx * dx + dy * dy) <= cr * cr;
22
+ }
23
+ /**
24
+ * Test AABB vs AABB overlap.
25
+ */
26
+ export function testAabbAabb(aMinX, aMinY, aMaxX, aMaxY, bMinX, bMinY, bMaxX, bMaxY) {
27
+ return aMinX <= bMaxX && aMaxX >= bMinX &&
28
+ aMinY <= bMaxY && aMaxY >= bMinY;
29
+ }
@@ -0,0 +1,12 @@
1
+ import type { IVector2Like } from '@lagless/math';
2
+ import type { AABB } from '../types/geometry.js';
3
+ import type { ISeededRandom } from '../types/prng-interface.js';
4
+ /**
5
+ * Generates jagged polygon points around an AABB border.
6
+ * Counter-clockwise winding: bottom→right→top→left.
7
+ * Corner points are NOT offset. Intermediate points are offset by random variation.
8
+ *
9
+ * Port of survev/shared/utils/terrainGen.ts:15-56.
10
+ */
11
+ export declare function generateJaggedAabbPoints(aabb: AABB, divisionsX: number, divisionsY: number, variation: number, random: ISeededRandom): IVector2Like[];
12
+ //# sourceMappingURL=jagged-aabb.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jagged-aabb.d.ts","sourceRoot":"","sources":["../../../src/lib/math/jagged-aabb.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;GAMG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,IAAI,EACV,UAAU,EAAE,MAAM,EAClB,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,MAAM,EAAE,aAAa,GACpB,YAAY,EAAE,CA8ChB"}