@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,85 @@
1
+ function hash(ix, iz, salt) {
2
+ let h = salt | 0;
3
+ h = Math.imul(h ^ (ix | 0), 0x27d4eb2d);
4
+ h = Math.imul(h ^ (iz | 0), 0x85ebca6b);
5
+ h ^= h >>> 15;
6
+ h = Math.imul(h, 0x2c1b3c6d);
7
+ h ^= h >>> 12;
8
+ return (h >>> 0) / 4294967295;
9
+ }
10
+ const DEFAULT_MAX_SLOPE = 0.55;
11
+ /**
12
+ * Deterministically place opaque items across `area`, grounded on a region field.
13
+ * For each grid cell it asks `layersFor` which items may appear in that region and
14
+ * rolls one against their densities. The engine never interprets `item` — a game
15
+ * maps it to a mesh or entity. Content scatter (region-driven density) as opposed to
16
+ * `scatter` in `./scatter`, which is renderer-free geometric point distribution.
17
+ */
18
+ export function scatterItems(field, area, layersFor, options = {}) {
19
+ const cell = options.cell ?? 5;
20
+ const max = options.max ?? 1400;
21
+ const salt = field.seed ^ (options.saltKey ?? 0x5f3a);
22
+ const out = [];
23
+ for (let gx = Math.floor(area.minX / cell); gx <= Math.ceil(area.maxX / cell); gx += 1) {
24
+ for (let gz = Math.floor(area.minZ / cell); gz <= Math.ceil(area.maxZ / cell); gz += 1) {
25
+ const roll = hash(gx, gz, salt);
26
+ const x = (gx + hash(gx, gz, salt ^ 0x11)) * cell;
27
+ const z = (gz + hash(gx, gz, salt ^ 0x22)) * cell;
28
+ const y = field.sampleHeight(x, z);
29
+ const sample = field.sampleRegion(x, z);
30
+ const layers = layersFor(sample);
31
+ if (layers.length === 0)
32
+ continue;
33
+ let total = 0;
34
+ for (const layer of layers)
35
+ total += layer.density;
36
+ if (roll > total)
37
+ continue;
38
+ let acc = 0;
39
+ let chosen = layers[0];
40
+ for (const layer of layers) {
41
+ acc += layer.density;
42
+ if (roll <= acc) {
43
+ chosen = layer;
44
+ break;
45
+ }
46
+ }
47
+ if ((chosen.aboveWaterOnly ?? true) && y < field.seaLevel + 0.1)
48
+ continue;
49
+ if (1 - field.sampleNormal(x, z)[1] > (chosen.maxSlope ?? DEFAULT_MAX_SLOPE))
50
+ continue;
51
+ const scaleRoll = hash(gx, gz, salt ^ 0x99);
52
+ const min = chosen.minScale ?? 0.8;
53
+ const maxScale = chosen.maxScale ?? 1.2;
54
+ out.push({
55
+ id: `${gx}:${gz}`,
56
+ item: chosen.item,
57
+ x,
58
+ z,
59
+ y,
60
+ scale: min + scaleRoll * (maxScale - min),
61
+ rotation: hash(gx, gz, salt ^ 0xabcd) * Math.PI * 2,
62
+ regionId: sample.region.id,
63
+ });
64
+ if (out.length >= max)
65
+ return out;
66
+ }
67
+ }
68
+ return out;
69
+ }
70
+ /** Weighted pick from opaque entries; `roll` in [0, 1). Returns null when empty. */
71
+ export function pickWeighted(entries, roll) {
72
+ if (entries.length === 0)
73
+ return null;
74
+ let total = 0;
75
+ for (const entry of entries)
76
+ total += entry.weight;
77
+ const pick = roll * total;
78
+ let acc = 0;
79
+ for (const entry of entries) {
80
+ acc += entry.weight;
81
+ if (pick <= acc)
82
+ return entry.value;
83
+ }
84
+ return entries[entries.length - 1].value;
85
+ }
@@ -0,0 +1,27 @@
1
+ import type { AddBodyOptions } from "../physics/physicsWorld.js";
2
+ import type { ConnectorVec3 } from "./connectors.js";
3
+ export interface SupportPiece {
4
+ id: string;
5
+ grounded?: boolean;
6
+ position?: ConnectorVec3;
7
+ halfExtents?: ConnectorVec3;
8
+ mass?: number;
9
+ }
10
+ export interface SupportLink {
11
+ a: string;
12
+ b: string;
13
+ }
14
+ export interface SupportConfig {
15
+ maxDistance?: number;
16
+ }
17
+ export interface SupportResult {
18
+ supported: readonly string[];
19
+ unsupported: readonly string[];
20
+ distance: Readonly<Record<string, number>>;
21
+ }
22
+ export declare function solveSupport(pieces: readonly SupportPiece[], links: readonly SupportLink[], config?: SupportConfig): SupportResult;
23
+ export interface DebrisOptions {
24
+ scatter?: number;
25
+ seed?: number;
26
+ }
27
+ export declare function toDebrisBodies(pieces: readonly SupportPiece[], collapsedIds: readonly string[], options?: DebrisOptions): AddBodyOptions[];
@@ -0,0 +1,75 @@
1
+ export function solveSupport(pieces, links, config = {}) {
2
+ const maxDistance = config.maxDistance ?? Infinity;
3
+ const ids = new Set(pieces.map((piece) => piece.id));
4
+ const adjacency = new Map();
5
+ for (const id of ids)
6
+ adjacency.set(id, []);
7
+ for (const link of links) {
8
+ if (!ids.has(link.a) || !ids.has(link.b))
9
+ continue;
10
+ adjacency.get(link.a).push(link.b);
11
+ adjacency.get(link.b).push(link.a);
12
+ }
13
+ const distance = {};
14
+ const queue = [];
15
+ for (const piece of pieces) {
16
+ if (piece.grounded === true) {
17
+ distance[piece.id] = 0;
18
+ queue.push(piece.id);
19
+ }
20
+ }
21
+ let head = 0;
22
+ while (head < queue.length) {
23
+ const current = queue[head];
24
+ head += 1;
25
+ const currentDistance = distance[current];
26
+ if (currentDistance >= maxDistance)
27
+ continue;
28
+ for (const neighbor of adjacency.get(current) ?? []) {
29
+ const next = currentDistance + 1;
30
+ if (distance[neighbor] === undefined || next < distance[neighbor]) {
31
+ distance[neighbor] = next;
32
+ queue.push(neighbor);
33
+ }
34
+ }
35
+ }
36
+ const supported = [];
37
+ const unsupported = [];
38
+ for (const piece of pieces) {
39
+ const d = distance[piece.id];
40
+ if (d !== undefined && d <= maxDistance)
41
+ supported.push(piece.id);
42
+ else
43
+ unsupported.push(piece.id);
44
+ }
45
+ return { supported, unsupported, distance };
46
+ }
47
+ function scatterHash(seed, index) {
48
+ let h = (seed ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0;
49
+ h = Math.imul(h ^ (h >>> 15), 0x85ebca6b);
50
+ h ^= h >>> 13;
51
+ return ((h >>> 0) / 4294967295) * 2 - 1;
52
+ }
53
+ export function toDebrisBodies(pieces, collapsedIds, options = {}) {
54
+ const scatter = options.scatter ?? 1.5;
55
+ const seed = options.seed ?? 1;
56
+ const collapse = new Set(collapsedIds);
57
+ const bodies = [];
58
+ let index = 0;
59
+ for (const piece of pieces) {
60
+ if (!collapse.has(piece.id))
61
+ continue;
62
+ const position = piece.position ?? [0, 0, 0];
63
+ const halfExtents = piece.halfExtents ?? [0.5, 0.5, 0.5];
64
+ const vx = scatterHash(seed, index) * scatter;
65
+ const vz = scatterHash(seed, index + 977) * scatter;
66
+ bodies.push({
67
+ position,
68
+ halfExtents,
69
+ velocity: [vx, 0, vz],
70
+ ...(piece.mass === undefined ? {} : { mass: piece.mass }),
71
+ });
72
+ index += 1;
73
+ }
74
+ return bodies;
75
+ }
@@ -0,0 +1,54 @@
1
+ import type { Aabb, Vec2 } from "./geometry.js";
2
+ import { type TerrainField } from "./terrain.js";
3
+ export type TerraformMode = "raise" | "lower" | "flatten" | "paint";
4
+ export type TerraformFalloff = "smooth" | "linear" | "none";
5
+ export interface TerraformEdit {
6
+ mode: TerraformMode;
7
+ center: Vec2;
8
+ radius: number;
9
+ strength?: number;
10
+ target?: number;
11
+ surface?: string;
12
+ falloff?: TerraformFalloff;
13
+ }
14
+ export interface EditableTerrainConfig {
15
+ bounds: Aabb;
16
+ base?: TerrainField;
17
+ cellSize?: number;
18
+ }
19
+ export interface TerraformSnapshot {
20
+ bounds: Aabb;
21
+ cellSize: number;
22
+ cols: number;
23
+ rows: number;
24
+ offsets: readonly number[];
25
+ surfaces: readonly (string | null)[];
26
+ }
27
+ export interface EditableTerrain extends TerrainField {
28
+ apply(edit: TerraformEdit): number;
29
+ heightOffsetAt(x: number, z: number): number;
30
+ surfaceAt(x: number, z: number): string | null;
31
+ reset(): void;
32
+ snapshot(): TerraformSnapshot;
33
+ restore(snapshot: TerraformSnapshot): void;
34
+ }
35
+ export declare function brushWeight(distance: number, radius: number, falloff: TerraformFalloff): number;
36
+ export declare function createEditableTerrain(config: EditableTerrainConfig): EditableTerrain;
37
+ export interface TerraformBrushConfig {
38
+ radius?: number;
39
+ strength?: number;
40
+ falloff?: TerraformFalloff;
41
+ surface?: string;
42
+ }
43
+ export interface TerraformBrush {
44
+ raise(center: Vec2): number;
45
+ lower(center: Vec2): number;
46
+ flatten(center: Vec2, target?: number): number;
47
+ paint(center: Vec2, surface?: string): number;
48
+ setRadius(radius: number): void;
49
+ setStrength(strength: number): void;
50
+ config(): Required<Omit<TerraformBrushConfig, "surface">> & {
51
+ surface: string | undefined;
52
+ };
53
+ }
54
+ export declare function createTerraformBrush(terrain: Pick<EditableTerrain, "apply">, config?: TerraformBrushConfig): TerraformBrush;
@@ -0,0 +1,186 @@
1
+ import { FLAT_FIELD, withNormal } from "./terrain.js";
2
+ export function brushWeight(distance, radius, falloff) {
3
+ if (radius <= 0)
4
+ return distance <= 0 ? 1 : 0;
5
+ const t = distance / radius;
6
+ if (t >= 1)
7
+ return 0;
8
+ if (falloff === "none")
9
+ return 1;
10
+ if (falloff === "linear")
11
+ return 1 - t;
12
+ const s = 1 - t;
13
+ return s * s * (3 - 2 * s);
14
+ }
15
+ function clampIndex(value, max) {
16
+ return value < 0 ? 0 : value > max ? max : value;
17
+ }
18
+ export function createEditableTerrain(config) {
19
+ const base = config.base ?? FLAT_FIELD;
20
+ const bounds = config.bounds;
21
+ const cellSize = config.cellSize && config.cellSize > 0 ? config.cellSize : 1;
22
+ const spanX = Math.max(bounds.maxX - bounds.minX, cellSize);
23
+ const spanZ = Math.max(bounds.maxZ - bounds.minZ, cellSize);
24
+ const cols = Math.max(1, Math.round(spanX / cellSize));
25
+ const rows = Math.max(1, Math.round(spanZ / cellSize));
26
+ const vertsX = cols + 1;
27
+ const vertsZ = rows + 1;
28
+ const offsets = new Float32Array(vertsX * vertsZ);
29
+ const surfaces = new Array(cols * rows).fill(null);
30
+ function gridX(x) {
31
+ return ((x - bounds.minX) / spanX) * cols;
32
+ }
33
+ function gridZ(z) {
34
+ return ((z - bounds.minZ) / spanZ) * rows;
35
+ }
36
+ function heightOffsetAt(x, z) {
37
+ const fx = gridX(x);
38
+ const fz = gridZ(z);
39
+ const x0 = clampIndex(Math.floor(fx), cols);
40
+ const z0 = clampIndex(Math.floor(fz), rows);
41
+ const x1 = clampIndex(x0 + 1, cols);
42
+ const z1 = clampIndex(z0 + 1, rows);
43
+ const tx = fx - x0 < 0 ? 0 : fx - x0 > 1 ? 1 : fx - x0;
44
+ const tz = fz - z0 < 0 ? 0 : fz - z0 > 1 ? 1 : fz - z0;
45
+ const v00 = offsets[z0 * vertsX + x0];
46
+ const v10 = offsets[z0 * vertsX + x1];
47
+ const v01 = offsets[z1 * vertsX + x0];
48
+ const v11 = offsets[z1 * vertsX + x1];
49
+ const top = v00 + (v10 - v00) * tx;
50
+ const bottom = v01 + (v11 - v01) * tx;
51
+ return top + (bottom - top) * tz;
52
+ }
53
+ const sampleHeight = (x, z) => base.sampleHeight(x, z) + heightOffsetAt(x, z);
54
+ const sampleNormal = withNormal(sampleHeight);
55
+ function editHeight(edit) {
56
+ const falloff = edit.falloff ?? "smooth";
57
+ const strength = edit.strength ?? 1;
58
+ const target = edit.mode === "flatten" ? edit.target ?? sampleHeight(edit.center[0], edit.center[1]) : 0;
59
+ const rCells = Math.ceil(edit.radius / cellSize) + 1;
60
+ const cx = gridX(edit.center[0]);
61
+ const cz = gridZ(edit.center[1]);
62
+ let changed = 0;
63
+ for (let iz = -rCells; iz <= rCells; iz += 1) {
64
+ const gz = Math.round(cz) + iz;
65
+ if (gz < 0 || gz >= vertsZ)
66
+ continue;
67
+ for (let ix = -rCells; ix <= rCells; ix += 1) {
68
+ const gx = Math.round(cx) + ix;
69
+ if (gx < 0 || gx >= vertsX)
70
+ continue;
71
+ const wx = bounds.minX + (gx / cols) * spanX;
72
+ const wz = bounds.minZ + (gz / rows) * spanZ;
73
+ const dist = Math.hypot(wx - edit.center[0], wz - edit.center[1]);
74
+ const weight = brushWeight(dist, edit.radius, falloff);
75
+ if (weight <= 0)
76
+ continue;
77
+ const index = gz * vertsX + gx;
78
+ if (edit.mode === "raise")
79
+ offsets[index] += strength * weight;
80
+ else if (edit.mode === "lower")
81
+ offsets[index] -= strength * weight;
82
+ else {
83
+ const current = base.sampleHeight(wx, wz) + offsets[index];
84
+ offsets[index] += (target - current) * weight * strength;
85
+ }
86
+ changed += 1;
87
+ }
88
+ }
89
+ return changed;
90
+ }
91
+ function paintSurface(edit) {
92
+ const surface = edit.surface ?? null;
93
+ const rCells = Math.ceil(edit.radius / cellSize) + 1;
94
+ const cx = gridX(edit.center[0]);
95
+ const cz = gridZ(edit.center[1]);
96
+ let painted = 0;
97
+ for (let iz = -rCells; iz <= rCells; iz += 1) {
98
+ const gz = Math.floor(cz) + iz;
99
+ if (gz < 0 || gz >= rows)
100
+ continue;
101
+ for (let ix = -rCells; ix <= rCells; ix += 1) {
102
+ const gx = Math.floor(cx) + ix;
103
+ if (gx < 0 || gx >= cols)
104
+ continue;
105
+ const wx = bounds.minX + ((gx + 0.5) / cols) * spanX;
106
+ const wz = bounds.minZ + ((gz + 0.5) / rows) * spanZ;
107
+ if (Math.hypot(wx - edit.center[0], wz - edit.center[1]) > edit.radius)
108
+ continue;
109
+ surfaces[gz * cols + gx] = surface;
110
+ painted += 1;
111
+ }
112
+ }
113
+ return painted;
114
+ }
115
+ function surfaceAt(x, z) {
116
+ const gx = Math.floor(gridX(x));
117
+ const gz = Math.floor(gridZ(z));
118
+ if (gx < 0 || gx >= cols || gz < 0 || gz >= rows)
119
+ return null;
120
+ return surfaces[gz * cols + gx] ?? null;
121
+ }
122
+ const field = {
123
+ sampleHeight,
124
+ sampleNormal,
125
+ ...(base.bounds === undefined ? {} : { bounds: base.bounds }),
126
+ ...(base.waterLevel === undefined ? {} : { waterLevel: base.waterLevel }),
127
+ apply(edit) {
128
+ return edit.mode === "paint" ? paintSurface(edit) : editHeight(edit);
129
+ },
130
+ heightOffsetAt,
131
+ surfaceAt,
132
+ reset() {
133
+ offsets.fill(0);
134
+ surfaces.fill(null);
135
+ },
136
+ snapshot() {
137
+ return {
138
+ bounds: { ...bounds },
139
+ cellSize,
140
+ cols,
141
+ rows,
142
+ offsets: Array.from(offsets),
143
+ surfaces: surfaces.slice(),
144
+ };
145
+ },
146
+ restore(snapshot) {
147
+ const count = Math.min(offsets.length, snapshot.offsets.length);
148
+ for (let index = 0; index < count; index += 1)
149
+ offsets[index] = snapshot.offsets[index];
150
+ const surfaceCount = Math.min(surfaces.length, snapshot.surfaces.length);
151
+ for (let index = 0; index < surfaceCount; index += 1)
152
+ surfaces[index] = snapshot.surfaces[index] ?? null;
153
+ },
154
+ };
155
+ return field;
156
+ }
157
+ export function createTerraformBrush(terrain, config = {}) {
158
+ let radius = config.radius ?? 3;
159
+ let strength = config.strength ?? 0.5;
160
+ const falloff = config.falloff ?? "smooth";
161
+ const surface = config.surface;
162
+ return {
163
+ raise(center) {
164
+ return terrain.apply({ mode: "raise", center, radius, strength, falloff });
165
+ },
166
+ lower(center) {
167
+ return terrain.apply({ mode: "lower", center, radius, strength, falloff });
168
+ },
169
+ flatten(center, target) {
170
+ return terrain.apply({ mode: "flatten", center, radius, strength, falloff, ...(target === undefined ? {} : { target }) });
171
+ },
172
+ paint(center, paintSurface) {
173
+ const chosen = paintSurface ?? surface;
174
+ return terrain.apply({ mode: "paint", center, radius, ...(chosen === undefined ? {} : { surface: chosen }) });
175
+ },
176
+ setRadius(next) {
177
+ radius = next;
178
+ },
179
+ setStrength(next) {
180
+ strength = next;
181
+ },
182
+ config() {
183
+ return { radius, strength, falloff, surface };
184
+ },
185
+ };
186
+ }
@@ -0,0 +1,54 @@
1
+ import type { TerrainEnvironmentDescriptor, WorldBounds } from "./features.js";
2
+ export type TerrainNormal = readonly [number, number, number];
3
+ export interface TerrainField {
4
+ sampleHeight(x: number, z: number): number;
5
+ sampleNormal(x: number, z: number): TerrainNormal;
6
+ readonly bounds?: WorldBounds;
7
+ readonly waterLevel?: number;
8
+ }
9
+ export declare function valueNoise(x: number, z: number, seed: number): number;
10
+ export interface FractalNoiseConfig {
11
+ seed: number;
12
+ frequency: number;
13
+ octaves: number;
14
+ lacunarity: number;
15
+ persistence: number;
16
+ ridged: boolean;
17
+ }
18
+ export declare function fractalNoise(x: number, z: number, config: FractalNoiseConfig): number;
19
+ export declare function seedFrom(value: string | number | undefined, fallback: number): number;
20
+ export declare function withNormal(sampleHeight: (x: number, z: number) => number): TerrainField["sampleNormal"];
21
+ export declare const FLAT_FIELD: TerrainField;
22
+ export declare function flatField(): TerrainField;
23
+ export interface NoiseFieldConfig {
24
+ seed?: string | number;
25
+ amplitude?: number;
26
+ frequency?: number;
27
+ octaves?: number;
28
+ lacunarity?: number;
29
+ persistence?: number;
30
+ baseHeight?: number;
31
+ ridged?: boolean;
32
+ bounds?: WorldBounds;
33
+ waterLevel?: number;
34
+ }
35
+ export declare function noiseField(config?: NoiseFieldConfig): TerrainField;
36
+ export interface RollingFieldConfig {
37
+ seed?: string | number;
38
+ amplitude?: number;
39
+ frequency?: number;
40
+ bounds?: WorldBounds;
41
+ }
42
+ export declare function rollingField(config?: RollingFieldConfig): TerrainField;
43
+ export interface ArenaFieldConfig {
44
+ seed?: string | number;
45
+ bounds?: WorldBounds;
46
+ }
47
+ export declare const ARENA_WATER_LEVEL = -0.9;
48
+ export declare function arenaField(config?: ArenaFieldConfig): TerrainField;
49
+ export declare function resolveTerrainField(descriptor?: TerrainEnvironmentDescriptor): TerrainField;
50
+ export declare const DEFAULT_MAX_WALK_SLOPE = 0.6;
51
+ export declare function resolveGroundStep(field: TerrainField, x: number, z: number, stepX: number, stepZ: number, maxSlope?: number): {
52
+ stepX: number;
53
+ stepZ: number;
54
+ };
@@ -0,0 +1,181 @@
1
+ function clamp01(value) {
2
+ return value < 0 ? 0 : value > 1 ? 1 : value;
3
+ }
4
+ function lerp(a, b, t) {
5
+ return a + (b - a) * t;
6
+ }
7
+ function hash2(ix, iz, seed) {
8
+ let h = seed | 0;
9
+ h = Math.imul(h ^ (ix | 0), 0x27d4eb2d);
10
+ h = Math.imul(h ^ (iz | 0), 0x85ebca6b);
11
+ h ^= h >>> 15;
12
+ h = Math.imul(h, 0x2c1b3c6d);
13
+ h ^= h >>> 12;
14
+ return (h >>> 0) / 4294967295;
15
+ }
16
+ export function valueNoise(x, z, seed) {
17
+ const x0 = Math.floor(x);
18
+ const z0 = Math.floor(z);
19
+ const fx = x - x0;
20
+ const fz = z - z0;
21
+ const v00 = hash2(x0, z0, seed);
22
+ const v10 = hash2(x0 + 1, z0, seed);
23
+ const v01 = hash2(x0, z0 + 1, seed);
24
+ const v11 = hash2(x0 + 1, z0 + 1, seed);
25
+ const sx = fx * fx * (3 - 2 * fx);
26
+ const sz = fz * fz * (3 - 2 * fz);
27
+ const top = lerp(v00, v10, sx);
28
+ const bottom = lerp(v01, v11, sx);
29
+ return lerp(top, bottom, sz) * 2 - 1;
30
+ }
31
+ export function fractalNoise(x, z, config) {
32
+ let amplitude = 1;
33
+ let frequency = config.frequency;
34
+ let sum = 0;
35
+ let norm = 0;
36
+ for (let octave = 0; octave < config.octaves; octave += 1) {
37
+ let sample = valueNoise(x * frequency, z * frequency, config.seed + octave * 1013);
38
+ if (config.ridged)
39
+ sample = 1 - 2 * Math.abs(sample);
40
+ sum += sample * amplitude;
41
+ norm += amplitude;
42
+ amplitude *= config.persistence;
43
+ frequency *= config.lacunarity;
44
+ }
45
+ return norm === 0 ? 0 : sum / norm;
46
+ }
47
+ export function seedFrom(value, fallback) {
48
+ if (typeof value === "number")
49
+ return value | 0;
50
+ if (typeof value === "string") {
51
+ let hash = 2166136261;
52
+ for (let index = 0; index < value.length; index += 1) {
53
+ hash ^= value.charCodeAt(index);
54
+ hash = Math.imul(hash, 16777619);
55
+ }
56
+ return hash | 0;
57
+ }
58
+ return fallback;
59
+ }
60
+ export function withNormal(sampleHeight) {
61
+ const epsilon = 0.75;
62
+ return (x, z) => {
63
+ const hx = sampleHeight(x + epsilon, z) - sampleHeight(x - epsilon, z);
64
+ const hz = sampleHeight(x, z + epsilon) - sampleHeight(x, z - epsilon);
65
+ const nx = -hx;
66
+ const nz = -hz;
67
+ const ny = 2 * epsilon;
68
+ const length = Math.hypot(nx, ny, nz) || 1;
69
+ return [nx / length, ny / length, nz / length];
70
+ };
71
+ }
72
+ function fieldFromHeight(sampleHeight, extra) {
73
+ return {
74
+ sampleHeight,
75
+ sampleNormal: withNormal(sampleHeight),
76
+ ...(extra?.bounds === undefined ? {} : { bounds: extra.bounds }),
77
+ ...(extra?.waterLevel === undefined ? {} : { waterLevel: extra.waterLevel }),
78
+ };
79
+ }
80
+ export const FLAT_FIELD = {
81
+ sampleHeight: () => 0,
82
+ sampleNormal: () => [0, 1, 0],
83
+ };
84
+ export function flatField() {
85
+ return FLAT_FIELD;
86
+ }
87
+ export function noiseField(config = {}) {
88
+ const fractal = {
89
+ seed: seedFrom(config.seed, 1337),
90
+ frequency: config.frequency ?? 0.03,
91
+ octaves: config.octaves ?? 4,
92
+ lacunarity: config.lacunarity ?? 2,
93
+ persistence: config.persistence ?? 0.5,
94
+ ridged: config.ridged ?? false,
95
+ };
96
+ const amplitude = config.amplitude ?? 1;
97
+ const baseHeight = config.baseHeight ?? 0;
98
+ return fieldFromHeight((x, z) => baseHeight + amplitude * fractalNoise(x, z, fractal), {
99
+ bounds: config.bounds,
100
+ waterLevel: config.waterLevel,
101
+ });
102
+ }
103
+ export function rollingField(config = {}) {
104
+ return noiseField({
105
+ seed: config.seed ?? "rolling",
106
+ amplitude: config.amplitude ?? 0.55,
107
+ frequency: config.frequency ?? 0.03,
108
+ octaves: 3,
109
+ persistence: 0.55,
110
+ bounds: config.bounds,
111
+ });
112
+ }
113
+ const ARENA_PLATEAU_HEIGHT = 6;
114
+ const ARENA_HILL_HEIGHT = 4.2;
115
+ const ARENA_BASIN_DEPTH = 2.6;
116
+ export const ARENA_WATER_LEVEL = -0.9;
117
+ function smoothstep(edge0, edge1, value) {
118
+ if (edge0 === edge1)
119
+ return value < edge0 ? 0 : 1;
120
+ const t = clamp01((value - edge0) / (edge1 - edge0));
121
+ return t * t * (3 - 2 * t);
122
+ }
123
+ export function arenaField(config = {}) {
124
+ const seed = seedFrom(config.seed ?? "arena", 7);
125
+ const rolling = {
126
+ seed,
127
+ frequency: 0.03,
128
+ octaves: 3,
129
+ lacunarity: 2,
130
+ persistence: 0.5,
131
+ ridged: false,
132
+ };
133
+ const sampleHeight = (x, z) => {
134
+ const distToSpawn = Math.hypot(x, z);
135
+ const spawn = 1 - smoothstep(14, 24, distToSpawn);
136
+ const undulation = 0.55 * fractalNoise(x, z, rolling);
137
+ const cliff = smoothstep(20, 27, z);
138
+ const ramp = smoothstep(16, 46, z);
139
+ const inRamp = 1 - clamp01(Math.abs(x) / 6);
140
+ const plateauExtent = 1 - smoothstep(30, 42, Math.abs(x));
141
+ const mesa = ARENA_PLATEAU_HEIGHT * plateauExtent * lerp(cliff, ramp, inRamp);
142
+ const hillDist = Math.hypot(x - 34, z + 8) / 18;
143
+ const hillFalloff = smoothstep(0, 1, 1 - hillDist);
144
+ const hill = ARENA_HILL_HEIGHT * hillFalloff;
145
+ const basinDist = Math.hypot(x + 40, z + 20) / 20;
146
+ const basin = -ARENA_BASIN_DEPTH * smoothstep(0, 1, 1 - basinDist);
147
+ return (undulation + mesa + hill + basin) * (1 - spawn);
148
+ };
149
+ return fieldFromHeight(sampleHeight, { bounds: config.bounds, waterLevel: ARENA_WATER_LEVEL });
150
+ }
151
+ export function resolveTerrainField(descriptor) {
152
+ if (descriptor === undefined)
153
+ return flatField();
154
+ return noiseField({
155
+ seed: descriptor.seed,
156
+ amplitude: descriptor.height,
157
+ frequency: descriptor.frequency,
158
+ octaves: descriptor.octaves,
159
+ ridged: descriptor.ridged,
160
+ baseHeight: descriptor.baseHeight,
161
+ waterLevel: descriptor.waterLevel,
162
+ bounds: descriptor.bounds,
163
+ });
164
+ }
165
+ export const DEFAULT_MAX_WALK_SLOPE = 0.6;
166
+ export function resolveGroundStep(field, x, z, stepX, stepZ, maxSlope = DEFAULT_MAX_WALK_SLOPE) {
167
+ const baseHeight = field.sampleHeight(x, z);
168
+ const tooSteep = (dx, dz) => {
169
+ const distance = Math.hypot(dx, dz);
170
+ if (distance < 1e-6)
171
+ return false;
172
+ const rise = field.sampleHeight(x + dx, z + dz) - baseHeight;
173
+ return rise / distance > maxSlope;
174
+ };
175
+ if (!tooSteep(stepX, stepZ))
176
+ return { stepX, stepZ };
177
+ return {
178
+ stepX: tooSteep(stepX, 0) ? 0 : stepX,
179
+ stepZ: tooSteep(0, stepZ) ? 0 : stepZ,
180
+ };
181
+ }