@jgengine/core 0.7.0 → 0.8.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 (159) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +2 -0
  3. package/dist/ai/spawnDirector.d.ts +5 -0
  4. package/dist/ai/spawnDirector.js +13 -1
  5. package/dist/anim/easing.d.ts +14 -0
  6. package/dist/anim/easing.js +55 -0
  7. package/dist/behaviour/behaviour.d.ts +136 -0
  8. package/dist/behaviour/behaviour.js +388 -0
  9. package/dist/cards/cardPile.d.ts +1 -0
  10. package/dist/cards/cardPile.js +12 -3
  11. package/dist/combat/abilityKit.d.ts +5 -0
  12. package/dist/combat/abilityKit.js +10 -0
  13. package/dist/combat/effects.d.ts +1 -1
  14. package/dist/combat/effects.js +9 -6
  15. package/dist/combat/projectiles.d.ts +42 -5
  16. package/dist/combat/projectiles.js +98 -12
  17. package/dist/combat/telegraph.d.ts +5 -1
  18. package/dist/combat/telegraph.js +14 -0
  19. package/dist/commands/commandRegistry.d.ts +1 -1
  20. package/dist/commands/commandRegistry.js +2 -1
  21. package/dist/data/dataSource.d.ts +26 -0
  22. package/dist/data/dataSource.js +93 -0
  23. package/dist/data/devProxy.d.ts +11 -0
  24. package/dist/data/devProxy.js +38 -0
  25. package/dist/data/fetchJson.d.ts +20 -0
  26. package/dist/data/fetchJson.js +43 -0
  27. package/dist/data/jsonDataSource.d.ts +4 -0
  28. package/dist/data/jsonDataSource.js +6 -0
  29. package/dist/devtools/devtools.d.ts +157 -0
  30. package/dist/devtools/devtools.js +484 -0
  31. package/dist/devtools/transformTunables.d.ts +14 -0
  32. package/dist/devtools/transformTunables.js +38 -0
  33. package/dist/game/chat.d.ts +66 -0
  34. package/dist/game/chat.js +165 -0
  35. package/dist/game/defineGame.d.ts +8 -2
  36. package/dist/game/defineGame.js +2 -1
  37. package/dist/game/events.d.ts +25 -0
  38. package/dist/game/levelSequence.d.ts +44 -0
  39. package/dist/game/levelSequence.js +78 -0
  40. package/dist/game/playableGame.d.ts +237 -6
  41. package/dist/game/playableGame.js +6 -1
  42. package/dist/game/progression.d.ts +1 -0
  43. package/dist/game/progression.js +23 -2
  44. package/dist/game/race.d.ts +5 -0
  45. package/dist/game/race.js +20 -0
  46. package/dist/game/social.d.ts +42 -0
  47. package/dist/game/social.js +122 -0
  48. package/dist/game/spawnPoints.d.ts +19 -0
  49. package/dist/game/spawnPoints.js +23 -0
  50. package/dist/game/worldItem.d.ts +3 -0
  51. package/dist/game/worldItem.js +12 -6
  52. package/dist/input/actionBindings.d.ts +10 -0
  53. package/dist/input/actionBindings.js +14 -0
  54. package/dist/input/gestureSurface.d.ts +31 -0
  55. package/dist/input/gestureSurface.js +120 -0
  56. package/dist/input/pointer.d.ts +45 -0
  57. package/dist/input/pointer.js +48 -0
  58. package/dist/input/touchScheme.d.ts +83 -0
  59. package/dist/input/touchScheme.js +115 -0
  60. package/dist/item/affix.d.ts +1 -1
  61. package/dist/item/affix.js +1 -19
  62. package/dist/meta/changelog.d.ts +1 -1
  63. package/dist/meta/changelog.js +37 -1
  64. package/dist/movement/movementModel.d.ts +36 -3
  65. package/dist/movement/movementModel.js +81 -5
  66. package/dist/movement/voxelController.d.ts +61 -0
  67. package/dist/movement/voxelController.js +229 -0
  68. package/dist/multiplayer/chatContract.d.ts +42 -0
  69. package/dist/multiplayer/chatContract.js +57 -0
  70. package/dist/multiplayer/identity.d.ts +13 -0
  71. package/dist/multiplayer/identity.js +20 -0
  72. package/dist/multiplayer/poseSyncGate.d.ts +1 -0
  73. package/dist/multiplayer/poseSyncGate.js +13 -1
  74. package/dist/multiplayer/presenceModel.d.ts +3 -0
  75. package/dist/multiplayer/presenceModel.js +15 -1
  76. package/dist/multiplayer/voiceContract.d.ts +44 -0
  77. package/dist/multiplayer/voiceContract.js +108 -0
  78. package/dist/nav/navConstrain.d.ts +14 -0
  79. package/dist/nav/navConstrain.js +26 -0
  80. package/dist/nav/navFromEnvironment.d.ts +11 -0
  81. package/dist/nav/navFromEnvironment.js +16 -0
  82. package/dist/nav/navGrid.d.ts +10 -0
  83. package/dist/nav/navGrid.js +18 -1
  84. package/dist/nav/pathFollow.d.ts +4 -2
  85. package/dist/nav/pathFollow.js +5 -3
  86. package/dist/physics/ballisticSweep.d.ts +18 -0
  87. package/dist/physics/ballisticSweep.js +49 -0
  88. package/dist/physics/forceVolume.js +5 -5
  89. package/dist/physics/physicsWorld.d.ts +41 -2
  90. package/dist/physics/physicsWorld.js +232 -40
  91. package/dist/physics/spatialGrid.d.ts +1 -1
  92. package/dist/physics/spatialGrid.js +5 -0
  93. package/dist/physics/vehicleBody.d.ts +6 -0
  94. package/dist/physics/vehicleBody.js +16 -17
  95. package/dist/puzzle/cellGrid.d.ts +26 -0
  96. package/dist/puzzle/cellGrid.js +110 -0
  97. package/dist/puzzle/fallingPiece.d.ts +24 -0
  98. package/dist/puzzle/fallingPiece.js +51 -0
  99. package/dist/random/nameGen.d.ts +18 -0
  100. package/dist/random/nameGen.js +35 -0
  101. package/dist/random/rng.d.ts +2 -0
  102. package/dist/random/rng.js +23 -0
  103. package/dist/runtime/adapter.d.ts +35 -0
  104. package/dist/runtime/adapter.js +37 -1
  105. package/dist/runtime/cameraDirector.d.ts +10 -0
  106. package/dist/runtime/cameraDirector.js +25 -0
  107. package/dist/runtime/gameContext.d.ts +44 -2
  108. package/dist/runtime/gameContext.js +100 -4
  109. package/dist/runtime/hostPersistence.d.ts +21 -0
  110. package/dist/runtime/hostPersistence.js +50 -0
  111. package/dist/runtime/inputSnapshot.d.ts +7 -0
  112. package/dist/runtime/inputSnapshot.js +12 -0
  113. package/dist/runtime/motionIntents.d.ts +25 -0
  114. package/dist/runtime/motionIntents.js +25 -0
  115. package/dist/runtime/transport.d.ts +32 -0
  116. package/dist/scene/entityStore.d.ts +8 -0
  117. package/dist/scene/entityStore.js +25 -2
  118. package/dist/scene/movementSpeed.d.ts +35 -0
  119. package/dist/scene/movementSpeed.js +20 -0
  120. package/dist/scene/objectQuery.d.ts +18 -0
  121. package/dist/scene/objectQuery.js +108 -0
  122. package/dist/scene/objectStore.d.ts +11 -0
  123. package/dist/scene/objectStore.js +109 -4
  124. package/dist/scene/paintLayer.d.ts +15 -0
  125. package/dist/scene/paintLayer.js +53 -0
  126. package/dist/scene/spatial.d.ts +21 -0
  127. package/dist/scene/spatial.js +63 -1
  128. package/dist/sensor/concealment.d.ts +21 -0
  129. package/dist/sensor/concealment.js +63 -0
  130. package/dist/sensor/freezeMonitor.d.ts +17 -0
  131. package/dist/sensor/freezeMonitor.js +35 -0
  132. package/dist/session/roles.d.ts +6 -0
  133. package/dist/session/roles.js +32 -0
  134. package/dist/session/roundState.d.ts +41 -17
  135. package/dist/session/roundState.js +67 -38
  136. package/dist/tactics/fallingGrid.d.ts +62 -0
  137. package/dist/tactics/fallingGrid.js +158 -0
  138. package/dist/tactics/tacticalGrid.d.ts +11 -0
  139. package/dist/tactics/tacticalGrid.js +17 -0
  140. package/dist/time/simClock.d.ts +25 -1
  141. package/dist/time/simClock.js +28 -3
  142. package/dist/turn/intent.d.ts +14 -0
  143. package/dist/turn/intent.js +29 -0
  144. package/dist/turn/turnLoop.d.ts +2 -0
  145. package/dist/turn/turnLoop.js +8 -1
  146. package/dist/world/environmentSummary.d.ts +53 -0
  147. package/dist/world/environmentSummary.js +129 -0
  148. package/dist/world/features.d.ts +87 -6
  149. package/dist/world/features.js +52 -2
  150. package/dist/world/gridInstances.d.ts +13 -0
  151. package/dist/world/gridInstances.js +22 -0
  152. package/dist/world/placementController.d.ts +22 -1
  153. package/dist/world/placementController.js +56 -0
  154. package/dist/world/terrain.d.ts +23 -1
  155. package/dist/world/terrain.js +64 -1
  156. package/dist/world/voxelField.d.ts +54 -0
  157. package/dist/world/voxelField.js +241 -0
  158. package/llms.txt +4102 -0
  159. package/package.json +3 -2
@@ -0,0 +1,51 @@
1
+ import { cellAt } from "./cellGrid.js";
2
+ export function pieceCells(table, piece) {
3
+ const rotations = table[piece.shape];
4
+ const index = ((piece.rotation % rotations.length) + rotations.length) % rotations.length;
5
+ const state = rotations[index];
6
+ return state.map(([ox, oy]) => [piece.x + ox, piece.y + oy]);
7
+ }
8
+ export function pieceCollides(grid, table, piece) {
9
+ for (const [x, y] of pieceCells(table, piece)) {
10
+ if (x < 0 || x >= grid.width || y >= grid.height)
11
+ return true;
12
+ if (y >= 0 && cellAt(grid, x, y) !== null)
13
+ return true;
14
+ }
15
+ return false;
16
+ }
17
+ export function mergePiece(grid, table, piece, value) {
18
+ const cells = grid.cells.slice();
19
+ for (const [x, y] of pieceCells(table, piece)) {
20
+ if (x >= 0 && x < grid.width && y >= 0 && y < grid.height)
21
+ cells[y * grid.width + x] = value;
22
+ }
23
+ return { width: grid.width, height: grid.height, cells };
24
+ }
25
+ export function dropDistance(grid, table, piece) {
26
+ let distance = 0;
27
+ while (!pieceCollides(grid, table, { ...piece, y: piece.y + distance + 1 }))
28
+ distance += 1;
29
+ return distance;
30
+ }
31
+ export function gravityInterval(level, base = 0.8, perLevel = 0.07, min = 0.05) {
32
+ return Math.max(min, base - level * perLevel);
33
+ }
34
+ export function levelForLines(lines, linesPerLevel = 10) {
35
+ return Math.floor(lines / linesPerLevel);
36
+ }
37
+ const CLASSIC_LINE_SCORE = [0, 40, 100, 300, 1200];
38
+ export function lineScore(cleared, level, table = CLASSIC_LINE_SCORE) {
39
+ return (table[cleared] ?? 0) * (level + 1);
40
+ }
41
+ export function createLockDelay(delaySeconds) {
42
+ return { delaySeconds, elapsed: 0 };
43
+ }
44
+ export function stepLockDelay(state, grounded, dt) {
45
+ if (!grounded)
46
+ return { state: { ...state, elapsed: 0 }, locked: false };
47
+ const elapsed = state.elapsed + dt;
48
+ if (elapsed >= state.delaySeconds)
49
+ return { state: { ...state, elapsed: 0 }, locked: true };
50
+ return { state: { ...state, elapsed }, locked: false };
51
+ }
@@ -0,0 +1,18 @@
1
+ export interface SyllableBank {
2
+ onset?: readonly string[];
3
+ nucleus: readonly string[];
4
+ coda?: readonly string[];
5
+ }
6
+ export interface NameGeneratorOptions {
7
+ rng: () => number;
8
+ syllables: SyllableBank;
9
+ minSyllables?: number;
10
+ maxSyllables?: number;
11
+ capitalize?: boolean;
12
+ }
13
+ export interface NameGenerator {
14
+ name(): string;
15
+ }
16
+ export declare function pickFrom(rng: () => number, bank: readonly string[]): string;
17
+ export declare function createNameGenerator(options: NameGeneratorOptions): NameGenerator;
18
+ export declare function fillTemplate(template: string, vars: Record<string, () => string> | Record<string, string>): string;
@@ -0,0 +1,35 @@
1
+ export function pickFrom(rng, bank) {
2
+ if (bank.length === 0)
3
+ throw new Error("pickFrom: bank must not be empty");
4
+ return bank[Math.floor(rng() * bank.length)];
5
+ }
6
+ export function createNameGenerator(options) {
7
+ const { rng, syllables } = options;
8
+ const minSyllables = Math.max(1, options.minSyllables ?? 2);
9
+ const maxSyllables = Math.max(minSyllables, options.maxSyllables ?? minSyllables);
10
+ const capitalize = options.capitalize ?? true;
11
+ function syllable() {
12
+ const onset = syllables.onset === undefined || syllables.onset.length === 0 ? "" : pickFrom(rng, syllables.onset);
13
+ const nucleus = pickFrom(rng, syllables.nucleus);
14
+ const coda = syllables.coda === undefined || syllables.coda.length === 0 ? "" : pickFrom(rng, syllables.coda);
15
+ return `${onset}${nucleus}${coda}`;
16
+ }
17
+ return {
18
+ name() {
19
+ const span = maxSyllables - minSyllables;
20
+ const count = minSyllables + (span === 0 ? 0 : Math.floor(rng() * (span + 1)));
21
+ let result = "";
22
+ for (let i = 0; i < count; i++)
23
+ result += syllable();
24
+ return capitalize && result.length > 0 ? result.charAt(0).toUpperCase() + result.slice(1) : result;
25
+ },
26
+ };
27
+ }
28
+ export function fillTemplate(template, vars) {
29
+ return template.replace(/\{(\w+)\}/g, (match, key) => {
30
+ if (!(key in vars))
31
+ return match;
32
+ const value = vars[key];
33
+ return typeof value === "function" ? value() : value;
34
+ });
35
+ }
@@ -0,0 +1,2 @@
1
+ export declare function seededRng(seed: string | number): () => number;
2
+ export declare function seededStreams(seed: string | number): (stream: string) => () => number;
@@ -0,0 +1,23 @@
1
+ function hashSeed(seed) {
2
+ const text = typeof seed === "number" ? seed.toString() : seed;
3
+ let h = 2166136261 >>> 0;
4
+ for (let i = 0; i < text.length; i++) {
5
+ h ^= text.charCodeAt(i);
6
+ h = Math.imul(h, 16777619);
7
+ }
8
+ return h >>> 0;
9
+ }
10
+ export function seededRng(seed) {
11
+ let a = hashSeed(seed) >>> 0;
12
+ return () => {
13
+ a = (a + 0x6d2b79f5) >>> 0;
14
+ let t = a;
15
+ t = Math.imul(t ^ (t >>> 15), t | 1);
16
+ t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
17
+ return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
18
+ };
19
+ }
20
+ export function seededStreams(seed) {
21
+ const base = typeof seed === "number" ? seed.toString() : seed;
22
+ return (stream) => seededRng(`${base}:${stream}`);
23
+ }
@@ -5,6 +5,20 @@ export type MultiplayerAdapterConfig = {
5
5
  } | {
6
6
  kind: "ws";
7
7
  topology?: MultiplayerTopology;
8
+ url?: string;
9
+ } | {
10
+ kind: "socketio";
11
+ topology?: MultiplayerTopology;
12
+ url?: string;
13
+ } | {
14
+ kind: "p2p";
15
+ topology?: MultiplayerTopology;
16
+ room?: string;
17
+ } | {
18
+ kind: "lan";
19
+ topology?: MultiplayerTopology;
20
+ port?: number;
21
+ path?: string;
8
22
  } | {
9
23
  kind: "offline";
10
24
  };
@@ -13,8 +27,29 @@ export declare function convex(config?: {
13
27
  }): MultiplayerAdapterConfig;
14
28
  export declare function ws(config?: {
15
29
  topology?: MultiplayerTopology;
30
+ url?: string;
31
+ }): MultiplayerAdapterConfig;
32
+ export declare function fly(config: {
33
+ app: string;
34
+ topology?: MultiplayerTopology;
35
+ path?: string;
36
+ }): MultiplayerAdapterConfig;
37
+ export declare function socketIo(config?: {
38
+ topology?: MultiplayerTopology;
39
+ url?: string;
40
+ }): MultiplayerAdapterConfig;
41
+ export declare function p2p(config?: {
42
+ topology?: MultiplayerTopology;
43
+ room?: string;
44
+ }): MultiplayerAdapterConfig;
45
+ export declare function lan(config?: {
46
+ topology?: MultiplayerTopology;
47
+ port?: number;
48
+ path?: string;
16
49
  }): MultiplayerAdapterConfig;
17
50
  export declare function offline(): MultiplayerAdapterConfig;
51
+ export declare function adapterOf(multiplayer: unknown): MultiplayerAdapterConfig | null;
52
+ export declare function multiplayerAdapterKind(multiplayer: unknown): string | null;
18
53
  export type ServersPoolConfig = {
19
54
  maxServers: number;
20
55
  slotsPerServer: number;
@@ -2,11 +2,47 @@ export function convex(config) {
2
2
  return { kind: "convex", topology: config?.topology ?? "shared" };
3
3
  }
4
4
  export function ws(config) {
5
- return { kind: "ws", topology: config?.topology ?? "shared" };
5
+ return { kind: "ws", topology: config?.topology ?? "shared", url: config?.url };
6
+ }
7
+ export function fly(config) {
8
+ return {
9
+ kind: "ws",
10
+ topology: config.topology ?? "shared",
11
+ url: `wss://${config.app}.fly.dev${config.path ?? "/ws"}`,
12
+ };
13
+ }
14
+ export function socketIo(config) {
15
+ return { kind: "socketio", topology: config?.topology ?? "shared", url: config?.url };
16
+ }
17
+ export function p2p(config) {
18
+ return { kind: "p2p", topology: config?.topology ?? "private", room: config?.room };
19
+ }
20
+ export function lan(config) {
21
+ return { kind: "lan", topology: config?.topology ?? "shared", port: config?.port, path: config?.path };
6
22
  }
7
23
  export function offline() {
8
24
  return { kind: "offline" };
9
25
  }
26
+ const ADAPTER_KINDS = new Set(["convex", "ws", "socketio", "p2p", "lan", "offline"]);
27
+ function isAdapterConfig(value) {
28
+ if (typeof value !== "object" || value === null)
29
+ return false;
30
+ const kind = value.kind;
31
+ return typeof kind === "string" && ADAPTER_KINDS.has(kind);
32
+ }
33
+ export function adapterOf(multiplayer) {
34
+ if (isAdapterConfig(multiplayer))
35
+ return multiplayer;
36
+ if (typeof multiplayer === "object" && multiplayer !== null) {
37
+ const nested = multiplayer.adapter;
38
+ if (isAdapterConfig(nested))
39
+ return nested;
40
+ }
41
+ return null;
42
+ }
43
+ export function multiplayerAdapterKind(multiplayer) {
44
+ return adapterOf(multiplayer)?.kind ?? null;
45
+ }
10
46
  export function servers(config) {
11
47
  return config;
12
48
  }
@@ -0,0 +1,10 @@
1
+ import type { CinematicCameraConfig } from "../game/playableGame.js";
2
+ export interface CameraDirector {
3
+ follow(entityId: string | null): void;
4
+ /** `undefined` means no runtime override — the shell falls back to the static `playable.camera.followEntityId`. `null` means explicitly follow nothing. */
5
+ followedEntityId(): string | null | undefined;
6
+ setCinematic(config: CinematicCameraConfig | null): void;
7
+ cinematic(): CinematicCameraConfig | null;
8
+ subscribe(listener: () => void): () => void;
9
+ }
10
+ export declare function createCameraDirector(): CameraDirector;
@@ -0,0 +1,25 @@
1
+ export function createCameraDirector() {
2
+ const listeners = new Set();
3
+ let followEntity = undefined;
4
+ let cinematicConfig = null;
5
+ function notify() {
6
+ for (const listener of listeners)
7
+ listener();
8
+ }
9
+ return {
10
+ follow(entityId) {
11
+ followEntity = entityId;
12
+ notify();
13
+ },
14
+ followedEntityId: () => followEntity,
15
+ setCinematic(config) {
16
+ cinematicConfig = config;
17
+ notify();
18
+ },
19
+ cinematic: () => cinematicConfig,
20
+ subscribe(listener) {
21
+ listeners.add(listener);
22
+ return () => listeners.delete(listener);
23
+ },
24
+ };
25
+ }
@@ -1,3 +1,4 @@
1
+ import { type CardPile, type CardPileConfig } from "../cards/cardPile.js";
1
2
  import { type OnDeathSpec } from "../combat/death.js";
2
3
  import { type EffectInput, type EffectResult, type EffectVia, type ReceiveMap, type SingleTargetEffectInput } from "../combat/effects.js";
3
4
  import { type ProjectileSystem } from "../combat/projectiles.js";
@@ -6,6 +7,7 @@ import { type TelegraphShape } from "../combat/telegraph.js";
6
7
  import { type CommandDefinition, type CommandResult } from "../commands/commandRegistry.js";
7
8
  import { type Cosmetics } from "../game/cosmetics.js";
8
9
  import type { GameDefinition } from "../game/defineGame.js";
10
+ import { type TerrainField } from "../world/terrain.js";
9
11
  import { type GameEventMap, type GameEvents } from "../game/events.js";
10
12
  import { type GameFeed } from "../game/feed.js";
11
13
  import { type Leaderboard } from "../game/leaderboard.js";
@@ -13,6 +15,7 @@ import { type Loadouts } from "../game/loadout.js";
13
15
  import { type Drop, type LootTableDef } from "../game/lootTable.js";
14
16
  import { type QuestJournal } from "../game/quest.js";
15
17
  import { type WorldItemRecord, type WorldItemSpawnInput } from "../game/worldItem.js";
18
+ import { type Chat } from "../game/chat.js";
16
19
  import { type Social } from "../game/social.js";
17
20
  import { type TradeField, type TradeSystem } from "../game/trade.js";
18
21
  import { type Unlocks } from "../game/unlocks.js";
@@ -23,16 +26,23 @@ import { type ItemUseHandler, type ItemUseInput, type ItemUseRejection, type Ite
23
26
  import { type WeaponStats } from "../item/weapon.js";
24
27
  import { type PoseAllowedStates, type PoseState } from "../movement/poseState.js";
25
28
  import type { ModelAssetRef } from "../scene/assetCatalog.js";
29
+ import { type PaintLayer } from "../scene/paintLayer.js";
26
30
  import { type EntityStatsApi, type StatCatalog } from "../scene/entityStats.js";
27
- import type { EntityPose, EntityPosition, SceneEntity, SpawnOptions } from "../scene/entityStore.js";
31
+ import type { EntityPose, EntityPosition, EntityStore, SceneEntity, SpawnOptions, SpawnPose } from "../scene/entityStore.js";
28
32
  import { type Forms } from "../scene/form.js";
33
+ import { type ObjectRaycastHit, type ObjectRaycastInput } from "../scene/objectQuery.js";
29
34
  import { type ObjectStore } from "../scene/objectStore.js";
30
35
  import { type Roster } from "../scene/roster.js";
31
36
  import { type Possession } from "../scene/possession.js";
32
37
  import { type SpatialApi } from "../scene/spatial.js";
33
38
  import { type CycleTargetOptions } from "../scene/targeting.js";
34
39
  import { type Stats } from "../stats/statModifiers.js";
40
+ import { type ObservableKeyedStore } from "../store/observableKeyedStore.js";
35
41
  import { type SimClock } from "../time/simClock.js";
42
+ import { type TurnLoop, type TurnLoopConfig } from "../turn/turnLoop.js";
43
+ import { type CameraDirector } from "./cameraDirector.js";
44
+ import { type InputSnapshot } from "./inputSnapshot.js";
45
+ import { type MotionIntents } from "./motionIntents.js";
36
46
  export interface GameContextItemEntry {
37
47
  use?: string;
38
48
  weapon?: Record<string, unknown>;
@@ -79,6 +89,8 @@ export interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAsset
79
89
  }
80
90
  export interface SceneObjectContext extends ObjectStore {
81
91
  catalog(instanceId: string): GameContextObjectEntry | null;
92
+ raycast(input: ObjectRaycastInput): ObjectRaycastHit | null;
93
+ raycastAll(input: ObjectRaycastInput): readonly ObjectRaycastHit[];
82
94
  }
83
95
  export interface FloatTextInput {
84
96
  instanceId?: string;
@@ -115,9 +127,13 @@ export interface HitReactionInput {
115
127
  export interface SceneEntityContext {
116
128
  spawn(name: string, options?: SpawnOptions): string;
117
129
  despawn(instanceId: string): boolean;
130
+ update: EntityStore["update"];
118
131
  setPose(instanceId: string, pose: EntityPose): boolean;
119
132
  get(instanceId: string): SceneEntity | null;
120
133
  list(): readonly SceneEntity[];
134
+ spawnPoseOf(instanceId: string): SpawnPose | null;
135
+ resetToSpawn(instanceId: string): boolean;
136
+ resetAllToSpawn(filter?: (entity: SceneEntity) => boolean): number;
121
137
  stats: EntityStatsApi;
122
138
  floatText(input: FloatTextInput): void;
123
139
  telegraph(input: TelegraphInput): () => void;
@@ -125,7 +141,7 @@ export interface SceneEntityContext {
125
141
  setTarget(fromId: string, toId: string | null): void;
126
142
  getTarget(fromId: string): string | null;
127
143
  cycleTarget(fromId: string, options?: CycleTargetOptions): string | null;
128
- canReceive(instanceId: string, effect: string): string | null;
144
+ canReceive(instanceId: string, effect: string, magnitude?: number): string | null;
129
145
  preview(input: SingleTargetEffectInput): number;
130
146
  effect(input: EffectInput): EffectResult[];
131
147
  willHitProjectile: ProjectileSystem["willHitProjectile"];
@@ -137,6 +153,7 @@ export interface SceneEntityContext {
137
153
  queryArc: SpatialApi["queryArc"];
138
154
  moveToward: SpatialApi["moveToward"];
139
155
  form: Forms;
156
+ paint: PaintLayer;
140
157
  }
141
158
  export type WorldItemPickupResult = {
142
159
  status: "ok";
@@ -151,6 +168,7 @@ export interface SceneWorldItemContext {
151
168
  list(): readonly WorldItemRecord[];
152
169
  nearestInRadius(from: EntityPosition, radius: number, filter?: (record: WorldItemRecord) => boolean): string | null;
153
170
  pickup(instanceId: string, userId: string): WorldItemPickupResult;
171
+ consume(instanceId: string): WorldItemRecord | null;
154
172
  }
155
173
  export interface GameContextCommands {
156
174
  define<TInput>(name: string, definition: CommandDefinition<GameContext, TInput>): void;
@@ -180,12 +198,25 @@ export interface GameContextItemUse {
180
198
  can(input: ItemUseInput): ItemUseRejection | null;
181
199
  use(input: ItemUseInput): ItemUseResult<GameContext>;
182
200
  }
201
+ export interface GameContextWorld {
202
+ ground: TerrainField;
203
+ groundHeightAt(x: number, z: number): number;
204
+ }
205
+ export interface GameContextCards {
206
+ /** Lazily creates (on first call, `config` required) or returns the existing notify-wrapped pile for `id`. */
207
+ pile(id: string, config?: CardPileConfig): CardPile;
208
+ }
209
+ export interface GameContextTurn {
210
+ /** Lazily creates (on first call, `config` required) or returns the existing notify-wrapped turn loop for `id`. */
211
+ loop(id: string, config?: TurnLoopConfig): TurnLoop;
212
+ }
183
213
  export interface GameContext {
184
214
  scene: {
185
215
  object: SceneObjectContext;
186
216
  entity: SceneEntityContext;
187
217
  worldItem: SceneWorldItemContext;
188
218
  };
219
+ world: GameContextWorld;
189
220
  game: {
190
221
  commands: GameContextCommands;
191
222
  events: GameEvents;
@@ -194,10 +225,15 @@ export interface GameContext {
194
225
  trade: TradeSystem;
195
226
  quest: QuestJournal;
196
227
  social: Social;
228
+ chat: Chat;
197
229
  unlocks: Unlocks;
198
230
  economy: GameContextEconomy;
199
231
  leaderboard: Leaderboard;
200
232
  roster: Roster;
233
+ /** Game-defined keyed reactive store slot (#163.1); mutations bump `ctx.version()`/notify `ctx.subscribe`. */
234
+ store: ObservableKeyedStore<unknown>;
235
+ cards: GameContextCards;
236
+ turn: GameContextTurn;
201
237
  };
202
238
  player: {
203
239
  userId: string;
@@ -211,12 +247,18 @@ export interface GameContext {
211
247
  movement: PoseState;
212
248
  possession: Possession;
213
249
  cosmetics: Cosmetics;
250
+ /** Vertical-motion seam into the shell-owned FrameDriver (#162.4); see `MotionIntents`. */
251
+ motion: MotionIntents;
214
252
  };
215
253
  item: {
216
254
  use: GameContextItemUse;
217
255
  weapon: WeaponStats;
218
256
  };
219
257
  time: SimClock;
258
+ /** Runtime camera-follow/cinematic override (#196.2); the shell reads `followedEntityId()` each frame. */
259
+ camera: CameraDirector;
260
+ /** Held-input snapshot published by the shell each frame before `onTick` (#164.1). */
261
+ input: InputSnapshot;
220
262
  subscribe(listener: () => void): () => void;
221
263
  version(): number;
222
264
  }
@@ -1,3 +1,4 @@
1
+ import { createCardPile } from "../cards/cardPile.js";
1
2
  import { createDeathSystem, deathReasonFromEffect, normalizeOnDeath } from "../combat/death.js";
2
3
  import { createEffectSystem, } from "../combat/effects.js";
3
4
  import { createProjectileSystem } from "../combat/projectiles.js";
@@ -6,6 +7,7 @@ import { pointInTelegraph, } from "../combat/telegraph.js";
6
7
  import { createCommandRegistry, } from "../commands/commandRegistry.js";
7
8
  import { balance as walletBalance, canAfford as walletCanAfford, charge as walletCharge, chargeAll as walletChargeAll, createEmptyWallet, grant as walletGrant, } from "../economy/wallet.js";
8
9
  import { createCosmetics } from "../game/cosmetics.js";
10
+ import { groundFieldFor } from "../world/terrain.js";
9
11
  import { createGameEvents } from "../game/events.js";
10
12
  import { createGameFeed } from "../game/feed.js";
11
13
  import { createLeaderboard } from "../game/leaderboard.js";
@@ -13,6 +15,7 @@ import { createLoadouts } from "../game/loadout.js";
13
15
  import { createLootRegistry, grantDrops } from "../game/lootTable.js";
14
16
  import { createQuestJournal } from "../game/quest.js";
15
17
  import { createWorldItemStore, resolveDeathDrops, DEFAULT_RARITY, WORLD_ITEM_ENTITY_NAME, } from "../game/worldItem.js";
18
+ import { createChat } from "../game/chat.js";
16
19
  import { createSocial } from "../game/social.js";
17
20
  import { createTradeSystem } from "../game/trade.js";
18
21
  import { createUnlocks } from "../game/unlocks.js";
@@ -20,8 +23,10 @@ import { createInventorySet, putItem, } from "../inventory/inventoryModel.js";
20
23
  import { createItemUse, } from "../item/use.js";
21
24
  import { createWeaponStats } from "../item/weapon.js";
22
25
  import { createPoseState } from "../movement/poseState.js";
26
+ import { createPaintLayer } from "../scene/paintLayer.js";
23
27
  import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
24
28
  import { createForms } from "../scene/form.js";
29
+ import { raycastObjects, raycastObjectsAll } from "../scene/objectQuery.js";
25
30
  import { createObjectStore } from "../scene/objectStore.js";
26
31
  import { createRoster } from "../scene/roster.js";
27
32
  import { createPossession } from "../scene/possession.js";
@@ -29,12 +34,18 @@ import { createSpatialApi } from "../scene/spatial.js";
29
34
  import { createTargeting } from "../scene/targeting.js";
30
35
  import { createStats } from "../stats/statModifiers.js";
31
36
  import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
37
+ import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
32
38
  import { createSimClock } from "../time/simClock.js";
39
+ import { createTurnLoop } from "../turn/turnLoop.js";
40
+ import { createCameraDirector } from "./cameraDirector.js";
41
+ import { createInputSnapshot } from "./inputSnapshot.js";
42
+ import { createMotionIntents } from "./motionIntents.js";
33
43
  export function createGameContext(options) {
34
44
  const { definition, content, player } = options;
35
45
  const now = options.now ?? Date.now;
36
46
  const signal = createChangeSignal();
37
47
  const time = createSimClock({ config: definition.time, onChange: signal.notify });
48
+ const ground = groundFieldFor(definition.world);
38
49
  const entities = definition.scene;
39
50
  const objects = createObjectStore();
40
51
  entities.subscribe(signal.notify);
@@ -84,7 +95,7 @@ export function createGameContext(options) {
84
95
  signal.notify();
85
96
  },
86
97
  };
87
- const feed = createGameFeed();
98
+ const feed = createGameFeed(definition.feed);
88
99
  const lootRegistry = createLootRegistry();
89
100
  const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], signal.notify);
90
101
  const rawSocial = createSocial({
@@ -96,11 +107,22 @@ export function createGameContext(options) {
96
107
  },
97
108
  });
98
109
  const social = {
99
- friends: notifyAfter(rawSocial.friends, ["request", "accept", "remove", "block", "hydrate"], signal.notify),
100
- party: notifyAfter(rawSocial.party, ["invite", "accept", "kick", "leave", "promote"], signal.notify),
110
+ friends: notifyAfter(rawSocial.friends, ["request", "accept", "decline", "remove", "block", "hydrate"], signal.notify),
111
+ party: notifyAfter(rawSocial.party, ["invite", "accept", "decline", "kick", "leave", "promote"], signal.notify),
101
112
  presence: rawSocial.presence,
102
113
  emotes: rawSocial.emotes,
114
+ worldInvites: notifyAfter(rawSocial.worldInvites, ["invite", "accept", "decline"], signal.notify),
103
115
  };
116
+ const chat = notifyAfter(createChat({
117
+ events,
118
+ now,
119
+ party: rawSocial.party,
120
+ proximity: {
121
+ entities: { get: (id) => entities.get(id) },
122
+ spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
123
+ },
124
+ blockedBy: (userId) => rawSocial.friends.snapshot(userId).blocked,
125
+ }), ["register", "send", "whisper", "hydrate"], signal.notify);
104
126
  const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify);
105
127
  const roster = notifyAfter(createRoster({ now }), ["capture", "release", "setEquipped", "hydrate"], signal.notify);
106
128
  const playerStats = createStats({});
@@ -110,6 +132,7 @@ export function createGameContext(options) {
110
132
  const possession = notifyAfter(createPossession({ entities, events }), ["possess", "own", "disown"], signal.notify);
111
133
  const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signal.notify);
112
134
  const cosmetics = notifyAfter(createCosmetics({ events }), ["apply", "equip", "hydrate"], signal.notify);
135
+ const paintLayer = notifyAfter(createPaintLayer(), ["paint", "clear"], signal.notify);
113
136
  const inventoryDeclarations = definition.inventories ?? {};
114
137
  const inventoryIds = Object.keys(inventoryDeclarations);
115
138
  const layouts = {};
@@ -261,11 +284,21 @@ export function createGameContext(options) {
261
284
  pose.clear(instanceId);
262
285
  return existed;
263
286
  }
287
+ function resetAllToSpawn(filter) {
288
+ let count = 0;
289
+ for (const entity of entities.list()) {
290
+ if (filter !== undefined && !filter(entity))
291
+ continue;
292
+ if (entities.resetToSpawn(entity.id))
293
+ count += 1;
294
+ }
295
+ return count;
296
+ }
264
297
  const worldItems = notifyAfter(createWorldItemStore({
265
298
  spawnEntity: (position) => entities.spawn(WORLD_ITEM_ENTITY_NAME, { position, role: "prop" }),
266
299
  despawnEntity,
267
300
  resolvePosition: (instanceId) => entities.get(instanceId)?.position,
268
- }), ["spawn", "take"], signal.notify);
301
+ }), ["spawn", "take", "remove"], signal.notify);
269
302
  function spawnWorldItem(input) {
270
303
  const record = worldItems.spawn(input);
271
304
  events.emit("worldItem.dropped", {
@@ -484,16 +517,66 @@ export function createGameContext(options) {
484
517
  const sceneObjects = {
485
518
  ...objects,
486
519
  catalog: (instanceId) => catalogObject(instanceId) ?? null,
520
+ raycast: (input) => raycastObjects(objects.list(), input),
521
+ raycastAll: (input) => raycastObjectsAll(objects.list(), input),
487
522
  };
523
+ const store = notifyAfter(createObservableKeyedStore(), ["set", "delete"], signal.notify);
524
+ const cardPiles = new Map();
525
+ function pile(id, config) {
526
+ const existing = cardPiles.get(id);
527
+ if (existing !== undefined)
528
+ return existing;
529
+ if (config === undefined) {
530
+ throw new Error(`cardPile "${id}" has not been created yet; pass a config on first access`);
531
+ }
532
+ const created = notifyAfter(createCardPile(config), ["shuffle", "draw", "discard", "exhaust", "move", "reset"], signal.notify);
533
+ cardPiles.set(id, created);
534
+ return created;
535
+ }
536
+ const turnLoops = new Map();
537
+ function loop(id, config) {
538
+ const existing = turnLoops.get(id);
539
+ if (existing !== undefined)
540
+ return existing;
541
+ if (config === undefined) {
542
+ throw new Error(`turn loop "${id}" has not been created yet; pass a config on first access`);
543
+ }
544
+ const raw = createTurnLoop(config);
545
+ const wrappedCommit = notifyAfter(raw.commit, ["submit", "expected", "commit", "rewind", "clear"], signal.notify);
546
+ const wrapped = {
547
+ ...notifyAfter(raw, [
548
+ "setOrder",
549
+ "addParticipant",
550
+ "removeParticipant",
551
+ "advancePhase",
552
+ "advanceTurn",
553
+ "advanceRound",
554
+ "spend",
555
+ "gain",
556
+ "refill",
557
+ "restore",
558
+ ], signal.notify),
559
+ commit: wrappedCommit,
560
+ };
561
+ turnLoops.set(id, wrapped);
562
+ return wrapped;
563
+ }
564
+ const camera = notifyAfter(createCameraDirector(), ["follow", "setCinematic"], signal.notify);
565
+ const input = createInputSnapshot();
566
+ const motion = createMotionIntents();
488
567
  const ctx = {
489
568
  scene: {
490
569
  object: sceneObjects,
491
570
  entity: {
492
571
  spawn: spawnEntity,
493
572
  despawn: despawnEntity,
573
+ update: entities.update,
494
574
  setPose: entities.setPose,
495
575
  get: entities.get,
496
576
  list: entities.list,
577
+ spawnPoseOf: entities.spawnPoseOf,
578
+ resetToSpawn: entities.resetToSpawn,
579
+ resetAllToSpawn,
497
580
  stats: entityStats,
498
581
  floatText: emitFloatText,
499
582
  telegraph: fireTelegraph,
@@ -513,6 +596,7 @@ export function createGameContext(options) {
513
596
  queryArc: spatial.queryArc,
514
597
  moveToward: spatial.moveToward,
515
598
  form: forms,
599
+ paint: paintLayer,
516
600
  },
517
601
  worldItem: {
518
602
  spawn: spawnWorldItem,
@@ -520,8 +604,13 @@ export function createGameContext(options) {
520
604
  list: worldItems.list,
521
605
  nearestInRadius: worldItems.nearestInRadius,
522
606
  pickup: pickupWorldItem,
607
+ consume: worldItems.remove,
523
608
  },
524
609
  },
610
+ world: {
611
+ ground,
612
+ groundHeightAt: ground.sampleHeight,
613
+ },
525
614
  game: {
526
615
  commands: {
527
616
  define: commandRegistry.define,
@@ -552,10 +641,14 @@ export function createGameContext(options) {
552
641
  trade,
553
642
  quest,
554
643
  social,
644
+ chat,
555
645
  unlocks,
556
646
  economy,
557
647
  leaderboard,
558
648
  roster,
649
+ store,
650
+ cards: { pile },
651
+ turn: { loop },
559
652
  },
560
653
  player: {
561
654
  userId: player.userId,
@@ -567,6 +660,7 @@ export function createGameContext(options) {
567
660
  movement: pose,
568
661
  possession,
569
662
  cosmetics,
663
+ motion,
570
664
  },
571
665
  item: {
572
666
  use: {
@@ -582,6 +676,8 @@ export function createGameContext(options) {
582
676
  weapon,
583
677
  },
584
678
  time,
679
+ camera,
680
+ input,
585
681
  subscribe: signal.subscribe,
586
682
  version: signal.version,
587
683
  };