@jgengine/core 0.17.0 → 0.18.1

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 (174) hide show
  1. package/CHANGELOG.md +130 -2
  2. package/dist/ai/decisionGraph.d.ts +52 -0
  3. package/dist/ai/decisionGraph.js +82 -0
  4. package/dist/ai/mobBrainGraph.d.ts +3 -0
  5. package/dist/ai/mobBrainGraph.js +25 -0
  6. package/dist/ai/tacticalQueries.d.ts +30 -0
  7. package/dist/ai/tacticalQueries.js +45 -0
  8. package/dist/anim/animGraph.d.ts +144 -0
  9. package/dist/anim/animGraph.js +286 -0
  10. package/dist/anim/ikSolver.d.ts +51 -0
  11. package/dist/anim/ikSolver.js +124 -0
  12. package/dist/anim/locomotionGraph.d.ts +26 -0
  13. package/dist/anim/locomotionGraph.js +33 -0
  14. package/dist/assets/spriteAtlas.d.ts +21 -0
  15. package/dist/assets/spriteAtlas.js +103 -0
  16. package/dist/audio/audioFalloff.d.ts +10 -0
  17. package/dist/economy/currency.d.ts +16 -2
  18. package/dist/economy/currency.js +55 -14
  19. package/dist/economy/wallet.d.ts +5 -4
  20. package/dist/economy/wallet.js +8 -4
  21. package/dist/editor/commandApply.js +1 -0
  22. package/dist/editor/commands.d.ts +4 -1
  23. package/dist/editor/document.d.ts +2 -1
  24. package/dist/editor/document.js +89 -0
  25. package/dist/editor/environment.d.ts +3 -0
  26. package/dist/editor/environment.js +16 -0
  27. package/dist/editor/index.d.ts +4 -2
  28. package/dist/editor/index.js +1 -1
  29. package/dist/editor/types.d.ts +16 -0
  30. package/dist/game/cameraConfig.d.ts +8 -0
  31. package/dist/game/chat.d.ts +22 -1
  32. package/dist/game/chat.js +48 -19
  33. package/dist/game/controlGate.d.ts +6 -0
  34. package/dist/game/controlGate.js +30 -1
  35. package/dist/game/defineGame.d.ts +10 -1
  36. package/dist/game/events.d.ts +8 -1
  37. package/dist/game/pixelPerfect.d.ts +25 -0
  38. package/dist/game/pixelPerfect.js +29 -0
  39. package/dist/game/playableGame.d.ts +97 -3
  40. package/dist/game/systemRuntime.js +6 -1
  41. package/dist/input/actionBindings.d.ts +2 -1
  42. package/dist/input/actionBindings.js +10 -8
  43. package/dist/input/actionContexts.d.ts +21 -0
  44. package/dist/input/actionContexts.js +56 -0
  45. package/dist/input/gamepadModel.d.ts +45 -0
  46. package/dist/input/gamepadModel.js +69 -0
  47. package/dist/meta/changelog.d.ts +1 -1
  48. package/dist/meta/changelog.js +103 -1
  49. package/dist/movement/characterController.d.ts +76 -0
  50. package/dist/movement/characterController.js +216 -0
  51. package/dist/movement/freeFlight.d.ts +119 -0
  52. package/dist/movement/freeFlight.js +294 -0
  53. package/dist/movement/playerMovement.d.ts +10 -2
  54. package/dist/movement/playerMovement.js +218 -4
  55. package/dist/movement/serverStep.d.ts +5 -0
  56. package/dist/movement/serverStep.js +6 -0
  57. package/dist/movement/solidObstacles.d.ts +30 -7
  58. package/dist/movement/solidObstacles.js +27 -4
  59. package/dist/multiplayer/poseSyncGate.d.ts +3 -1
  60. package/dist/multiplayer/poseSyncGate.js +9 -1
  61. package/dist/multiplayer/presenceContract.d.ts +6 -0
  62. package/dist/multiplayer/presenceModel.d.ts +2 -0
  63. package/dist/multiplayer/presenceModel.js +11 -0
  64. package/dist/nav/navMesh.d.ts +33 -0
  65. package/dist/nav/navMesh.js +164 -0
  66. package/dist/physics/flightDynamics.d.ts +8 -0
  67. package/dist/physics/kinematicVehicle.d.ts +6 -1
  68. package/dist/physics/physicsBackend.d.ts +191 -0
  69. package/dist/physics/physicsBackend.js +49 -0
  70. package/dist/physics/physicsBackendConformance.d.ts +27 -0
  71. package/dist/physics/physicsBackendConformance.js +117 -0
  72. package/dist/physics/physicsWorldBackend.d.ts +44 -0
  73. package/dist/physics/physicsWorldBackend.js +485 -0
  74. package/dist/physics/worldColliders.d.ts +13 -0
  75. package/dist/physics/worldColliders.js +96 -0
  76. package/dist/random/rng.d.ts +12 -1
  77. package/dist/random/rng.js +20 -3
  78. package/dist/render/environment.d.ts +23 -0
  79. package/dist/render/environment.js +1 -0
  80. package/dist/render/lookPreset.d.ts +11 -5
  81. package/dist/render/lookPreset.js +29 -4
  82. package/dist/render/postProcessing.d.ts +10 -1
  83. package/dist/render/sprite2d.d.ts +38 -0
  84. package/dist/render/sprite2d.js +40 -0
  85. package/dist/runtime/adapter.d.ts +20 -1
  86. package/dist/runtime/adapter.js +8 -1
  87. package/dist/runtime/commandInput.d.ts +15 -0
  88. package/dist/runtime/commandInput.js +13 -0
  89. package/dist/runtime/commandRunner.d.ts +44 -3
  90. package/dist/runtime/commandRunner.js +41 -3
  91. package/dist/runtime/gameContext.js +11 -1
  92. package/dist/runtime/gameContextTypes.d.ts +14 -4
  93. package/dist/runtime/gameRuntime.d.ts +33 -4
  94. package/dist/runtime/gameRuntime.js +26 -5
  95. package/dist/runtime/headlessRunner.d.ts +10 -0
  96. package/dist/runtime/headlessRunner.js +26 -8
  97. package/dist/runtime/hostPolicy.d.ts +3 -2
  98. package/dist/runtime/hostPolicy.js +8 -10
  99. package/dist/runtime/hostedGameRunner.js +34 -6
  100. package/dist/runtime/hostedWorldSession.d.ts +12 -16
  101. package/dist/runtime/hostedWorldSession.js +29 -6
  102. package/dist/runtime/hostedWorldStore.d.ts +20 -0
  103. package/dist/runtime/hostedWorldStore.js +1 -0
  104. package/dist/runtime/inputRecorder.d.ts +30 -0
  105. package/dist/runtime/inputRecorder.js +59 -0
  106. package/dist/runtime/inputSnapshot.d.ts +8 -0
  107. package/dist/runtime/inputSnapshot.js +1 -0
  108. package/dist/runtime/poseInterpolation.d.ts +36 -0
  109. package/dist/runtime/poseInterpolation.js +91 -0
  110. package/dist/runtime/prediction.d.ts +45 -0
  111. package/dist/runtime/prediction.js +62 -0
  112. package/dist/runtime/simContext.d.ts +45 -0
  113. package/dist/runtime/simContext.js +65 -0
  114. package/dist/runtime/simLoop.d.ts +52 -0
  115. package/dist/runtime/simLoop.js +72 -0
  116. package/dist/runtime/simSnapshot.d.ts +32 -0
  117. package/dist/runtime/simSnapshot.js +60 -0
  118. package/dist/runtime/snapshot.d.ts +8 -0
  119. package/dist/runtime/snapshotBuffer.d.ts +33 -0
  120. package/dist/runtime/snapshotBuffer.js +58 -0
  121. package/dist/runtime/transport.d.ts +38 -2
  122. package/dist/runtime/worldChunks.d.ts +2 -0
  123. package/dist/runtime/worldChunks.js +14 -0
  124. package/dist/runtime/worldProjection.d.ts +9 -0
  125. package/dist/runtime/worldProjection.js +17 -1
  126. package/dist/runtime/worldSnapshot.d.ts +6 -0
  127. package/dist/runtime/worldSnapshot.js +3 -0
  128. package/dist/scene/behaviorRuntime.d.ts +19 -1
  129. package/dist/scene/behaviorRuntime.js +55 -4
  130. package/dist/scene/behaviors.d.ts +8 -1
  131. package/dist/scene/builtinSceneKinds.js +10 -0
  132. package/dist/sensor/perception.d.ts +60 -0
  133. package/dist/sensor/perception.js +142 -0
  134. package/dist/settings/graphicsProfile.d.ts +19 -0
  135. package/dist/settings/graphicsProfile.js +36 -0
  136. package/dist/time/accrueSince.d.ts +10 -0
  137. package/dist/time/accrueSince.js +9 -0
  138. package/dist/time/rateWindow.d.ts +16 -0
  139. package/dist/time/rateWindow.js +14 -0
  140. package/dist/time/serverTick.d.ts +21 -3
  141. package/dist/time/serverTick.js +14 -13
  142. package/dist/vfx/particleDirector.d.ts +68 -0
  143. package/dist/vfx/particleDirector.js +81 -0
  144. package/dist/world/buildings.d.ts +12 -0
  145. package/dist/world/buildings.js +43 -3
  146. package/dist/world/cityContent.js +103 -25
  147. package/dist/world/cityKind.d.ts +132 -0
  148. package/dist/world/cityKind.js +315 -15
  149. package/dist/world/features.d.ts +27 -1
  150. package/dist/world/features.js +7 -2
  151. package/dist/world/placement.d.ts +7 -0
  152. package/dist/world/placement.js +2 -0
  153. package/dist/world/placementController.d.ts +1 -1
  154. package/dist/world/placementController.js +7 -0
  155. package/dist/world/raceCircuit.d.ts +168 -0
  156. package/dist/world/raceCircuit.js +797 -0
  157. package/dist/world/terrain.d.ts +5 -1
  158. package/dist/world/terrain.js +5 -0
  159. package/dist/world/territory.d.ts +97 -0
  160. package/dist/world/territory.js +182 -0
  161. package/dist/world.d.ts +5 -3
  162. package/dist/world.js +4 -2
  163. package/package.json +1 -1
  164. package/skills/ce-handoff/SKILL.md +1 -1
  165. package/skills/game-design/SKILL.md +11 -1
  166. package/skills/jgengine/SKILL.md +2 -0
  167. package/skills/jgengine/api.md +98 -22
  168. package/skills/jgengine/capabilities.md +28 -0
  169. package/skills/jgengine-gameplay/SKILL.md +10 -0
  170. package/skills/jgengine-gameplay/api.md +40 -18
  171. package/skills/jgengine-gameplay/capabilities.md +12 -0
  172. package/skills/jgengine-world/SKILL.md +6 -0
  173. package/skills/jgengine-world/api.md +312 -12
  174. package/skills/jgengine-world/capabilities.md +102 -0
@@ -1,6 +1,8 @@
1
1
  export interface CurrencyDefinition<TCurrencyId extends string = string> {
2
2
  id: TCurrencyId;
3
3
  name: string;
4
+ /** Fractional digits in major-unit amounts; defaults to 0. */
5
+ decimals?: number;
4
6
  /** Prepended when formatting amounts (e.g. "$"). */
5
7
  symbol?: string;
6
8
  /** Appended when formatting amounts (e.g. "tokens"). */
@@ -16,7 +18,19 @@ export type CurrencyAdjustment = {
16
18
  reason: string;
17
19
  };
18
20
  /** @internal */
19
- export declare function sanitizeCurrencyAmount(amount: number): number;
21
+ export declare function sanitizeCurrencyAmount(amount: number, currency?: CurrencyDefinition): number;
22
+ /** Convert major units to integer minor units, rounding once at the write boundary.
23
+ * @capability currency-minor-units round decimal currency into safe integer minor units
24
+ */
25
+ export declare function toMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number;
26
+ /** Convert stored integer minor units to major units for display or existing balance records.
27
+ * @capability currency-major-units convert safe minor-unit integers into currency amounts
28
+ */
29
+ export declare function fromMinorUnits(currency: Pick<CurrencyDefinition, "decimals"> | undefined, value: number): number;
30
+ /** Format a major-unit value using the currency's declared precision.
31
+ * @capability currency-precision-format display a currency using its declared decimal precision
32
+ */
33
+ export declare function formatCurrency(currency: CurrencyDefinition, value: number): string;
20
34
  /**
21
35
  * Format a currency amount with its symbol and grouping for HUD display.
22
36
  *
@@ -31,7 +45,7 @@ export declare function applyCurrencyOperation(currency: CurrencyDefinition, cur
31
45
  /** Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
32
46
  * @internal
33
47
  */
34
- export declare function resolveCurrencyDelta(current: number, delta: number): {
48
+ export declare function resolveCurrencyDelta(current: number, delta: number, currency?: CurrencyDefinition): {
35
49
  operation: CurrencyOperation;
36
50
  amount: number;
37
51
  };
@@ -1,6 +1,43 @@
1
1
  /** @internal */
2
- export function sanitizeCurrencyAmount(amount) {
3
- return Math.max(0, Math.floor(amount));
2
+ export function sanitizeCurrencyAmount(amount, currency) {
3
+ if (!Number.isFinite(amount))
4
+ throw new RangeError("Currency amount must be finite");
5
+ return fromMinorUnits(currency, toMinorUnits(currency, Math.max(0, amount)));
6
+ }
7
+ function currencyScale(currency) {
8
+ const decimals = currency?.decimals ?? 0;
9
+ if (!Number.isInteger(decimals) || decimals < 0 || decimals > 9) {
10
+ throw new RangeError("Currency decimals must be an integer between 0 and 9");
11
+ }
12
+ return 10 ** decimals;
13
+ }
14
+ /** Convert major units to integer minor units, rounding once at the write boundary.
15
+ * @capability currency-minor-units round decimal currency into safe integer minor units
16
+ */
17
+ export function toMinorUnits(currency, value) {
18
+ if (!Number.isFinite(value))
19
+ throw new RangeError("Currency amount must be finite");
20
+ const scaled = value * currencyScale(currency);
21
+ const result = Math.sign(scaled) * Math.round(Math.abs(scaled) + Math.min(1e-7, Number.EPSILON * Math.abs(scaled)));
22
+ if (!Number.isSafeInteger(result))
23
+ throw new RangeError("Currency amount exceeds safe integer precision");
24
+ return result;
25
+ }
26
+ /** Convert stored integer minor units to major units for display or existing balance records.
27
+ * @capability currency-major-units convert safe minor-unit integers into currency amounts
28
+ */
29
+ export function fromMinorUnits(currency, value) {
30
+ if (!Number.isSafeInteger(value))
31
+ throw new RangeError("Minor units must be a safe integer");
32
+ return value / currencyScale(currency);
33
+ }
34
+ /** Format a major-unit value using the currency's declared precision.
35
+ * @capability currency-precision-format display a currency using its declared decimal precision
36
+ */
37
+ export function formatCurrency(currency, value) {
38
+ const decimals = currency.decimals ?? 0;
39
+ const rounded = fromMinorUnits(currency, toMinorUnits(currency, value));
40
+ return `${currency.symbol ?? ""}${rounded.toFixed(decimals)}${currency.unit ? ` ${currency.unit}` : ""}`;
4
41
  }
5
42
  /**
6
43
  * Format a currency amount with its symbol and grouping for HUD display.
@@ -9,9 +46,7 @@ export function sanitizeCurrencyAmount(amount) {
9
46
  * @internal
10
47
  */
11
48
  export function formatCurrencyAmount(currency, amount) {
12
- const prefix = currency.symbol ?? "";
13
- const suffix = currency.unit ? ` ${currency.unit}` : "";
14
- return `${prefix}${amount}${suffix}`;
49
+ return formatCurrency(currency, amount);
15
50
  }
16
51
  /** @internal */
17
52
  export function insufficientCurrencyReason(currency, needed, current) {
@@ -19,20 +54,26 @@ export function insufficientCurrencyReason(currency, needed, current) {
19
54
  }
20
55
  /** @internal */
21
56
  export function applyCurrencyOperation(currency, current, operation, amount) {
22
- const safeAmount = sanitizeCurrencyAmount(amount);
23
- if (safeAmount === 0)
24
- return { success: true, newBalance: current, appliedDelta: 0 };
25
- if (operation === "deduct" && current < safeAmount) {
26
- return { success: false, reason: insufficientCurrencyReason(currency, safeAmount, current) };
57
+ if (!Number.isFinite(amount) || amount < 0 || !Number.isFinite(current)) {
58
+ return { success: false, reason: "Invalid currency amount" };
59
+ }
60
+ const currentMinor = toMinorUnits(currency, current);
61
+ const amountMinor = toMinorUnits(currency, amount);
62
+ if (operation === "deduct" && currentMinor < amountMinor) {
63
+ return { success: false, reason: insufficientCurrencyReason(currency, fromMinorUnits(currency, amountMinor), current) };
27
64
  }
28
- const appliedDelta = operation === "add" ? safeAmount : -safeAmount;
29
- return { success: true, newBalance: current + appliedDelta, appliedDelta };
65
+ const deltaMinor = operation === "add" ? amountMinor : -amountMinor;
66
+ return {
67
+ success: true,
68
+ newBalance: fromMinorUnits(currency, currentMinor + deltaMinor),
69
+ appliedDelta: fromMinorUnits(currency, deltaMinor),
70
+ };
30
71
  }
31
72
  /** Deducts clamp to the available balance (a delta can never overdraw); adds apply in full.
32
73
  * @internal
33
74
  */
34
- export function resolveCurrencyDelta(current, delta) {
35
- const safeDelta = Math.floor(delta);
75
+ export function resolveCurrencyDelta(current, delta, currency) {
76
+ const safeDelta = fromMinorUnits(currency, toMinorUnits(currency, delta));
36
77
  if (safeDelta > 0)
37
78
  return { operation: "add", amount: safeDelta };
38
79
  return { operation: "deduct", amount: Math.min(Math.abs(safeDelta), current) };
@@ -1,3 +1,4 @@
1
+ import { type CurrencyDefinition } from "./currency.js";
1
2
  export interface WalletState {
2
3
  balances: Readonly<Record<string, number>>;
3
4
  }
@@ -31,16 +32,16 @@ export interface ChargeOptions {
31
32
  * @capability wallet hold currency balances with charge and affordability checks
32
33
  */
33
34
  export declare function createEmptyWallet(): WalletState;
34
- export declare function balance(state: WalletState, currency: string): number;
35
- export declare function grant(state: WalletState, currency: string, amount: number): WalletState;
35
+ export declare function balance(state: WalletState, currency: string | CurrencyDefinition): number;
36
+ export declare function grant(state: WalletState, currency: string | CurrencyDefinition, amount: number): WalletState;
36
37
  /**
37
38
  * Deduct `amount`, rejecting when it would leave the balance negative unless `options.overdraft`
38
39
  * opts into carrying debt (`true` unlimited, `{ max }` capped) — the strict same-tick affordability
39
40
  * check stays the default with `options` omitted.
40
41
  */
41
- export declare function charge(state: WalletState, currency: string, amount: number, options?: ChargeOptions): ChargeResult;
42
+ export declare function charge(state: WalletState, currency: string | CurrencyDefinition, amount: number, options?: ChargeOptions): ChargeResult;
42
43
  /** True once `balance(state, currency)` has gone negative under an overdraft-enabled charge. */
43
- export declare function isOverdrawn(state: WalletState, currency: string): boolean;
44
+ export declare function isOverdrawn(state: WalletState, currency: string | CurrencyDefinition): boolean;
44
45
  /** True when every currency in `costs` has at least that much balance (a pure, non-mutating check). */
45
46
  export declare function canAfford(state: WalletState, costs: Readonly<Record<string, number>>): boolean;
46
47
  export declare function chargeAll(state: WalletState, costs: Readonly<Record<string, number>>, options?: ChargeOptions): ChargeResult;
@@ -1,3 +1,4 @@
1
+ import { fromMinorUnits, toMinorUnits } from "./currency.js";
1
2
  /**
2
3
  * Hold per-currency balances with affordability checks and charge/grant operations.
3
4
  *
@@ -12,14 +13,16 @@ function assertValidAmount(amount) {
12
13
  }
13
14
  }
14
15
  export function balance(state, currency) {
15
- return state.balances[currency] ?? 0;
16
+ return state.balances[typeof currency === "string" ? currency : currency.id] ?? 0;
16
17
  }
17
18
  export function grant(state, currency, amount) {
18
19
  assertValidAmount(amount);
19
20
  return {
20
21
  balances: {
21
22
  ...state.balances,
22
- [currency]: balance(state, currency) + amount,
23
+ [typeof currency === "string" ? currency : currency.id]: typeof currency === "string"
24
+ ? balance(state, currency) + amount
25
+ : fromMinorUnits(currency, toMinorUnits(currency, balance(state, currency)) + toMinorUnits(currency, amount)),
23
26
  },
24
27
  };
25
28
  }
@@ -40,7 +43,8 @@ function withinOverdraft(nextBalance, overdraft) {
40
43
  export function charge(state, currency, amount, options) {
41
44
  assertValidAmount(amount);
42
45
  const current = balance(state, currency);
43
- const next = current - amount;
46
+ const next = typeof currency === "string" ? current - amount
47
+ : fromMinorUnits(currency, toMinorUnits(currency, current) - toMinorUnits(currency, amount));
44
48
  if (!withinOverdraft(next, options?.overdraft)) {
45
49
  return { status: "rejected", reason: "insufficient-funds" };
46
50
  }
@@ -49,7 +53,7 @@ export function charge(state, currency, amount, options) {
49
53
  state: {
50
54
  balances: {
51
55
  ...state.balances,
52
- [currency]: next,
56
+ [typeof currency === "string" ? currency : currency.id]: next,
53
57
  },
54
58
  },
55
59
  };
@@ -447,6 +447,7 @@ const mutationHandlers = {
447
447
  }),
448
448
  setTerrain: (state, command) => ({ ...state, document: { ...state.document, terrain: command.terrain } }),
449
449
  setMinimapBake: (state, command) => ({ ...state, document: { ...state.document, minimap: command.minimap } }),
450
+ setBake: (state, command) => ({ ...state, document: { ...state.document, bakes: [...(state.document.bakes ?? []).filter((bake) => bake.id !== command.bake.id), command.bake] } }),
450
451
  setEnvironment: (state, command) => {
451
452
  if (command.environment === undefined) {
452
453
  const { environment: _removed, ...rest } = state.document;
@@ -2,7 +2,7 @@ import { type SurfaceDelta, type TerraformDelta, type TerrainMaterialLayer, type
2
2
  import type { EditorUiDocument, EditorUiPanelLayout } from "../ui/hudDocument.js";
3
3
  import type { EditorGridCellEdit, EditorGridLayer } from "./grid.js";
4
4
  import type { ParamSchema } from "../scene/sceneKinds.js";
5
- import type { EditorCatalogEntry, EditorDocument, EditorEnvironment, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorTerrain, EditorVec3, EditorVolume } from "./types.js";
5
+ import type { EditorCatalogEntry, EditorBake, EditorDocument, EditorEnvironment, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorTerrain, EditorVec3, EditorVolume } from "./types.js";
6
6
  /** A single editor mutation — select, move, add, remove, undo/redo — dispatched to a session. */
7
7
  export type EditorCommand = {
8
8
  type: "select";
@@ -122,6 +122,9 @@ export type EditorCommand = {
122
122
  } | {
123
123
  type: "setMinimapBake";
124
124
  minimap: EditorMinimapBake;
125
+ } | {
126
+ type: "setBake";
127
+ bake: EditorBake;
125
128
  }
126
129
  /**
127
130
  * Replace the scene-document environment/lighting bag. Pass `undefined` to clear authored sky/fog
@@ -1,5 +1,5 @@
1
1
  import { type EditorGridLayer } from "./grid.js";
2
- import type { EditorCatalogData, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
2
+ import type { EditorCatalogData, EditorBake, EditorCatalogDefinition, EditorCatalogEntry, EditorCollection, EditorDirective, EditorDocument, EditorEnvironment, EditorFragmentContent, EditorLayersInput, EditorMarker, EditorMinimapBake, EditorNote, EditorPath, EditorPrefab, EditorTerrain, EditorVolume } from "./types.js";
3
3
  /** Spread-ready non-placeable fields preserved across document rebuilds. @internal */
4
4
  export declare function editorDocumentExtras(doc: EditorDocument): {
5
5
  prefabs: EditorPrefab[];
@@ -10,6 +10,7 @@ export declare function editorDocumentExtras(doc: EditorDocument): {
10
10
  ui?: EditorDocument["ui"];
11
11
  directives?: EditorDirective[];
12
12
  minimap?: EditorMinimapBake;
13
+ bakes?: EditorBake[];
13
14
  environment?: EditorEnvironment;
14
15
  };
15
16
  /** Builds a fresh, empty editor document to start authoring a scene from scratch.
@@ -5,9 +5,14 @@ import { cloneGridLayer, migrateGridLayer, } from "./grid.js";
5
5
  function cloneMinimapBake(minimap) {
6
6
  return { background: minimap.background, bounds: { ...minimap.bounds } };
7
7
  }
8
+ function cloneBakes(bakes) {
9
+ return bakes === undefined ? undefined : bakes.map((bake) => ({ ...bake, data: structuredClone(bake.data) }));
10
+ }
8
11
  /** Deep-copies scene-document environment/lighting so edits never mutate history snapshots. */
9
12
  function cloneEnvironment(environment) {
10
13
  return {
14
+ ...(environment.source === undefined ? {} : { source: cloneEnvironmentSource(environment.source) }),
15
+ ...(environment.pointLights === undefined ? {} : { pointLights: environment.pointLights.map((light) => ({ ...light, position: [...light.position] })) }),
11
16
  ...(environment.preset === undefined ? {} : { preset: environment.preset }),
12
17
  ...(environment.timeOfDay === undefined ? {} : { timeOfDay: environment.timeOfDay }),
13
18
  ...(environment.horizonColor === undefined ? {} : { horizonColor: environment.horizonColor }),
@@ -17,6 +22,9 @@ function cloneEnvironment(environment) {
17
22
  ...(environment.fog === undefined ? {} : { fog: { ...environment.fog } }),
18
23
  };
19
24
  }
25
+ function cloneEnvironmentSource(source) {
26
+ return source.kind === "cube" ? { ...source, urls: [...source.urls] } : { ...source };
27
+ }
20
28
  function cloneDirective(directive) {
21
29
  const area = directive.area === undefined
22
30
  ? undefined
@@ -66,6 +74,7 @@ export function editorDocumentExtras(doc) {
66
74
  ...(doc.ui === undefined ? {} : { ui: doc.ui }),
67
75
  ...(doc.directives === undefined ? {} : { directives: doc.directives }),
68
76
  ...(doc.minimap === undefined ? {} : { minimap: doc.minimap }),
77
+ ...(doc.bakes === undefined ? {} : { bakes: cloneBakes(doc.bakes) }),
69
78
  ...(doc.environment === undefined ? {} : { environment: doc.environment }),
70
79
  };
71
80
  }
@@ -147,6 +156,7 @@ export function cloneEditorDocument(doc) {
147
156
  ...(ui === undefined ? {} : { ui }),
148
157
  ...(directives === undefined ? {} : { directives }),
149
158
  ...(doc.minimap === undefined ? {} : { minimap: cloneMinimapBake(doc.minimap) }),
159
+ ...(doc.bakes === undefined ? {} : { bakes: cloneBakes(doc.bakes) }),
150
160
  ...(doc.environment === undefined ? {} : { environment: cloneEnvironment(doc.environment) }),
151
161
  };
152
162
  }
@@ -204,6 +214,7 @@ export function normalizeEditorLayers(input) {
204
214
  ...(ui === undefined ? {} : { ui }),
205
215
  ...(directives === undefined ? {} : { directives }),
206
216
  ...(resolved.minimap === undefined ? {} : { minimap: cloneMinimapBake(resolved.minimap) }),
217
+ ...(resolved.bakes === undefined ? {} : { bakes: cloneBakes(resolved.bakes) }),
207
218
  ...(resolved.environment === undefined ? {} : { environment: cloneEnvironment(resolved.environment) }),
208
219
  };
209
220
  }
@@ -949,6 +960,21 @@ function decodeMinimapBake(value, path, errors) {
949
960
  bounds: { minX: b.minX, minZ: b.minZ, maxX: b.maxX, maxZ: b.maxZ },
950
961
  };
951
962
  }
963
+ function decodeBakes(value, path, errors) {
964
+ if (!Array.isArray(value)) {
965
+ errors.push({ path, message: "expected an array" });
966
+ return undefined;
967
+ }
968
+ const bakes = [];
969
+ value.forEach((item, index) => {
970
+ if (!isPlainObject(item) || (item.kind !== "nav" && item.kind !== "minimap") || typeof item.id !== "string" || item.data === undefined) {
971
+ errors.push({ path: `${path}[${index}]`, message: 'expected {kind: "nav" | "minimap", id, data}' });
972
+ return;
973
+ }
974
+ bakes.push({ kind: item.kind, id: item.id, data: structuredClone(item.data) });
975
+ });
976
+ return bakes;
977
+ }
952
978
  const SKY_PRESETS = new Set(["day", "dusk", "night"]);
953
979
  /**
954
980
  * Validates scene-document environment/lighting. Unknown presets, non-finite intensities, and
@@ -962,6 +988,67 @@ function decodeEnvironment(value, path, errors) {
962
988
  }
963
989
  let failed = false;
964
990
  const env = {};
991
+ if (value.source !== undefined) {
992
+ const source = value.source;
993
+ if (!isPlainObject(source) || typeof source.kind !== "string") {
994
+ errors.push({ path: `${path}.source`, message: "expected an environment source object" });
995
+ failed = true;
996
+ }
997
+ else if (source.kind === "gradient") {
998
+ if (typeof source.sky !== "string") {
999
+ errors.push({ path: `${path}.source.sky`, message: "expected a string" });
1000
+ failed = true;
1001
+ }
1002
+ if (typeof source.ground !== "string") {
1003
+ errors.push({ path: `${path}.source.ground`, message: "expected a string" });
1004
+ failed = true;
1005
+ }
1006
+ if (source.sun !== undefined && typeof source.sun !== "string") {
1007
+ errors.push({ path: `${path}.source.sun`, message: "expected a string" });
1008
+ failed = true;
1009
+ }
1010
+ if (source.intensity !== undefined && (typeof source.intensity !== "number" || !Number.isFinite(source.intensity))) {
1011
+ errors.push({ path: `${path}.source.intensity`, message: "expected a finite number" });
1012
+ failed = true;
1013
+ }
1014
+ if (!failed)
1015
+ env.source = { kind: "gradient", sky: source.sky, ground: source.ground, ...(source.sun === undefined ? {} : { sun: source.sun }), ...(source.intensity === undefined ? {} : { intensity: source.intensity }) };
1016
+ }
1017
+ else if (source.kind === "hdri" && typeof source.url === "string") {
1018
+ env.source = { kind: "hdri", url: source.url, ...(typeof source.rotation === "number" ? { rotation: source.rotation } : {}), ...(typeof source.intensity === "number" ? { intensity: source.intensity } : {}) };
1019
+ }
1020
+ else if (source.kind === "cube" && Array.isArray(source.urls) && source.urls.length === 6 && source.urls.every((url) => typeof url === "string")) {
1021
+ env.source = { kind: "cube", urls: [...source.urls], ...(typeof source.intensity === "number" ? { intensity: source.intensity } : {}) };
1022
+ }
1023
+ else {
1024
+ errors.push({ path: `${path}.source`, message: 'expected kind "gradient", "hdri", or "cube" with valid fields' });
1025
+ failed = true;
1026
+ }
1027
+ }
1028
+ if (value.pointLights !== undefined) {
1029
+ if (!Array.isArray(value.pointLights)) {
1030
+ errors.push({ path: `${path}.pointLights`, message: "expected an array" });
1031
+ failed = true;
1032
+ }
1033
+ else if (value.pointLights.length > 64 || value.pointLights.some((light) => {
1034
+ if (!isPlainObject(light) || !Array.isArray(light.position) || light.position.length !== 3 || light.position.some((n) => typeof n !== "number" || !Number.isFinite(n)))
1035
+ return true;
1036
+ return light.intensity !== undefined && (typeof light.intensity !== "number" || !Number.isFinite(light.intensity));
1037
+ })) {
1038
+ errors.push({ path: `${path}.pointLights`, message: "expected point lights with finite positions and intensities" });
1039
+ failed = true;
1040
+ }
1041
+ else {
1042
+ env.pointLights = value.pointLights.map((light) => ({
1043
+ position: [...light.position],
1044
+ ...(typeof light.color === "string" ? { color: light.color } : {}),
1045
+ ...(typeof light.intensity === "number" ? { intensity: light.intensity } : {}),
1046
+ ...(typeof light.distance === "number" ? { distance: light.distance } : {}),
1047
+ ...(typeof light.decay === "number" ? { decay: light.decay } : {}),
1048
+ ...(typeof light.castShadow === "boolean" ? { castShadow: light.castShadow } : {}),
1049
+ }));
1050
+ }
1051
+ }
965
1052
  if (value.preset !== undefined) {
966
1053
  if (typeof value.preset === "string" && SKY_PRESETS.has(value.preset)) {
967
1054
  env.preset = value.preset;
@@ -1152,6 +1239,7 @@ export function decodeEditorDocument(raw) {
1152
1239
  errors.push({ path: "$.terrain", message: "expected an object" });
1153
1240
  }
1154
1241
  const minimap = raw.minimap === undefined ? undefined : decodeMinimapBake(raw.minimap, "$.minimap", errors);
1242
+ const bakes = raw.bakes === undefined ? undefined : decodeBakes(raw.bakes, "$.bakes", errors);
1155
1243
  const environment = raw.environment === undefined ? undefined : decodeEnvironment(raw.environment, "$.environment", errors);
1156
1244
  // Placeable object ids form one document-global namespace (selection, parenting, and removal all
1157
1245
  // treat them that way), so a document that reuses an id — even across two different collections —
@@ -1194,6 +1282,7 @@ export function decodeEditorDocument(raw) {
1194
1282
  ...(ui === undefined ? {} : { ui }),
1195
1283
  ...(directives === undefined ? {} : { directives }),
1196
1284
  ...(minimap === undefined ? {} : { minimap }),
1285
+ ...(bakes === undefined && minimap === undefined ? {} : { bakes: bakes ?? [{ kind: "minimap", id: "minimap", data: minimap }] }),
1197
1286
  ...(environment === undefined ? {} : { environment }),
1198
1287
  },
1199
1288
  };
@@ -3,6 +3,7 @@ import type { AvoidZone } from "../world/geometry.js";
3
3
  import { type ClearanceOptions } from "../world/scatterRegion.js";
4
4
  import type { TerraformSnapshot } from "../world/terraform.js";
5
5
  import type { EditorDocument, EditorEnvironment } from "./types.js";
6
+ import type { LightingConfig } from "../game/playableGame.js";
6
7
  /**
7
8
  * World-authoring convergence (#1018, epic #1006 Phase 4): derive the **coordinate/placement**
8
9
  * content of an `environment()` world from the scene document, so a game with an authored document
@@ -46,6 +47,8 @@ export declare function skyFromDocument(doc: EditorDocument): SkyEnvironmentConf
46
47
  * @internal
47
48
  */
48
49
  export declare function skyConfigFromEnvironment(env: EditorEnvironment): SkyEnvironmentConfig;
50
+ /** Converts authored `light` markers into the runtime lighting contract. */
51
+ export declare function lightingFromDocument(doc: EditorDocument): LightingConfig | undefined;
49
52
  /**
50
53
  * The origin-centered terrain footprint that covers every authored object in a document, padded and
51
54
  * floored to `minBounds`. An empty document yields exactly `minBounds`. Because `environment()`
@@ -28,6 +28,22 @@ export function skyConfigFromEnvironment(env) {
28
28
  ...(env.fog === undefined ? {} : { fog: { ...env.fog } }),
29
29
  };
30
30
  }
31
+ /** Converts authored `light` markers into the runtime lighting contract. */
32
+ export function lightingFromDocument(doc) {
33
+ const points = [];
34
+ const spots = [];
35
+ for (const marker of doc.markers) {
36
+ if (marker.kind !== "light" || marker.position === undefined)
37
+ continue;
38
+ const meta = marker.meta ?? {};
39
+ const base = { position: [marker.position.x, marker.position.y, marker.position.z], color: typeof meta.color === "string" ? meta.color : "#ffd6a0", intensity: typeof meta.intensity === "number" ? meta.intensity : 2, distance: typeof meta.range === "number" ? meta.range : 12, castShadow: meta.castShadow === true };
40
+ if (meta.type === "spot")
41
+ spots.push({ ...base, angle: typeof meta.angle === "number" ? meta.angle : Math.PI / 6 });
42
+ else
43
+ points.push(base);
44
+ }
45
+ return points.length === 0 && spots.length === 0 ? undefined : { point: points, spot: spots };
46
+ }
31
47
  /**
32
48
  * The origin-centered terrain footprint that covers every authored object in a document, padded and
33
49
  * floored to `minBounds`. An empty document yields exactly `minBounds`. Because `environment()`
@@ -5,7 +5,9 @@ export type { EditorTerrain } from "./types.js";
5
5
  /** A baked top-down minimap (PNG data URI + world bounds) persisted on the scene document. */
6
6
  export type { EditorMinimapBake } from "./types.js";
7
7
  /** Scene-document sky/fog/lighting bag edited by the lighting workspace. */
8
- export type { EditorEnvironment, EditorFogConfig, EditorSkyPreset } from "./types.js";
8
+ export type { EditorEnvironment, EditorFogConfig, EditorSkyPreset, PointLightingConfig } from "./types.js";
9
+ /** Declarative HDRI, cube, or gradient environment source used by the shell. */
10
+ export type { EnvironmentSource } from "../render/environment.js";
9
11
  /** Per-kind show/hide flags for the editor's layer panel. */
10
12
  export type { EditorKindVisibility } from "./types.js";
11
13
  /** The four placeable-object collections a prefab fragment or clipboard fragment carries. */
@@ -62,7 +64,7 @@ export { WORLD_BASE_SHARD_ID, WORLD_MANIFEST_KIND, decodeWorldManifest, exportWo
62
64
  export type { DecodeWorldManifestResult, SplitWorldOptions, SplitWorldResult, WorldGrid, WorldManifest, WorldManifestShard, WorldQuery, WorldShardBounds, WorldShardResidency, WorldShardResolver, WorldStreamingConfig, } from "./world.js";
63
65
  /** Result of {@link decodeEditorDocument}: a typed document, or every diagnostic collected while decoding it. */
64
66
  export type { DecodeEditorDocumentResult } from "./document.js";
65
- export { environmentContentFromDocument, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
67
+ export { environmentContentFromDocument, lightingFromDocument, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
66
68
  /** Coordinate/placement content of an `environment()` world derived from the scene document. */
67
69
  export type { EnvironmentContent, EnvironmentContentOptions } from "./environment.js";
68
70
  export { bakeMinimapFromDocument, documentBakeZones } from "./minimap.js";
@@ -3,7 +3,7 @@ export { ENTITY_CATALOG_ID, entityDefinitionSchema, entityEntryFromCatalog, reso
3
3
  export { WELL_KNOWN_MARKER_KINDS, WELL_KNOWN_PATH_KINDS, WELL_KNOWN_VOLUME_KINDS, } from "./types.js";
4
4
  export { applyEditorDocumentOverlay, cloneEditorDocument, collectDescendants, createEmptyEditorDocument, createPrefabFragment, decodeEditorDocument, editorChildren, editorDocumentBounds, editorDocumentExtras, editorDocumentSize, editorMarkerPosition, editorMarkerXZ, editorParentOf, editorRoots, exportEditorDocumentJson, extractEditorFragment, findEditorCatalog, findEditorCatalogEntry, findEditorCollection, findEditorMarker, findEditorNote, findEditorPath, findEditorPrefab, findEditorVolume, importEditorDocumentJson, isEditorObjectCollectionLocked, isEditorObjectHidden, isEditorObjectLocked, isEditorObjectSelfLocked, listEditorKinds, mergeEditorDocuments, normalizeEditorLayers, requireEditorMarker, seedEditorCatalogs, wouldCreateCycle, } from "./document.js";
5
5
  export { WORLD_BASE_SHARD_ID, WORLD_MANIFEST_KIND, decodeWorldManifest, exportWorldManifestJson, loadWorldDocument, selectWorldShards, shardMatchesQuery, singleShardWorldManifest, splitEditorDocumentIntoShards, } from "./world.js";
6
- export { environmentContentFromDocument, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
6
+ export { environmentContentFromDocument, lightingFromDocument, skyConfigFromEnvironment, skyFromDocument, terrainBoundsFromDocument, } from "./environment.js";
7
7
  export { bakeMinimapFromDocument, documentBakeZones } from "./minimap.js";
8
8
  export { createWorldStreamer, shardDistance } from "./streamer.js";
9
9
  export { applyDirectiveOverlay, materializeDirective, materializeDirectives, materializePopulationDirective, materializeScatterDirective, resolveDirectiveFootprint, } from "./directives.js";
@@ -3,6 +3,10 @@ import type { EditorUiDocument } from "../ui/hudDocument.js";
3
3
  import type { MinimapBakeBounds } from "../world/minimapBake.js";
4
4
  import type { TerraformSnapshot } from "../world/terraform.js";
5
5
  import type { EditorGridLayer } from "./grid.js";
6
+ import type { EnvironmentSource } from "../render/environment.js";
7
+ import type { PointLightingConfig } from "../game/playableGame.js";
8
+ /** Runtime point-light shape reused by authored editor light markers. */
9
+ export type { PointLightingConfig } from "../game/playableGame.js";
6
10
  export type { EditorUiDocument, EditorUiPanelLayout, HudResizeAxes, HudPanelTypeDef } from "../ui/hudDocument.js";
7
11
  /** A world-space point used across editor markers, volumes, and paths. */
8
12
  export type EditorVec3 = {
@@ -177,6 +181,12 @@ export interface EditorMinimapBake {
177
181
  /** World-space rectangle the baked image spans. */
178
182
  bounds: MinimapBakeBounds;
179
183
  }
184
+ /** A named baked artifact produced by an editor or build tool. */
185
+ export interface EditorBake {
186
+ kind: "nav" | "minimap";
187
+ id: string;
188
+ data: unknown;
189
+ }
180
190
  /** Named sky look stored on the scene document; matches runtime `SkyEnvironmentConfig.preset`. */
181
191
  export type EditorSkyPreset = "day" | "dusk" | "night";
182
192
  /**
@@ -197,6 +207,10 @@ export interface EditorFogConfig {
197
207
  * @capability editor-environment persist sky/fog/lighting knobs on the scene document
198
208
  */
199
209
  export interface EditorEnvironment {
210
+ /** Optional runtime environment map source; absent keeps the procedural gradient. */
211
+ source?: EnvironmentSource;
212
+ /** Authored local point lights; the runtime clamps these to its bounded light budget. */
213
+ pointLights?: readonly PointLightingConfig[];
200
214
  /** Fixed sky look when `timeOfDay` is off (or no clock is available). */
201
215
  preset?: EditorSkyPreset;
202
216
  /** Drive sun/sky from the world clock's day fraction instead of the fixed `preset`. */
@@ -246,6 +260,8 @@ export interface EditorDocument {
246
260
  * until baked, so old docs and unbaked games load unchanged.
247
261
  */
248
262
  minimap?: EditorMinimapBake;
263
+ /** Named baked artifacts; `minimap` remains as a compatible legacy alias. */
264
+ bakes?: EditorBake[];
249
265
  /**
250
266
  * Scene-document sky/fog/lighting (#1110). Absent until authored in the lighting workspace or
251
267
  * seeded via `editorLayers`, so old docs load unchanged and world.ts can keep a fallback sky.
@@ -45,6 +45,12 @@ export interface FirstPersonCameraConfig {
45
45
  export type CameraRigKind = "orbit" | "first" | "topDown" | "rts" | "shoulder" | "lockOn" | "chase" | "observer" | "turntable" | "sideScroll" | "inspection" | "none";
46
46
  /** Canvas camera projection. "orthographic" renders a flat 2D-style view (side-scrollers, falling-block puzzles) — pair with `rig: "sideScroll"`; default "perspective". */
47
47
  export type CameraProjection = "perspective" | "orthographic";
48
+ /** Pixel-art orthographic camera tuning. `pixelsPerUnit` is the authored pixel density. */
49
+ export interface PixelPerfectCameraConfig {
50
+ pixelsPerUnit: number;
51
+ /** Increase density in whole-number steps to use the largest scale that fits the viewport. */
52
+ integerScale?: boolean;
53
+ }
48
54
  /** Fixed lateral 2.5D follow (side-on platformer cam): the camera sits perpendicular to the travel axis, tracks the followed entity, and never reads player look input. */
49
55
  export interface SideScrollCameraConfig {
50
56
  /** World axis the action travels along; the camera watches from the perpendicular side. Default "x". */
@@ -378,6 +384,8 @@ export interface GameCameraConfig {
378
384
  rig?: CameraRigKind;
379
385
  /** Canvas camera projection. "orthographic" renders a flat 2D-style view (side-scrollers, falling-block puzzles) — pair with `rig: "sideScroll"`; default perspective. */
380
386
  projection?: CameraProjection;
387
+ /** Pixel-perfect orthographic rendering; ignored for perspective cameras. */
388
+ pixelPerfect?: PixelPerfectCameraConfig;
381
389
  /** Render frustum overrides applied to the canvas camera. `far` defaults to 300 — raise it for worlds whose content spans more than a few hundred units, or distant scenery silently clips. `zoom` is the orthographic zoom in canvas pixels per world unit, read only when `projection` is "orthographic"; default 50. */
382
390
  frustum?: {
383
391
  fov?: number;
@@ -30,11 +30,18 @@ export type ChatSendResult = {
30
30
  export interface ChatSnapshot {
31
31
  messages: Record<string, ChatMessage[]>;
32
32
  counter: number;
33
+ rateWindows?: Record<string, Record<string, number[]>>;
33
34
  }
34
35
  export interface Chat {
35
36
  register(def: ChatChannelDef): void;
36
37
  channels(): ChatChannelDef[];
38
+ send(fromUserId: string, body: string): ChatSendResult;
37
39
  send(fromUserId: string, channelId: string, body: string): ChatSendResult;
40
+ recent(options?: {
41
+ limit?: number;
42
+ channelId?: string;
43
+ viewerUserId?: string;
44
+ }): ChatMessage[];
38
45
  whisper(fromUserId: string, toUserId: string, body: string): ChatSendResult;
39
46
  history(channelId: string, options?: {
40
47
  limit?: number;
@@ -57,12 +64,26 @@ export interface ChatDeps {
57
64
  }
58
65
  export declare const DEFAULT_CHAT_RATE_LIMIT: ChatRateLimit;
59
66
  export declare const DEFAULT_CHAT_HISTORY_LIMIT = 100;
60
- export declare const DEFAULT_CHAT_BODY_LENGTH = 500;
67
+ export declare const DEFAULT_CHAT_BODY_LENGTH = 240;
61
68
  export declare const DEFAULT_PROXIMITY_CHAT_RADIUS = 20;
62
69
  export declare const WHISPER_CHANNEL_PREFIX = "whisper:";
63
70
  export declare function whisperChannelId(a: string, b: string): string;
64
71
  export interface ChatRateLimiter {
65
72
  allow(key: string, atMs: number): boolean;
73
+ snapshot(): Record<string, number[]>;
74
+ restore(state: Record<string, number[]>): void;
66
75
  }
67
76
  export declare function createChatRateLimiter(limit: ChatRateLimit): ChatRateLimiter;
68
77
  export declare function createChat(deps: ChatDeps): Chat;
78
+ /** Sanitized chat text or a displayable validation failure. */
79
+ export type ChatValidation = {
80
+ ok: true;
81
+ text: string;
82
+ } | {
83
+ ok: false;
84
+ reason: string;
85
+ };
86
+ /** Strip control characters and enforce the shared chat message policy before storage or broadcast. */
87
+ export declare function validateChatMessage(value: unknown, options?: {
88
+ maxLength?: number;
89
+ }): ChatValidation;