@jgengine/core 0.9.0 → 0.10.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 (114) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/dist/ai/spawnDirector.d.ts +13 -0
  3. package/dist/ai/spawnDirector.js +40 -16
  4. package/dist/ai/spawnPoint.d.ts +5 -0
  5. package/dist/ai/spawnPoint.js +5 -0
  6. package/dist/audio/audioFalloff.d.ts +5 -2
  7. package/dist/audio/music.d.ts +53 -0
  8. package/dist/audio/music.js +34 -0
  9. package/dist/audio/synth.d.ts +60 -0
  10. package/dist/audio/synth.js +8 -0
  11. package/dist/authoring.d.ts +14 -0
  12. package/dist/authoring.js +14 -0
  13. package/dist/cartridge/runtime.js +2 -2
  14. package/dist/combat/dotField.d.ts +43 -0
  15. package/dist/combat/dotField.js +48 -0
  16. package/dist/combat/projectiles.d.ts +2 -2
  17. package/dist/combat/projectiles.js +18 -5
  18. package/dist/combat/regenShield.d.ts +35 -0
  19. package/dist/combat/regenShield.js +45 -0
  20. package/dist/combat/shotOrigin.d.ts +13 -1
  21. package/dist/combat/shotOrigin.js +31 -2
  22. package/dist/editor/commands.d.ts +65 -0
  23. package/dist/editor/commands.js +193 -0
  24. package/dist/editor/document.d.ts +40 -0
  25. package/dist/editor/document.js +153 -0
  26. package/dist/editor/index.d.ts +30 -0
  27. package/dist/editor/index.js +6 -0
  28. package/dist/editor/types.d.ts +69 -0
  29. package/dist/editor/types.js +29 -0
  30. package/dist/game/connectedPlayers.d.ts +28 -0
  31. package/dist/game/connectedPlayers.js +21 -0
  32. package/dist/game/defineGame.d.ts +42 -1
  33. package/dist/game/defineGame.js +1 -0
  34. package/dist/game/events.d.ts +15 -0
  35. package/dist/game/keyValueStore.d.ts +25 -0
  36. package/dist/game/keyValueStore.js +61 -0
  37. package/dist/game/levelSequence.d.ts +24 -2
  38. package/dist/game/levelSequence.js +51 -2
  39. package/dist/game/lootTable.d.ts +4 -0
  40. package/dist/game/lootTable.js +1 -0
  41. package/dist/game/modelAnimation.d.ts +8 -0
  42. package/dist/game/modelAnimation.js +18 -0
  43. package/dist/game/playableGame.d.ts +68 -5
  44. package/dist/game/recordBook.d.ts +3 -5
  45. package/dist/index.d.ts +7 -0
  46. package/dist/index.js +7 -0
  47. package/dist/input/actionBindings.d.ts +2 -0
  48. package/dist/input/actionBindings.js +1 -0
  49. package/dist/input/axisInput.d.ts +7 -0
  50. package/dist/input/axisInput.js +16 -0
  51. package/dist/interaction/contextMenu.d.ts +1 -0
  52. package/dist/interaction/contextMenu.js +1 -0
  53. package/dist/interaction/proximityPrompt.d.ts +3 -1
  54. package/dist/interaction/proximityPrompt.js +3 -3
  55. package/dist/meta/changelog.d.ts +1 -1
  56. package/dist/meta/changelog.js +22 -1
  57. package/dist/movement/movementModel.d.ts +4 -0
  58. package/dist/movement/movementModel.js +13 -0
  59. package/dist/movement/playerMovement.d.ts +41 -0
  60. package/dist/movement/playerMovement.js +181 -0
  61. package/dist/random/rng.d.ts +2 -0
  62. package/dist/random/rng.js +2 -0
  63. package/dist/render/postProcessing.d.ts +56 -0
  64. package/dist/render/postProcessing.js +1 -0
  65. package/dist/runtime/adapter.d.ts +19 -0
  66. package/dist/runtime/adapter.js +11 -5
  67. package/dist/runtime/gameContext.d.ts +53 -10
  68. package/dist/runtime/gameContext.js +192 -54
  69. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  70. package/dist/runtime/hostedGameRunner.js +62 -0
  71. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  72. package/dist/runtime/hostedWorldSession.js +57 -0
  73. package/dist/runtime/inputSnapshot.d.ts +13 -0
  74. package/dist/runtime/inputSnapshot.js +2 -0
  75. package/dist/runtime/motionIntents.d.ts +2 -0
  76. package/dist/runtime/motionIntents.js +11 -0
  77. package/dist/runtime/worldChannel.d.ts +58 -0
  78. package/dist/runtime/worldChannel.js +69 -0
  79. package/dist/runtime/worldMirror.d.ts +24 -0
  80. package/dist/runtime/worldMirror.js +31 -0
  81. package/dist/runtime/worldReplication.d.ts +46 -0
  82. package/dist/runtime/worldReplication.js +185 -0
  83. package/dist/runtime/worldSnapshot.d.ts +17 -0
  84. package/dist/runtime/worldSnapshot.js +15 -0
  85. package/dist/scene/assetCatalog.d.ts +1 -0
  86. package/dist/scene/colliders.d.ts +6 -0
  87. package/dist/scene/colliders.js +25 -1
  88. package/dist/scene/entityStats.d.ts +4 -0
  89. package/dist/scene/entityStats.js +21 -0
  90. package/dist/scene/entityStore.d.ts +29 -0
  91. package/dist/scene/entityStore.js +60 -0
  92. package/dist/store/observableKeyedStore.d.ts +4 -0
  93. package/dist/store/observableKeyedStore.js +9 -0
  94. package/dist/tactics/fallingGrid.d.ts +2 -2
  95. package/dist/tactics/fallingGrid.js +2 -2
  96. package/dist/ui/gameLayout.d.ts +125 -0
  97. package/dist/ui/gameLayout.js +129 -0
  98. package/dist/ui/orientation.d.ts +39 -0
  99. package/dist/ui/orientation.js +49 -0
  100. package/dist/ui/swingTimer.d.ts +42 -0
  101. package/dist/ui/swingTimer.js +38 -0
  102. package/dist/visibility/spatialIndex.js +34 -20
  103. package/dist/world/environmentSummary.d.ts +11 -2
  104. package/dist/world/environmentSummary.js +11 -1
  105. package/dist/world/features.d.ts +134 -5
  106. package/dist/world/features.js +40 -3
  107. package/dist/world/roads.d.ts +47 -0
  108. package/dist/world/roads.js +146 -0
  109. package/dist/world/streets.d.ts +80 -0
  110. package/dist/world/streets.js +144 -0
  111. package/dist/world/terrain.d.ts +32 -5
  112. package/dist/world/terrain.js +124 -12
  113. package/llms.txt +485 -79
  114. package/package.json +1 -1
@@ -1,3 +1,4 @@
1
+ import { nearestOnPath } from "./roads.js";
1
2
  function clamp01(value) {
2
3
  return value < 0 ? 0 : value > 1 ? 1 : value;
3
4
  }
@@ -13,6 +14,7 @@ function hash2(ix, iz, seed) {
13
14
  h ^= h >>> 12;
14
15
  return (h >>> 0) / 4294967295;
15
16
  }
17
+ /** Smoothly interpolated 2D value noise in `[-1, 1]` for the given seed. */
16
18
  export function valueNoise(x, z, seed) {
17
19
  const x0 = Math.floor(x);
18
20
  const z0 = Math.floor(z);
@@ -28,6 +30,7 @@ export function valueNoise(x, z, seed) {
28
30
  const bottom = lerp(v01, v11, sx);
29
31
  return lerp(top, bottom, sz) * 2 - 1;
30
32
  }
33
+ /** Layers `valueNoise` octaves per `config` into a single normalized noise sample. */
31
34
  export function fractalNoise(x, z, config) {
32
35
  let amplitude = 1;
33
36
  let frequency = config.frequency;
@@ -57,6 +60,7 @@ export function seedFrom(value, fallback) {
57
60
  }
58
61
  return fallback;
59
62
  }
63
+ /** Derives a `TerrainField.sampleNormal` from a height sampler via finite-difference gradients. */
60
64
  export function withNormal(sampleHeight) {
61
65
  const epsilon = 0.75;
62
66
  return (x, z) => {
@@ -81,9 +85,11 @@ export const FLAT_FIELD = {
81
85
  sampleHeight: () => 0,
82
86
  sampleNormal: () => [0, 1, 0],
83
87
  };
88
+ /** A flat, zero-height `TerrainField` for arenas with no elevation. */
84
89
  export function flatField() {
85
90
  return FLAT_FIELD;
86
91
  }
92
+ /** Builds a `TerrainField` whose height is fractal noise shaped by `config`. */
87
93
  export function noiseField(config = {}) {
88
94
  const fractal = {
89
95
  seed: seedFrom(config.seed, 1337),
@@ -120,6 +126,7 @@ function smoothstep(edge0, edge1, value) {
120
126
  const t = clamp01((value - edge0) / (edge1 - edge0));
121
127
  return t * t * (3 - 2 * t);
122
128
  }
129
+ /** Builds a `TerrainField` with a flat spawn plateau, rolling hills, and a basin, for combat arenas. */
123
130
  export function arenaField(config = {}) {
124
131
  const seed = seedFrom(config.seed ?? "arena", 7);
125
132
  const rolling = {
@@ -153,6 +160,10 @@ export function groundFieldFor(world) {
153
160
  return resolveEnvironmentField(world);
154
161
  return flatField();
155
162
  }
163
+ /** Whether a world declares real terrain (base heightfield or islands) rather than a flat plane — gates terrain-floor sampling in the movement controllers. */
164
+ export function hasEnvironmentTerrain(world) {
165
+ return world?.kind === "environment" && (world.terrain !== undefined || (world.islands?.length ?? 0) > 0);
166
+ }
156
167
  /** Ground height between islands when no base terrain exists — deep enough to read as a fall into the void, finite so physics stays sane. */
157
168
  export const ISLAND_VOID_HEIGHT = -256;
158
169
  function islandContains(descriptor, x, z) {
@@ -236,6 +247,32 @@ export const TERRAIN_MATERIAL_PALETTES = {
236
247
  highland: { low: "#414f33", high: "#a3a86b", waterline: "#365f63" },
237
248
  slate: { low: "#2f3540", high: "#7d8896", waterline: "#26404f" },
238
249
  };
250
+ const DEFAULT_TERRAIN_DETAIL = {
251
+ rockColor: "#6f7175",
252
+ sandColor: "#c2b283",
253
+ snowColor: "#eef3f7",
254
+ rockSlopeStart: 0.42,
255
+ snowHeight: 26,
256
+ detailScale: 5,
257
+ macroScale: 45,
258
+ roughness: 0.9,
259
+ strength: 1,
260
+ };
261
+ /** Fill a `TerrainDetailConfig` with defaults; `waterLevel` falls back to the terrain's own water level. */
262
+ export function resolveTerrainDetail(config, terrainWaterLevel = 0) {
263
+ return {
264
+ rockColor: config.rockColor ?? DEFAULT_TERRAIN_DETAIL.rockColor,
265
+ sandColor: config.sandColor ?? DEFAULT_TERRAIN_DETAIL.sandColor,
266
+ snowColor: config.snowColor ?? DEFAULT_TERRAIN_DETAIL.snowColor,
267
+ rockSlopeStart: config.rockSlopeStart ?? DEFAULT_TERRAIN_DETAIL.rockSlopeStart,
268
+ snowHeight: config.snowHeight ?? DEFAULT_TERRAIN_DETAIL.snowHeight,
269
+ detailScale: config.detailScale ?? DEFAULT_TERRAIN_DETAIL.detailScale,
270
+ macroScale: config.macroScale ?? DEFAULT_TERRAIN_DETAIL.macroScale,
271
+ roughness: config.roughness ?? DEFAULT_TERRAIN_DETAIL.roughness,
272
+ strength: config.strength ?? DEFAULT_TERRAIN_DETAIL.strength,
273
+ waterLevel: config.waterLevel ?? terrainWaterLevel,
274
+ };
275
+ }
239
276
  export function resolveTerrainPalette(descriptor = {}) {
240
277
  const material = descriptor.material ?? DEFAULT_TERRAIN_MATERIAL;
241
278
  const preset = TERRAIN_MATERIAL_PALETTES[material];
@@ -278,35 +315,108 @@ function mixPalette(from, to, t) {
278
315
  };
279
316
  }
280
317
  /**
281
- * Per-position palette sampler over the descriptor's base `material`/`colors` plus its
282
- * `materialRegions` — the multi-biome coloring seam. Regions paint fully inside `radius`
283
- * and blend back to the surrounding palette across `falloff`; later regions win overlaps.
318
+ * Per-z palette sampler over a descriptor's `biomeBands` — ordered zones that cross-fade their
319
+ * ground palette across a `fade`-wide window centered on the midpoint z between adjacent centers.
320
+ * Below the first / above the last band clamps to that band's palette. Returns `fallback` when no
321
+ * bands are declared. Pure math, unit-testable independent of rendering.
322
+ */
323
+ export function createBiomeBandSampler(bands, fallback) {
324
+ if (bands.length === 0)
325
+ return () => fallback;
326
+ const resolved = bands
327
+ .map((band) => ({ z: band.z, fade: band.fade ?? 64, palette: resolveTerrainPalette(band) }))
328
+ .sort((a, b) => a.z - b.z);
329
+ const first = resolved[0];
330
+ const last = resolved[resolved.length - 1];
331
+ return (z) => {
332
+ if (z <= first.z)
333
+ return first.palette;
334
+ if (z >= last.z)
335
+ return last.palette;
336
+ for (let i = 0; i < resolved.length - 1; i += 1) {
337
+ const lower = resolved[i];
338
+ const upper = resolved[i + 1];
339
+ if (z < lower.z || z > upper.z)
340
+ continue;
341
+ const boundary = (lower.z + upper.z) / 2;
342
+ const half = Math.min(lower.fade, upper.fade) / 2;
343
+ if (z <= boundary - half)
344
+ return lower.palette;
345
+ if (z >= boundary + half)
346
+ return upper.palette;
347
+ return mixPalette(lower.palette, upper.palette, smoothstep(boundary - half, boundary + half, z));
348
+ }
349
+ return fallback;
350
+ };
351
+ }
352
+ /**
353
+ * Per-position palette sampler over the descriptor's base `material`/`colors` plus its z-ordered
354
+ * `biomeBands` (painted first) and radial `materialRegions` (painted over) — the multi-biome coloring
355
+ * seam. Regions paint fully inside `radius` and blend back across `falloff`; later regions win overlaps.
284
356
  */
285
357
  export function createTerrainPaletteSampler(descriptor) {
286
358
  const base = resolveTerrainPalette(descriptor);
359
+ const bandSampler = descriptor.biomeBands === undefined || descriptor.biomeBands.length === 0
360
+ ? null
361
+ : createBiomeBandSampler(descriptor.biomeBands, base);
287
362
  const regions = (descriptor.materialRegions ?? []).map((region) => ({
288
- center: region.center,
289
- radius: region.radius,
290
- falloff: region.falloff ?? region.radius * 0.5,
363
+ ...resolveRegionShape(region),
291
364
  palette: resolveTerrainPalette(region),
292
365
  }));
293
- if (regions.length === 0)
366
+ if (regions.length === 0 && bandSampler === null)
294
367
  return () => base;
295
368
  return (x, z) => {
296
- let palette = base;
369
+ let palette = bandSampler === null ? base : bandSampler(z);
297
370
  for (const region of regions) {
298
- const distance = Math.hypot(x - region.center[0], z - region.center[1]);
299
- if (distance <= region.radius) {
371
+ const distance = region.coreDistance(x, z);
372
+ if (distance <= 0) {
300
373
  palette = region.palette;
301
374
  }
302
- else if (distance <= region.radius + region.falloff) {
303
- const t = smoothstep(region.radius, region.radius + region.falloff, distance);
375
+ else if (distance <= region.falloff) {
376
+ const t = smoothstep(0, region.falloff, distance);
304
377
  palette = mixPalette(region.palette, palette, t);
305
378
  }
306
379
  }
307
380
  return palette;
308
381
  };
309
382
  }
383
+ /**
384
+ * Resolves a `TerrainMaterialRegion` of any shape to a `coreDistance(x, z)` — 0 inside the painted
385
+ * core, growing positive outward — plus its blend-ring `falloff`, so the palette sampler blends every
386
+ * shape through one shape-agnostic loop.
387
+ */
388
+ function resolveRegionShape(region) {
389
+ if (region.shape === "polyline") {
390
+ const half = region.width * 0.5;
391
+ return {
392
+ falloff: region.falloff ?? half,
393
+ coreDistance: (x, z) => {
394
+ const sample = nearestOnPath(region.points, x, z);
395
+ return sample === null ? Number.POSITIVE_INFINITY : Math.max(0, sample.distance - half);
396
+ },
397
+ };
398
+ }
399
+ if (region.shape === "rect") {
400
+ const [hx, hz] = region.halfExtents;
401
+ const angle = region.rotationY ?? 0;
402
+ const cos = Math.cos(angle);
403
+ const sin = Math.sin(angle);
404
+ return {
405
+ falloff: region.falloff ?? Math.min(hx, hz) * 0.5,
406
+ coreDistance: (x, z) => {
407
+ const dx = x - region.center[0];
408
+ const dz = z - region.center[1];
409
+ const lx = dx * cos + dz * sin;
410
+ const lz = -dx * sin + dz * cos;
411
+ return Math.hypot(Math.max(0, Math.abs(lx) - hx), Math.max(0, Math.abs(lz) - hz));
412
+ },
413
+ };
414
+ }
415
+ return {
416
+ falloff: region.falloff ?? region.radius * 0.5,
417
+ coreDistance: (x, z) => Math.max(0, Math.hypot(x - region.center[0], z - region.center[1]) - region.radius),
418
+ };
419
+ }
310
420
  function withFlattenMasks(sampleHeight, masks) {
311
421
  const resolved = masks.map((mask) => ({
312
422
  center: mask.center,
@@ -366,6 +476,7 @@ export function heightMapField(config) {
366
476
  ...(config.waterLevel === undefined ? {} : { waterLevel: config.waterLevel }),
367
477
  });
368
478
  }
479
+ /** Resolves a `TerrainEnvironmentDescriptor` into a concrete `TerrainField`, applying flatten masks. */
369
480
  export function resolveTerrainField(descriptor) {
370
481
  if (descriptor === undefined)
371
482
  return flatField();
@@ -407,6 +518,7 @@ export function snapEntityToGround(entities, id, field, offset = 0) {
407
518
  return entities.setPose(id, { position: snapToGround(field, entity.position, offset) });
408
519
  }
409
520
  export const DEFAULT_MAX_WALK_SLOPE = 0.6;
521
+ /** Zeroes out a movement step's x or z component where it would climb steeper than `maxSlope`. */
410
522
  export function resolveGroundStep(field, x, z, stepX, stepZ, maxSlope = DEFAULT_MAX_WALK_SLOPE) {
411
523
  const baseHeight = field.sampleHeight(x, z);
412
524
  const tooSteep = (dx, dz) => {