@jgengine/core 0.5.0 → 0.7.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 (244) hide show
  1. package/CHANGELOG.md +191 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +5 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +291 -5
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +53 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +219 -0
  116. package/dist/physics/physicsWorld.js +749 -0
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +66 -2
  128. package/dist/runtime/gameContext.js +157 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/assetCatalog.d.ts +13 -0
  134. package/dist/scene/autoTarget.d.ts +15 -0
  135. package/dist/scene/autoTarget.js +54 -0
  136. package/dist/scene/behaviors.d.ts +13 -1
  137. package/dist/scene/behaviors.js +3 -0
  138. package/dist/scene/captureCheck.d.ts +7 -0
  139. package/dist/scene/captureCheck.js +11 -0
  140. package/dist/scene/entityStore.d.ts +7 -1
  141. package/dist/scene/entityStore.js +15 -1
  142. package/dist/scene/form.d.ts +40 -0
  143. package/dist/scene/form.js +56 -0
  144. package/dist/scene/mount.d.ts +62 -0
  145. package/dist/scene/mount.js +107 -0
  146. package/dist/scene/possession.d.ts +30 -0
  147. package/dist/scene/possession.js +51 -0
  148. package/dist/scene/roster.d.ts +25 -0
  149. package/dist/scene/roster.js +57 -0
  150. package/dist/scene/selection.d.ts +30 -0
  151. package/dist/scene/selection.js +38 -0
  152. package/dist/scene/stationClaim.d.ts +43 -0
  153. package/dist/scene/stationClaim.js +109 -0
  154. package/dist/sensor/frustumSensor.d.ts +60 -0
  155. package/dist/sensor/frustumSensor.js +104 -0
  156. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  157. package/dist/sensor/hiddenStateProbe.js +40 -0
  158. package/dist/sensor/recordingBuffer.d.ts +29 -0
  159. package/dist/sensor/recordingBuffer.js +61 -0
  160. package/dist/sensor/revealQuery.d.ts +22 -0
  161. package/dist/sensor/revealQuery.js +36 -0
  162. package/dist/session/contestedChannel.d.ts +36 -0
  163. package/dist/session/contestedChannel.js +100 -0
  164. package/dist/session/extraction.d.ts +54 -0
  165. package/dist/session/extraction.js +108 -0
  166. package/dist/session/ring.d.ts +36 -0
  167. package/dist/session/ring.js +88 -0
  168. package/dist/session/roundState.d.ts +55 -0
  169. package/dist/session/roundState.js +129 -0
  170. package/dist/stats/accumulatorMeter.d.ts +31 -0
  171. package/dist/stats/accumulatorMeter.js +81 -0
  172. package/dist/stats/eventMeter.d.ts +34 -0
  173. package/dist/stats/eventMeter.js +76 -0
  174. package/dist/stats/rollCheck.d.ts +15 -0
  175. package/dist/stats/rollCheck.js +15 -0
  176. package/dist/survival/decayMeter.d.ts +59 -0
  177. package/dist/survival/decayMeter.js +95 -0
  178. package/dist/survival/moodle.d.ts +48 -0
  179. package/dist/survival/moodle.js +91 -0
  180. package/dist/survival/regionHealth.d.ts +72 -0
  181. package/dist/survival/regionHealth.js +129 -0
  182. package/dist/tactics/predictiveQuery.d.ts +34 -0
  183. package/dist/tactics/predictiveQuery.js +44 -0
  184. package/dist/tactics/snapshot.d.ts +18 -0
  185. package/dist/tactics/snapshot.js +65 -0
  186. package/dist/tactics/surface.d.ts +52 -0
  187. package/dist/tactics/surface.js +135 -0
  188. package/dist/tactics/tacticalGrid.d.ts +57 -0
  189. package/dist/tactics/tacticalGrid.js +244 -0
  190. package/dist/time/beatClock.d.ts +40 -0
  191. package/dist/time/beatClock.js +73 -0
  192. package/dist/time/simClock.d.ts +62 -0
  193. package/dist/time/simClock.js +132 -0
  194. package/dist/turn/commit.d.ts +27 -0
  195. package/dist/turn/commit.js +72 -0
  196. package/dist/turn/turnLoop.d.ts +58 -0
  197. package/dist/turn/turnLoop.js +216 -0
  198. package/dist/world/buildPermissions.d.ts +47 -0
  199. package/dist/world/buildPermissions.js +134 -0
  200. package/dist/world/buildingIndex.d.ts +16 -0
  201. package/dist/world/buildingIndex.js +63 -0
  202. package/dist/world/buildings.d.ts +98 -0
  203. package/dist/world/buildings.js +320 -0
  204. package/dist/world/carve.d.ts +105 -0
  205. package/dist/world/carve.js +175 -0
  206. package/dist/world/connectors.d.ts +40 -0
  207. package/dist/world/connectors.js +67 -0
  208. package/dist/world/envField.d.ts +71 -0
  209. package/dist/world/envField.js +96 -0
  210. package/dist/world/features.d.ts +105 -1
  211. package/dist/world/features.js +95 -0
  212. package/dist/world/fog.d.ts +40 -0
  213. package/dist/world/fog.js +137 -0
  214. package/dist/world/markers.d.ts +52 -0
  215. package/dist/world/markers.js +96 -0
  216. package/dist/world/minimap.d.ts +42 -0
  217. package/dist/world/minimap.js +72 -0
  218. package/dist/world/placedStructureStore.d.ts +39 -0
  219. package/dist/world/placedStructureStore.js +95 -0
  220. package/dist/world/placementController.d.ts +48 -0
  221. package/dist/world/placementController.js +97 -0
  222. package/dist/world/realm.d.ts +61 -0
  223. package/dist/world/realm.js +66 -0
  224. package/dist/world/regions.d.ts +66 -0
  225. package/dist/world/regions.js +158 -0
  226. package/dist/world/scatter.d.ts +24 -0
  227. package/dist/world/scatter.js +126 -0
  228. package/dist/world/scatterItems.d.ts +38 -0
  229. package/dist/world/scatterItems.js +85 -0
  230. package/dist/world/support.d.ts +27 -0
  231. package/dist/world/support.js +75 -0
  232. package/dist/world/terraform.d.ts +54 -0
  233. package/dist/world/terraform.js +186 -0
  234. package/dist/world/terrain.d.ts +54 -0
  235. package/dist/world/terrain.js +181 -0
  236. package/dist/world/walls.d.ts +61 -0
  237. package/dist/world/walls.js +221 -0
  238. package/dist/world/water.d.ts +27 -0
  239. package/dist/world/water.js +125 -0
  240. package/dist/world/weather.d.ts +92 -0
  241. package/dist/world/weather.js +169 -0
  242. package/dist/world/wind.d.ts +17 -0
  243. package/dist/world/wind.js +40 -0
  244. package/package.json +1 -1
@@ -0,0 +1,97 @@
1
+ import { footprintAabb, snapToGrid } from "./geometry.js";
2
+ import { validatePlacement } from "./placement.js";
3
+ const SNAP_ORDER = ["grid", "free", "surface"];
4
+ export function quarterTurnsToRotationY(quarterTurns) {
5
+ const turns = ((quarterTurns % 4) + 4) % 4;
6
+ return turns === 0 ? 0 : -turns * (Math.PI / 2);
7
+ }
8
+ export function createPlacementController(config) {
9
+ let footprint = config.footprint;
10
+ let rules = config.rules ?? {};
11
+ let snapMode = config.snapMode ?? "grid";
12
+ let grid = config.grid ?? rules.snap ?? 1;
13
+ let quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
14
+ let lastHit = null;
15
+ let preview = null;
16
+ function snapCenter(raw) {
17
+ if (snapMode === "grid")
18
+ return snapToGrid(raw, grid);
19
+ return raw;
20
+ }
21
+ function compute(hit) {
22
+ const raw = [hit.point[0], hit.point[2]];
23
+ const center = snapCenter(raw);
24
+ const rulesWithoutSnap = snapMode === "grid" ? { ...rules, snap: undefined } : rules;
25
+ const result = validatePlacement({ center, footprint, quarterTurns }, rulesWithoutSnap);
26
+ const aabb = result.status === "ok" ? result.aabb : footprintAabb(center, footprint, quarterTurns);
27
+ const next = {
28
+ center: result.status === "ok" ? result.center : center,
29
+ y: hit.point[1],
30
+ quarterTurns,
31
+ footprint,
32
+ aabb,
33
+ valid: result.status === "ok",
34
+ snapMode,
35
+ normal: hit.normal,
36
+ ...(result.status === "rejected" ? { reason: result.reason } : {}),
37
+ };
38
+ preview = next;
39
+ return next;
40
+ }
41
+ return {
42
+ hover(hit) {
43
+ lastHit = hit;
44
+ return compute(hit);
45
+ },
46
+ current() {
47
+ return preview;
48
+ },
49
+ rotate(steps = 1) {
50
+ quarterTurns = ((quarterTurns + steps) % 4 + 4) % 4;
51
+ return lastHit === null ? null : compute(lastHit);
52
+ },
53
+ setSnapMode(mode) {
54
+ snapMode = mode;
55
+ return lastHit === null ? null : compute(lastHit);
56
+ },
57
+ cycleSnapMode() {
58
+ const index = SNAP_ORDER.indexOf(snapMode);
59
+ snapMode = SNAP_ORDER[(index + 1) % SNAP_ORDER.length];
60
+ if (lastHit !== null)
61
+ compute(lastHit);
62
+ return snapMode;
63
+ },
64
+ setFootprint(next) {
65
+ footprint = next;
66
+ if (lastHit !== null)
67
+ compute(lastHit);
68
+ },
69
+ setRules(next) {
70
+ rules = next;
71
+ if (lastHit !== null)
72
+ compute(lastHit);
73
+ },
74
+ setGrid(next) {
75
+ grid = next;
76
+ if (lastHit !== null)
77
+ compute(lastHit);
78
+ },
79
+ commit() {
80
+ if (preview === null || !preview.valid)
81
+ return null;
82
+ return {
83
+ center: preview.center,
84
+ y: preview.y,
85
+ rotationY: quarterTurnsToRotationY(quarterTurns),
86
+ quarterTurns,
87
+ footprint: preview.footprint,
88
+ aabb: preview.aabb,
89
+ };
90
+ },
91
+ reset() {
92
+ lastHit = null;
93
+ preview = null;
94
+ quarterTurns = ((config.quarterTurns ?? 0) % 4 + 4) % 4;
95
+ },
96
+ };
97
+ }
@@ -0,0 +1,61 @@
1
+ import { type EnvironmentField, type EnvironmentFieldConfig } from "./envField.js";
2
+ import type { WeatherState } from "./weather.js";
3
+ /** The recomposable environment parameters a realm card can override. */
4
+ export interface RealmEnvironmentParams {
5
+ baseTemperature: number;
6
+ nightDrop: number;
7
+ altitudeLapse: number;
8
+ /** Game-seconds per day (a "long night" minor card shortens the day cycle). */
9
+ dayLength: number;
10
+ /** Ambient rain intensity 0..1. */
11
+ rain: number;
12
+ /** Ambient light floor 0..1 (moonlight). */
13
+ ambientFloor: number;
14
+ }
15
+ export declare const DEFAULT_REALM_ENVIRONMENT: RealmEnvironmentParams;
16
+ /** Spawn-table edits a card layers on: replace weights, add/scale, or remove ids. */
17
+ export interface SpawnTableOverride {
18
+ set?: Record<string, number>;
19
+ add?: Record<string, number>;
20
+ scale?: Record<string, number>;
21
+ remove?: readonly string[];
22
+ }
23
+ export interface RealmCard {
24
+ id: string;
25
+ /** A major card sets the biome base; minors layer weather/day-length/aesthetic on top. */
26
+ kind: "major" | "minor";
27
+ /** Environment param overrides (last write wins across the ordered deck). */
28
+ environment?: Partial<RealmEnvironmentParams>;
29
+ /** Weather state overrides (kind/intensity/wind). */
30
+ weather?: Partial<WeatherState>;
31
+ /** Spawn-table edits applied in deck order. */
32
+ spawn?: SpawnTableOverride;
33
+ /** Opaque aesthetic tag (skybox, palette) the renderer maps — engine never inspects it. */
34
+ aesthetic?: string;
35
+ }
36
+ export interface RealmBase {
37
+ environment?: Partial<RealmEnvironmentParams>;
38
+ weather?: WeatherState;
39
+ spawn?: Record<string, number>;
40
+ }
41
+ export interface ComposedRealm {
42
+ params: RealmEnvironmentParams;
43
+ weather: WeatherState;
44
+ spawnTable: Record<string, number>;
45
+ aesthetics: readonly string[];
46
+ cards: readonly RealmCard[];
47
+ /**
48
+ * Build an environment field (#91) from the composed params, wired to the composed
49
+ * weather's rain intensity. Extra config (terrain, occluders, heat sources) merges on top.
50
+ */
51
+ environmentField(extra?: Omit<EnvironmentFieldConfig, "baseTemperature" | "nightDrop" | "altitudeLapse" | "dayLength" | "rain" | "ambientFloor">): EnvironmentField;
52
+ }
53
+ /**
54
+ * Assemble a played realm instance at runtime from a deck of modifier cards — the
55
+ * Nightingale "realm card" model. A major card is the biome base; minor cards layer
56
+ * weather, day length, and spawn edits. The result recomposes both the environment
57
+ * (into a sampleable field via `environmentField()`) and the spawn table, and it depends
58
+ * on the weather hooks in this group (#92) to turn its `weather` into gameplay modifiers.
59
+ * Cards apply in array order; sort your deck (majors first) before composing.
60
+ */
61
+ export declare function composeRealm(base: RealmBase, cards: readonly RealmCard[]): ComposedRealm;
@@ -0,0 +1,66 @@
1
+ import { createEnvironmentField } from "./envField.js";
2
+ export const DEFAULT_REALM_ENVIRONMENT = {
3
+ baseTemperature: 20,
4
+ nightDrop: 12,
5
+ altitudeLapse: 0,
6
+ dayLength: 24 * 3600,
7
+ rain: 0,
8
+ ambientFloor: 0.05,
9
+ };
10
+ function applySpawn(table, override) {
11
+ if (override === undefined)
12
+ return;
13
+ if (override.set !== undefined)
14
+ for (const [id, weight] of Object.entries(override.set))
15
+ table[id] = weight;
16
+ if (override.add !== undefined)
17
+ for (const [id, weight] of Object.entries(override.add))
18
+ table[id] = (table[id] ?? 0) + weight;
19
+ if (override.scale !== undefined)
20
+ for (const [id, factor] of Object.entries(override.scale))
21
+ table[id] = (table[id] ?? 0) * factor;
22
+ if (override.remove !== undefined)
23
+ for (const id of override.remove)
24
+ delete table[id];
25
+ }
26
+ /**
27
+ * Assemble a played realm instance at runtime from a deck of modifier cards — the
28
+ * Nightingale "realm card" model. A major card is the biome base; minor cards layer
29
+ * weather, day length, and spawn edits. The result recomposes both the environment
30
+ * (into a sampleable field via `environmentField()`) and the spawn table, and it depends
31
+ * on the weather hooks in this group (#92) to turn its `weather` into gameplay modifiers.
32
+ * Cards apply in array order; sort your deck (majors first) before composing.
33
+ */
34
+ export function composeRealm(base, cards) {
35
+ const params = { ...DEFAULT_REALM_ENVIRONMENT, ...base.environment };
36
+ let weather = base.weather ?? { kind: "clear", intensity: 0 };
37
+ const spawnTable = { ...base.spawn };
38
+ const aesthetics = [];
39
+ for (const card of cards) {
40
+ if (card.environment !== undefined)
41
+ Object.assign(params, card.environment);
42
+ if (card.weather !== undefined)
43
+ weather = { ...weather, ...card.weather };
44
+ applySpawn(spawnTable, card.spawn);
45
+ if (card.aesthetic !== undefined)
46
+ aesthetics.push(card.aesthetic);
47
+ }
48
+ return {
49
+ params,
50
+ weather,
51
+ spawnTable,
52
+ aesthetics,
53
+ cards,
54
+ environmentField(extra) {
55
+ return createEnvironmentField({
56
+ ...extra,
57
+ baseTemperature: params.baseTemperature,
58
+ nightDrop: params.nightDrop,
59
+ altitudeLapse: params.altitudeLapse,
60
+ dayLength: params.dayLength,
61
+ rain: weather.kind === "clear" ? 0 : params.rain,
62
+ ambientFloor: params.ambientFloor,
63
+ });
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,66 @@
1
+ import type { WorldBounds } from "./features.js";
2
+ import { type TerrainField } from "./terrain.js";
3
+ export type Rgb = readonly [number, number, number];
4
+ export type ColorInput = string | Rgb;
5
+ export interface RegionHeight {
6
+ baseHeight: number;
7
+ amplitude: number;
8
+ frequency: number;
9
+ octaves?: number;
10
+ lacunarity?: number;
11
+ persistence?: number;
12
+ ridged?: boolean;
13
+ }
14
+ /**
15
+ * A content-agnostic region. The engine blends `height` and the visual/physical
16
+ * knobs (`tint`, `water`, `fog`, `speedMultiplier`, …) across region borders and
17
+ * selects the dominant region by nearest center in `selector` space. `data` is an
18
+ * opaque payload the engine never inspects — games hang their own content off it
19
+ * (spawn tables, prop palettes, structure ids, names, …).
20
+ */
21
+ export interface RegionDef<T = unknown> {
22
+ id: string;
23
+ selector: readonly number[];
24
+ height: RegionHeight;
25
+ tint?: ColorInput;
26
+ steepTint?: ColorInput;
27
+ water?: ColorInput;
28
+ fog?: ColorInput;
29
+ fogDensity?: number;
30
+ speedMultiplier?: number;
31
+ data?: T;
32
+ }
33
+ export interface RegionSample<T = unknown> {
34
+ region: RegionDef<T>;
35
+ weight: number;
36
+ selector: readonly number[];
37
+ tint: Rgb;
38
+ steepTint: Rgb;
39
+ water: Rgb | null;
40
+ fog: Rgb | null;
41
+ fogDensity: number;
42
+ speedMultiplier: number;
43
+ data: T | undefined;
44
+ }
45
+ export interface RegionField<T = unknown> extends TerrainField {
46
+ sampleRegion(x: number, z: number): RegionSample<T>;
47
+ readonly regions: readonly RegionDef<T>[];
48
+ readonly seaLevel: number;
49
+ readonly seed: number;
50
+ }
51
+ export interface RegionFieldConfig<T = unknown> {
52
+ regions: readonly RegionDef<T>[];
53
+ seed?: number;
54
+ bounds?: WorldBounds;
55
+ seaLevel?: number;
56
+ /** Noise frequency per selector axis (defaults to 0.004 for every axis). */
57
+ selectorFrequencies?: readonly number[];
58
+ /** Per-axis distance weight when picking the nearest region (defaults to 1). */
59
+ axisWeights?: readonly number[];
60
+ /** Border sharpness; higher = tighter blends. */
61
+ sharpness?: number;
62
+ /** Domain-warp distance applied to selector sampling (0 disables). */
63
+ warp?: number;
64
+ }
65
+ export declare function createRegionField<T = unknown>(config: RegionFieldConfig<T>): RegionField<T>;
66
+ export declare function isRegionField(field: TerrainField): field is RegionField;
@@ -0,0 +1,158 @@
1
+ import { fractalNoise, withNormal, } from "./terrain.js";
2
+ function toRgb(color, fallback) {
3
+ if (color === undefined)
4
+ return fallback;
5
+ if (typeof color !== "string")
6
+ return color;
7
+ const value = color.startsWith("#") ? color.slice(1) : color;
8
+ const int = Number.parseInt(value, 16);
9
+ return [((int >> 16) & 255) / 255, ((int >> 8) & 255) / 255, (int & 255) / 255];
10
+ }
11
+ function clamp01(value) {
12
+ return value < 0 ? 0 : value > 1 ? 1 : value;
13
+ }
14
+ function selectorNoise(x, z, seed, frequency) {
15
+ const config = { seed, frequency, octaves: 3, lacunarity: 2, persistence: 0.5, ridged: false };
16
+ return clamp01(0.5 + fractalNoise(x, z, config) * 0.9);
17
+ }
18
+ const DEFAULT_SELECTOR_FREQUENCY = 0.004;
19
+ const DEFAULT_SHARPNESS = 6;
20
+ export function createRegionField(config) {
21
+ if (config.regions.length === 0)
22
+ throw new Error("createRegionField: regions must be non-empty");
23
+ const seed = config.seed ?? 1337;
24
+ const regions = config.regions;
25
+ const seaLevel = config.seaLevel ?? 0;
26
+ const axes = regions[0].selector.length;
27
+ const sharpness = config.sharpness ?? DEFAULT_SHARPNESS;
28
+ const warp = config.warp ?? 40;
29
+ const heightConfigs = regions.map((region) => ({
30
+ seed: seed ^ hashId(region.id),
31
+ frequency: region.height.frequency,
32
+ octaves: region.height.octaves ?? 4,
33
+ lacunarity: region.height.lacunarity ?? 2,
34
+ persistence: region.height.persistence ?? 0.5,
35
+ ridged: region.height.ridged ?? false,
36
+ }));
37
+ const tints = regions.map((region) => toRgb(region.tint, [0.4, 0.42, 0.38]));
38
+ const steeps = regions.map((region, index) => toRgb(region.steepTint, tints[index]));
39
+ const waters = regions.map((region) => (region.water !== undefined ? toRgb(region.water, [0, 0, 0]) : null));
40
+ const fogs = regions.map((region) => (region.fog !== undefined ? toRgb(region.fog, [0, 0, 0]) : null));
41
+ const axisSeeds = Array.from({ length: axes }, (_, axis) => seed ^ (0x1111 * (axis + 1)));
42
+ const axisFrequencies = Array.from({ length: axes }, (_, axis) => config.selectorFrequencies?.[axis] ?? DEFAULT_SELECTOR_FREQUENCY);
43
+ const axisWeights = Array.from({ length: axes }, (_, axis) => config.axisWeights?.[axis] ?? 1);
44
+ const warpConfig = { seed: seed ^ 0x9e3779b1, frequency: 0.006, octaves: 2, lacunarity: 2, persistence: 0.5, ridged: false };
45
+ const sampleSelector = (x, z) => {
46
+ const wx = warp === 0 ? x : x + fractalNoise(x, z, warpConfig) * warp;
47
+ const wz = warp === 0 ? z : z + fractalNoise(z, x, warpConfig) * warp;
48
+ const values = new Array(axes);
49
+ for (let axis = 0; axis < axes; axis += 1) {
50
+ values[axis] = selectorNoise(wx, wz, axisSeeds[axis], axisFrequencies[axis]);
51
+ }
52
+ return values;
53
+ };
54
+ const weightsFor = (selector) => {
55
+ const raw = new Array(regions.length);
56
+ let total = 0;
57
+ for (let index = 0; index < regions.length; index += 1) {
58
+ const center = regions[index].selector;
59
+ let distanceSq = 0;
60
+ for (let axis = 0; axis < axes; axis += 1) {
61
+ const delta = (selector[axis] - center[axis]) * axisWeights[axis];
62
+ distanceSq += delta * delta;
63
+ }
64
+ const weight = Math.exp(-distanceSq * sharpness * regions.length);
65
+ raw[index] = weight;
66
+ total += weight;
67
+ }
68
+ if (total > 0)
69
+ for (let index = 0; index < raw.length; index += 1)
70
+ raw[index] /= total;
71
+ return raw;
72
+ };
73
+ const regionHeight = (index, x, z) => regions[index].height.baseHeight + regions[index].height.amplitude * fractalNoise(x, z, heightConfigs[index]);
74
+ const sampleHeight = (x, z) => {
75
+ const weights = weightsFor(sampleSelector(x, z));
76
+ let height = 0;
77
+ for (let index = 0; index < weights.length; index += 1) {
78
+ if (weights[index] < 0.01)
79
+ continue;
80
+ height += weights[index] * regionHeight(index, x, z);
81
+ }
82
+ return height;
83
+ };
84
+ const sampleRegion = (x, z) => {
85
+ const selector = sampleSelector(x, z);
86
+ const weights = weightsFor(selector);
87
+ let dominant = 0;
88
+ const tint = [0, 0, 0];
89
+ const steep = [0, 0, 0];
90
+ const water = [0, 0, 0];
91
+ let waterWeight = 0;
92
+ const fog = [0, 0, 0];
93
+ let fogWeight = 0;
94
+ let fogDensity = 0;
95
+ let speed = 0;
96
+ let speedWeight = 0;
97
+ for (let index = 0; index < weights.length; index += 1) {
98
+ const weight = weights[index];
99
+ if (weight > weights[dominant])
100
+ dominant = index;
101
+ if (weight < 0.01)
102
+ continue;
103
+ for (let channel = 0; channel < 3; channel += 1) {
104
+ tint[channel] += tints[index][channel] * weight;
105
+ steep[channel] += steeps[index][channel] * weight;
106
+ }
107
+ const w = waters[index];
108
+ if (w !== null) {
109
+ for (let channel = 0; channel < 3; channel += 1)
110
+ water[channel] += w[channel] * weight;
111
+ waterWeight += weight;
112
+ }
113
+ const f = fogs[index];
114
+ if (f !== null) {
115
+ for (let channel = 0; channel < 3; channel += 1)
116
+ fog[channel] += f[channel] * weight;
117
+ fogWeight += weight;
118
+ fogDensity += (regions[index].fogDensity ?? 0) * weight;
119
+ }
120
+ speed += (regions[index].speedMultiplier ?? 1) * weight;
121
+ speedWeight += weight;
122
+ }
123
+ const normalize = (rgb, weight) => weight > 0 ? [rgb[0] / weight, rgb[1] / weight, rgb[2] / weight] : rgb;
124
+ return {
125
+ region: regions[dominant],
126
+ weight: weights[dominant],
127
+ selector,
128
+ tint,
129
+ steepTint: steep,
130
+ water: waterWeight > 0.05 ? normalize(water, waterWeight) : null,
131
+ fog: fogWeight > 0.05 ? normalize(fog, fogWeight) : null,
132
+ fogDensity: fogWeight > 0.05 ? fogDensity / fogWeight : 0,
133
+ speedMultiplier: speedWeight > 0 ? speed / speedWeight : 1,
134
+ data: regions[dominant].data,
135
+ };
136
+ };
137
+ return {
138
+ sampleHeight,
139
+ sampleNormal: withNormal(sampleHeight),
140
+ sampleRegion,
141
+ regions,
142
+ seaLevel,
143
+ seed,
144
+ waterLevel: seaLevel,
145
+ bounds: config.bounds,
146
+ };
147
+ }
148
+ function hashId(id) {
149
+ let hash = 2166136261;
150
+ for (let index = 0; index < id.length; index += 1) {
151
+ hash ^= id.charCodeAt(index);
152
+ hash = Math.imul(hash, 16777619);
153
+ }
154
+ return hash | 0;
155
+ }
156
+ export function isRegionField(field) {
157
+ return "sampleRegion" in field;
158
+ }
@@ -0,0 +1,24 @@
1
+ import { type Aabb, type Vec2 } from "./geometry.js";
2
+ export interface ScatterArea {
3
+ w: number;
4
+ d: number;
5
+ center?: Vec2;
6
+ }
7
+ export interface ScatterConfig {
8
+ area: ScatterArea | Aabb;
9
+ count?: number;
10
+ density?: number;
11
+ seed?: string | number;
12
+ minDistance?: number;
13
+ avoid?: readonly Aabb[];
14
+ avoidMargin?: number;
15
+ jitter?: number;
16
+ maxAttempts?: number;
17
+ }
18
+ export interface ScatterPoint {
19
+ x: number;
20
+ z: number;
21
+ index: number;
22
+ }
23
+ export declare function scatterAabb(area: ScatterArea | Aabb): Aabb;
24
+ export declare function scatter(config: ScatterConfig): ScatterPoint[];
@@ -0,0 +1,126 @@
1
+ import { expandAabb, pointInAabb } from "./geometry.js";
2
+ function isAabb(area) {
3
+ return (typeof area.minX === "number" &&
4
+ typeof area.minZ === "number" &&
5
+ typeof area.maxX === "number" &&
6
+ typeof area.maxZ === "number");
7
+ }
8
+ export function scatterAabb(area) {
9
+ if (isAabb(area)) {
10
+ return { minX: area.minX, minZ: area.minZ, maxX: area.maxX, maxZ: area.maxZ };
11
+ }
12
+ const cx = area.center ? area.center[0] : 0;
13
+ const cz = area.center ? area.center[1] : 0;
14
+ const halfW = area.w / 2;
15
+ const halfD = area.d / 2;
16
+ return { minX: cx - halfW, minZ: cz - halfD, maxX: cx + halfW, maxZ: cz + halfD };
17
+ }
18
+ function hashSeed(seed) {
19
+ const text = typeof seed === "number" ? seed.toString() : seed;
20
+ let h = 2166136261 >>> 0;
21
+ for (let i = 0; i < text.length; i++) {
22
+ h ^= text.charCodeAt(i);
23
+ h = Math.imul(h, 16777619);
24
+ }
25
+ return h >>> 0;
26
+ }
27
+ function mulberry32(state) {
28
+ let a = state >>> 0;
29
+ return () => {
30
+ a = (a + 0x6d2b79f5) >>> 0;
31
+ let t = a;
32
+ t = Math.imul(t ^ (t >>> 15), t | 1);
33
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
34
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
35
+ };
36
+ }
37
+ export function scatter(config) {
38
+ const aabb = scatterAabb(config.area);
39
+ const width = aabb.maxX - aabb.minX;
40
+ const depth = aabb.maxZ - aabb.minZ;
41
+ const density = config.density ?? 0.01;
42
+ const target = config.count ?? Math.max(0, Math.floor(width * depth * density));
43
+ if (target <= 0 || width <= 0 || depth <= 0)
44
+ return [];
45
+ const minDistance = config.minDistance ?? 0;
46
+ const avoidMargin = config.avoidMargin ?? 0;
47
+ const jitter = config.jitter ?? 1;
48
+ const maxAttempts = config.maxAttempts ?? target * 30;
49
+ const avoid = (config.avoid ?? []).map((a) => (avoidMargin !== 0 ? expandAabb(a, avoidMargin) : a));
50
+ const rng = mulberry32(hashSeed(config.seed ?? "scatter"));
51
+ const useDistance = minDistance > 0;
52
+ const buckets = new Map();
53
+ const bucketKey = (x, z) => `${Math.floor(x / minDistance)}:${Math.floor(z / minDistance)}`;
54
+ const rejectedByAvoid = (point) => {
55
+ for (const a of avoid)
56
+ if (pointInAabb(point, a))
57
+ return true;
58
+ return false;
59
+ };
60
+ const rejectedByDistance = (point) => {
61
+ if (!useDistance)
62
+ return false;
63
+ const cx = Math.floor(point[0] / minDistance);
64
+ const cz = Math.floor(point[1] / minDistance);
65
+ const minSq = minDistance * minDistance;
66
+ for (let dx = -1; dx <= 1; dx++) {
67
+ for (let dz = -1; dz <= 1; dz++) {
68
+ const nearby = buckets.get(`${cx + dx}:${cz + dz}`);
69
+ if (!nearby)
70
+ continue;
71
+ for (const other of nearby) {
72
+ const ex = other[0] - point[0];
73
+ const ez = other[1] - point[1];
74
+ if (ex * ex + ez * ez < minSq)
75
+ return true;
76
+ }
77
+ }
78
+ }
79
+ return false;
80
+ };
81
+ const accepted = [];
82
+ const commit = (point) => {
83
+ accepted.push({ x: point[0], z: point[1], index: accepted.length });
84
+ if (useDistance) {
85
+ const key = bucketKey(point[0], point[1]);
86
+ const bucket = buckets.get(key);
87
+ if (bucket)
88
+ bucket.push(point);
89
+ else
90
+ buckets.set(key, [point]);
91
+ }
92
+ };
93
+ const cols = Math.max(1, Math.ceil(Math.sqrt(target * (width / depth))));
94
+ const rows = Math.max(1, Math.ceil(target / cols));
95
+ const cellW = width / cols;
96
+ const cellD = depth / rows;
97
+ for (let row = 0; row < rows && accepted.length < target; row++) {
98
+ for (let col = 0; col < cols && accepted.length < target; col++) {
99
+ const baseX = aabb.minX + (col + 0.5) * cellW;
100
+ const baseZ = aabb.minZ + (row + 0.5) * cellD;
101
+ const x = baseX + jitter * (rng() - 0.5) * cellW;
102
+ const z = baseZ + jitter * (rng() - 0.5) * cellD;
103
+ const point = [x, z];
104
+ if (!pointInAabb(point, aabb))
105
+ continue;
106
+ if (rejectedByAvoid(point))
107
+ continue;
108
+ if (rejectedByDistance(point))
109
+ continue;
110
+ commit(point);
111
+ }
112
+ }
113
+ let attempts = 0;
114
+ while (accepted.length < target && attempts < maxAttempts) {
115
+ attempts++;
116
+ const x = aabb.minX + rng() * width;
117
+ const z = aabb.minZ + rng() * depth;
118
+ const point = [x, z];
119
+ if (rejectedByAvoid(point))
120
+ continue;
121
+ if (rejectedByDistance(point))
122
+ continue;
123
+ commit(point);
124
+ }
125
+ return accepted;
126
+ }
@@ -0,0 +1,38 @@
1
+ import type { Aabb } from "./geometry.js";
2
+ import type { RegionField, RegionSample } from "./regions.js";
3
+ /** One placeable class. `item` is an opaque id the caller maps to a mesh/entity. */
4
+ export interface ScatterLayer {
5
+ item: string;
6
+ density: number;
7
+ minScale?: number;
8
+ maxScale?: number;
9
+ maxSlope?: number;
10
+ aboveWaterOnly?: boolean;
11
+ }
12
+ export interface ScatterInstance {
13
+ id: string;
14
+ item: string;
15
+ x: number;
16
+ z: number;
17
+ y: number;
18
+ scale: number;
19
+ rotation: number;
20
+ regionId: string;
21
+ }
22
+ /**
23
+ * Deterministically place opaque items across `area`, grounded on a region field.
24
+ * For each grid cell it asks `layersFor` which items may appear in that region and
25
+ * rolls one against their densities. The engine never interprets `item` — a game
26
+ * maps it to a mesh or entity. Content scatter (region-driven density) as opposed to
27
+ * `scatter` in `./scatter`, which is renderer-free geometric point distribution.
28
+ */
29
+ export declare function scatterItems<T>(field: RegionField<T>, area: Aabb, layersFor: (sample: RegionSample<T>) => readonly ScatterLayer[], options?: {
30
+ cell?: number;
31
+ max?: number;
32
+ saltKey?: number;
33
+ }): ScatterInstance[];
34
+ /** Weighted pick from opaque entries; `roll` in [0, 1). Returns null when empty. */
35
+ export declare function pickWeighted<T>(entries: readonly {
36
+ value: T;
37
+ weight: number;
38
+ }[], roll: number): T | null;