@jgengine/core 0.11.0 → 0.12.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 (219) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/dist/ai/populationDirector.d.ts +103 -0
  3. package/dist/ai/populationDirector.js +190 -0
  4. package/dist/ai/pursuit.d.ts +56 -0
  5. package/dist/ai/pursuit.js +53 -0
  6. package/dist/authoring.d.ts +1 -1
  7. package/dist/authoring.js +1 -1
  8. package/dist/combat.d.ts +1 -0
  9. package/dist/combat.js +1 -0
  10. package/dist/devtools/controls.d.ts +38 -0
  11. package/dist/devtools/controls.js +181 -0
  12. package/dist/devtools/devtools.d.ts +5 -197
  13. package/dist/devtools/devtools.js +38 -774
  14. package/dist/devtools/discover.d.ts +32 -0
  15. package/dist/devtools/discover.js +240 -0
  16. package/dist/devtools/fallbackSeams.d.ts +50 -0
  17. package/dist/devtools/fallbackSeams.js +111 -0
  18. package/dist/devtools/frame.d.ts +15 -0
  19. package/dist/devtools/frame.js +247 -0
  20. package/dist/devtools/logs.d.ts +15 -0
  21. package/dist/devtools/logs.js +86 -0
  22. package/dist/devtools/overrides.d.ts +16 -0
  23. package/dist/devtools/overrides.js +66 -0
  24. package/dist/devtools/types.d.ts +212 -0
  25. package/dist/devtools/types.js +1 -0
  26. package/dist/economy/auctionBook.d.ts +122 -0
  27. package/dist/economy/auctionBook.js +213 -0
  28. package/dist/economy/priceHistory.d.ts +44 -0
  29. package/dist/economy/priceHistory.js +89 -0
  30. package/dist/economy/unlockPoints.d.ts +92 -0
  31. package/dist/economy/unlockPoints.js +103 -0
  32. package/dist/editor/commandApply.d.ts +5 -0
  33. package/dist/editor/commandApply.js +755 -0
  34. package/dist/editor/commands.d.ts +48 -3
  35. package/dist/editor/commands.js +40 -754
  36. package/dist/editor/directives.d.ts +94 -0
  37. package/dist/editor/directives.js +206 -0
  38. package/dist/editor/document.d.ts +37 -2
  39. package/dist/editor/document.js +422 -6
  40. package/dist/editor/entityCatalog.d.ts +42 -0
  41. package/dist/editor/entityCatalog.js +101 -0
  42. package/dist/editor/environment.d.ts +74 -0
  43. package/dist/editor/environment.js +169 -0
  44. package/dist/editor/index.d.ts +26 -1
  45. package/dist/editor/index.js +8 -1
  46. package/dist/editor/minimap.d.ts +31 -0
  47. package/dist/editor/minimap.js +73 -0
  48. package/dist/editor/streamer.d.ts +66 -0
  49. package/dist/editor/streamer.js +68 -0
  50. package/dist/editor/types.d.ts +143 -0
  51. package/dist/editor/world.d.ts +136 -0
  52. package/dist/editor/world.js +303 -0
  53. package/dist/game/breeding.d.ts +161 -0
  54. package/dist/game/breeding.js +181 -0
  55. package/dist/game/cameraConfig.d.ts +451 -0
  56. package/dist/game/cameraConfig.js +1 -0
  57. package/dist/game/defineGame.d.ts +16 -9
  58. package/dist/game/defineGame.js +14 -6
  59. package/dist/game/defineSystem.d.ts +2 -0
  60. package/dist/game/defineSystem.js +2 -0
  61. package/dist/game/events.d.ts +28 -0
  62. package/dist/game/playableGame.d.ts +34 -435
  63. package/dist/game/playableGame.js +1 -1
  64. package/dist/game/progression.d.ts +35 -0
  65. package/dist/game/progression.js +19 -0
  66. package/dist/game/quest.js +1 -3
  67. package/dist/game/tribe.d.ts +184 -0
  68. package/dist/game/tribe.js +432 -0
  69. package/dist/gameplay.d.ts +3 -1
  70. package/dist/gameplay.js +3 -1
  71. package/dist/inventory/encumbrance.d.ts +69 -0
  72. package/dist/inventory/encumbrance.js +68 -0
  73. package/dist/item/durability.js +1 -3
  74. package/dist/meta/changelog.d.ts +1 -1
  75. package/dist/meta/changelog.js +24 -1
  76. package/dist/movement/movementModel.d.ts +15 -1
  77. package/dist/movement/movementModel.js +33 -15
  78. package/dist/movement/playerMovement.js +102 -2
  79. package/dist/multiplayer/combatSnapshot.js +13 -8
  80. package/dist/multiplayer/presenceModel.d.ts +2 -0
  81. package/dist/multiplayer/presenceModel.js +8 -0
  82. package/dist/physics/drivableVehicle.d.ts +2 -0
  83. package/dist/physics/drivableVehicle.js +1 -1
  84. package/dist/physics/flightDynamics.d.ts +82 -0
  85. package/dist/physics/flightDynamics.js +165 -0
  86. package/dist/physics/gravityField.d.ts +25 -0
  87. package/dist/physics/gravityField.js +41 -0
  88. package/dist/physics/kinematicVehicle.d.ts +87 -0
  89. package/dist/physics/kinematicVehicle.js +193 -11
  90. package/dist/physics/physicsWorld.d.ts +26 -0
  91. package/dist/physics/physicsWorld.js +109 -117
  92. package/dist/physics/vehicleObstacles.d.ts +44 -0
  93. package/dist/physics/vehicleObstacles.js +115 -0
  94. package/dist/random/rng.d.ts +4 -1
  95. package/dist/random/rng.js +4 -1
  96. package/dist/runtime/adapter.d.ts +13 -0
  97. package/dist/runtime/adapter.js +13 -0
  98. package/dist/runtime/context/combat.d.ts +51 -0
  99. package/dist/runtime/context/combat.js +130 -0
  100. package/dist/runtime/context/deathLoot.d.ts +30 -0
  101. package/dist/runtime/context/deathLoot.js +39 -0
  102. package/dist/runtime/context/player.d.ts +48 -0
  103. package/dist/runtime/context/player.js +125 -0
  104. package/dist/runtime/context/scene.d.ts +59 -0
  105. package/dist/runtime/context/scene.js +291 -0
  106. package/dist/runtime/context/snapshotCodecs.d.ts +12 -0
  107. package/dist/runtime/context/snapshotCodecs.js +26 -0
  108. package/dist/runtime/descriptors/baseline.js +26 -3
  109. package/dist/runtime/descriptors/features.d.ts +5 -0
  110. package/dist/runtime/descriptors/features.js +20 -2
  111. package/dist/runtime/gameContext.d.ts +2 -428
  112. package/dist/runtime/gameContext.js +88 -501
  113. package/dist/runtime/gameContextTypes.d.ts +467 -0
  114. package/dist/runtime/gameContextTypes.js +1 -0
  115. package/dist/runtime/headlessRunner.d.ts +4 -2
  116. package/dist/runtime/headlessRunner.js +1 -0
  117. package/dist/runtime/hostPolicy.d.ts +51 -0
  118. package/dist/runtime/hostPolicy.js +68 -0
  119. package/dist/runtime/hostedGameRunner.d.ts +3 -1
  120. package/dist/runtime/hostedGameRunner.js +1 -0
  121. package/dist/runtime/hostedWorldSession.d.ts +3 -1
  122. package/dist/runtime/hostedWorldSession.js +1 -0
  123. package/dist/runtime/worldSnapshot.d.ts +13 -1
  124. package/dist/runtime/worldSnapshot.js +15 -3
  125. package/dist/scene/assetCatalog.d.ts +5 -0
  126. package/dist/scene/assetSpace.js +1 -3
  127. package/dist/scene/authoredTriggers.d.ts +50 -0
  128. package/dist/scene/authoredTriggers.js +114 -0
  129. package/dist/scene/builtinSceneKinds.js +8 -3
  130. package/dist/scene/colliders.d.ts +58 -0
  131. package/dist/scene/colliders.js +108 -0
  132. package/dist/scene/collisionMesh.d.ts +94 -0
  133. package/dist/scene/collisionMesh.js +699 -0
  134. package/dist/scene/companion.d.ts +147 -0
  135. package/dist/scene/companion.js +193 -0
  136. package/dist/scene/entityStats.d.ts +7 -9
  137. package/dist/scene/entityStats.js +10 -14
  138. package/dist/scene/sceneKinds.d.ts +16 -0
  139. package/dist/scene/sceneKinds.js +4 -0
  140. package/dist/scene/sceneRaycast.js +15 -0
  141. package/dist/stats/spawnLevelStats.d.ts +110 -0
  142. package/dist/stats/spawnLevelStats.js +140 -0
  143. package/dist/stats/statPool.d.ts +70 -0
  144. package/dist/stats/statPool.js +59 -0
  145. package/dist/store/changeSignal.d.ts +3 -0
  146. package/dist/store/changeSignal.js +2 -0
  147. package/dist/store/observableKeyedStore.d.ts +2 -0
  148. package/dist/store/observableKeyedStore.js +1 -0
  149. package/dist/survival/decayMeter.js +1 -3
  150. package/dist/survival/regionHealth.js +1 -3
  151. package/dist/ui/hudLayout.d.ts +5 -0
  152. package/dist/ui/hudLayout.js +17 -1
  153. package/dist/world/authoredEntities.d.ts +29 -0
  154. package/dist/world/authoredEntities.js +29 -0
  155. package/dist/world/authoredObjects.d.ts +15 -1
  156. package/dist/world/authoredObjects.js +13 -1
  157. package/dist/world/authoredSpawn.d.ts +39 -0
  158. package/dist/world/authoredSpawn.js +37 -0
  159. package/dist/world/buildSockets.d.ts +152 -0
  160. package/dist/world/buildSockets.js +188 -0
  161. package/dist/world/cityBlocks.d.ts +152 -0
  162. package/dist/world/cityBlocks.js +670 -0
  163. package/dist/world/cityContent.d.ts +77 -0
  164. package/dist/world/cityContent.js +261 -0
  165. package/dist/world/cityGeometry.d.ts +114 -0
  166. package/dist/world/cityGeometry.js +573 -0
  167. package/dist/world/cityKind.d.ts +314 -0
  168. package/dist/world/cityKind.js +1578 -0
  169. package/dist/world/features.d.ts +18 -3
  170. package/dist/world/features.js +10 -1
  171. package/dist/world/grassKind.js +2 -2
  172. package/dist/world/markers.d.ts +50 -4
  173. package/dist/world/markers.js +49 -0
  174. package/dist/world/massing.js +1 -1
  175. package/dist/world/minimapBake.d.ts +78 -0
  176. package/dist/world/minimapBake.js +208 -0
  177. package/dist/world/minimapTrack.d.ts +17 -0
  178. package/dist/world/minimapTrack.js +22 -0
  179. package/dist/world/pathNetwork.d.ts +159 -0
  180. package/dist/world/pathNetwork.js +684 -0
  181. package/dist/world/place.d.ts +166 -0
  182. package/dist/world/place.js +96 -0
  183. package/dist/world/placeAsset.js +3 -1
  184. package/dist/world/poleLineKind.d.ts +40 -0
  185. package/dist/world/poleLineKind.js +97 -0
  186. package/dist/world/polyline.js +9 -2
  187. package/dist/world/resourceNode.d.ts +109 -0
  188. package/dist/world/resourceNode.js +140 -0
  189. package/dist/world/scatter.js +2 -20
  190. package/dist/world/streets.d.ts +29 -0
  191. package/dist/world/streets.js +55 -0
  192. package/dist/world/terraform.js +58 -88
  193. package/dist/world/terrain.d.ts +45 -0
  194. package/dist/world/terrain.js +78 -0
  195. package/dist/world.d.ts +14 -5
  196. package/dist/world.js +12 -3
  197. package/package.json +1 -1
  198. package/skills/game-design/SKILL.md +28 -0
  199. package/skills/game-design/agents/openai.yaml +4 -0
  200. package/skills/game-design/references/application-playbook.md +170 -0
  201. package/skills/game-design/references/field-guide.md +288 -0
  202. package/skills/jgengine/SKILL.md +7 -4
  203. package/skills/jgengine/api.md +172 -79
  204. package/skills/jgengine/capabilities.md +104 -2
  205. package/skills/jgengine/recipes/minimal-game.md +88 -0
  206. package/skills/jgengine-combat/SKILL.md +5 -1
  207. package/skills/jgengine-combat/api.md +36 -0
  208. package/skills/jgengine-combat/capabilities.md +44 -0
  209. package/skills/jgengine-combat/recipes/portable-stat-pools.md +122 -0
  210. package/skills/jgengine-combat/reference.md +12 -2
  211. package/skills/jgengine-gameplay/api.md +152 -26
  212. package/skills/jgengine-gameplay/capabilities.md +78 -2
  213. package/skills/jgengine-gameplay/reference-systems.md +1 -5
  214. package/skills/jgengine-world/SKILL.md +13 -0
  215. package/skills/jgengine-world/api.md +419 -15
  216. package/skills/jgengine-world/capabilities.md +108 -2
  217. package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
  218. package/skills/jgengine-world/reference.md +43 -4
  219. package/skills/jgengine/reference-cartridge.md +0 -72
@@ -0,0 +1,291 @@
1
+ import { createBodyBind } from "../../scene/bodyBind.js";
2
+ import { createEntityStatsApi, seedStatValues, } from "../../scene/entityStats.js";
3
+ import { createEntityStore, } from "../../scene/entityStore.js";
4
+ import { createForms } from "../../scene/form.js";
5
+ import { fittedEntityColliders, fittedObjectColliders, scaledEntityColliders, scaledObjectColliders, } from "../../scene/colliders.js";
6
+ import { raycastObjects, raycastObjectsAll } from "../../scene/objectQuery.js";
7
+ import { createObjectStore, objectVisualScale } from "../../scene/objectStore.js";
8
+ import { createPaintLayer } from "../../scene/paintLayer.js";
9
+ import { createSelectionSet } from "../../scene/selection.js";
10
+ import { createSceneRaycast, } from "../../scene/sceneRaycast.js";
11
+ import { createSpatialApi } from "../../scene/spatial.js";
12
+ import { createTargeting } from "../../scene/targeting.js";
13
+ import { notifyAfter } from "../../store/changeSignal.js";
14
+ /** @internal */
15
+ export function createSceneSubsystem(d) {
16
+ const { content, signalNotify, ground, events, time, occluder } = d;
17
+ const entities = createEntityStore();
18
+ const objects = createObjectStore();
19
+ entities.subscribe(signalNotify);
20
+ objects.subscribe(signalNotify);
21
+ const statsByInstance = new Map();
22
+ const entityStats = notifyAfter(createEntityStatsApi((instanceId) => statsByInstance.get(instanceId)), ["set", "delta"], signalNotify);
23
+ function ensureInstanceStats(instanceId) {
24
+ let map = statsByInstance.get(instanceId);
25
+ if (map === undefined) {
26
+ map = {};
27
+ statsByInstance.set(instanceId, map);
28
+ }
29
+ return map;
30
+ }
31
+ function catalogEntry(instanceId) {
32
+ const entity = entities.get(instanceId);
33
+ return entity === null ? undefined : content.entityById?.(entity.name);
34
+ }
35
+ function catalogObject(instanceId) {
36
+ const object = objects.get(instanceId);
37
+ return object === null ? undefined : content.objectById?.(object.catalogId);
38
+ }
39
+ let spatialGeneration = 0;
40
+ const candidateIds = [];
41
+ let candidateIdsDirty = true;
42
+ function refreshCandidateIds() {
43
+ if (!candidateIdsDirty)
44
+ return candidateIds;
45
+ candidateIds.length = 0;
46
+ for (const entity of entities.list())
47
+ candidateIds.push(entity.id);
48
+ candidateIdsDirty = false;
49
+ return candidateIds;
50
+ }
51
+ const spatial = createSpatialApi({
52
+ resolvePosition: (instanceId) => entities.get(instanceId)?.position,
53
+ candidates: refreshCandidateIds,
54
+ grid: { cellSize: 8 },
55
+ getVersion: () => spatialGeneration,
56
+ ...(occluder !== undefined ? { occluder } : {}),
57
+ });
58
+ entities.subscribe(() => {
59
+ candidateIdsDirty = true;
60
+ spatialGeneration += 1;
61
+ spatial.invalidate();
62
+ });
63
+ const entityColliders = new Map();
64
+ const objectColliders = new Map();
65
+ const fittedEntityByKind = new Map();
66
+ const fittedObjectByCatalogId = new Map();
67
+ function fittedEntitySetFor(kind) {
68
+ const cached = fittedEntityByKind.get(kind);
69
+ if (cached !== undefined)
70
+ return cached;
71
+ const model = d.models?.entity?.(kind);
72
+ const fitted = model === null || model === undefined ? null : fittedEntityColliders(model);
73
+ fittedEntityByKind.set(kind, fitted);
74
+ return fitted;
75
+ }
76
+ function fittedObjectSetFor(catalogId) {
77
+ const cached = fittedObjectByCatalogId.get(catalogId);
78
+ if (cached !== undefined)
79
+ return cached;
80
+ const model = d.models?.object?.(catalogId);
81
+ const fitted = model === null || model === undefined ? null : fittedObjectColliders(model);
82
+ fittedObjectByCatalogId.set(catalogId, fitted);
83
+ return fitted;
84
+ }
85
+ function entityCollidersOf(instanceId) {
86
+ const override = entityColliders.get(instanceId);
87
+ if (override !== undefined)
88
+ return override;
89
+ const entry = catalogEntry(instanceId);
90
+ if (entry?.colliders !== undefined)
91
+ return entry.colliders;
92
+ const kind = entities.get(instanceId)?.name;
93
+ if (kind !== undefined) {
94
+ const fitted = fittedEntitySetFor(kind);
95
+ if (fitted !== null)
96
+ return fitted;
97
+ }
98
+ const scale = entry?.scale;
99
+ if (scale !== undefined && scale !== 1)
100
+ return scaledEntityColliders(scale);
101
+ return null;
102
+ }
103
+ function entityVisualScaleOf(instanceId) {
104
+ return catalogEntry(instanceId)?.scale ?? 1;
105
+ }
106
+ function objectCollidersOf(instanceId) {
107
+ const override = objectColliders.get(instanceId);
108
+ if (override !== undefined)
109
+ return override;
110
+ const catalog = catalogObject(instanceId);
111
+ if (catalog?.colliders !== undefined)
112
+ return catalog.colliders;
113
+ if (catalog?.halfExtents !== undefined)
114
+ return null;
115
+ const object = objects.get(instanceId);
116
+ if (object === null)
117
+ return null;
118
+ const fitted = fittedObjectSetFor(object.catalogId);
119
+ if (fitted !== null)
120
+ return fitted;
121
+ if (object.visual?.scale === undefined)
122
+ return null;
123
+ return scaledObjectColliders(objectVisualScale(object.visual));
124
+ }
125
+ const targeting = notifyAfter(createTargeting({
126
+ candidates: () => [...refreshCandidateIds()],
127
+ classify(_fromId, toId) {
128
+ const role = catalogEntry(toId)?.role;
129
+ return role === "enemy" || role === "hostile" ? "hostile" : "friendly";
130
+ },
131
+ distance: (fromId, toId) => spatial.distance(fromId, toId),
132
+ }), ["setTarget", "cycleTarget"], signalNotify);
133
+ const combatSpatial = {
134
+ inRadius: (center, radius) => spatial.inRadius(center, radius),
135
+ hasLineOfSight: (from, to) => {
136
+ if (typeof from === "string")
137
+ return spatial.hasLineOfSight(from, to);
138
+ const toPos = entities.get(to)?.position;
139
+ if (toPos === undefined)
140
+ return false;
141
+ if (occluder === undefined)
142
+ return true;
143
+ return !occluder(from, toPos);
144
+ },
145
+ positionOf: (instanceId) => entities.get(instanceId)?.position,
146
+ };
147
+ const sceneRaycast = createSceneRaycast({
148
+ entities: {
149
+ list: () => entities.list().map((entity) => ({
150
+ id: entity.id,
151
+ position: entity.position,
152
+ rotationY: entity.rotationY,
153
+ name: entity.name,
154
+ })),
155
+ collidersOf: entityCollidersOf,
156
+ inRadius: (center, radius) => spatial.inRadius(center, radius),
157
+ get: (id) => {
158
+ const entity = entities.get(id);
159
+ if (entity === null)
160
+ return null;
161
+ return { id: entity.id, position: entity.position, rotationY: entity.rotationY, name: entity.name };
162
+ },
163
+ },
164
+ objects: {
165
+ list: () => objects.list(),
166
+ inBox: (min, max) => objects.inBox(min, max),
167
+ collidersOf: objectCollidersOf,
168
+ halfExtentsOf: (catalogId) => content.objectById?.(catalogId)?.halfExtents ?? null,
169
+ },
170
+ terrain: ground,
171
+ });
172
+ const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signalNotify);
173
+ const paintLayer = notifyAfter(createPaintLayer(), ["paint", "clear"], signalNotify);
174
+ let onAfterSpawn = () => { };
175
+ let onAfterDespawn = () => { };
176
+ function spawnEntity(name, spawnOptions) {
177
+ const entry = content.entityById?.(name);
178
+ const walkSpeed = spawnOptions?.movement?.walkSpeed ?? entry?.movement?.walkSpeed;
179
+ const options = walkSpeed === undefined
180
+ ? spawnOptions
181
+ : { ...spawnOptions, movement: { ...spawnOptions?.movement, walkSpeed } };
182
+ const instanceId = entities.spawn(name, options);
183
+ onAfterSpawn(instanceId);
184
+ statsByInstance.set(instanceId, entry?.stats === undefined ? {} : seedStatValues(entry.stats));
185
+ return instanceId;
186
+ }
187
+ function despawnEntity(instanceId) {
188
+ const existed = entities.despawn(instanceId);
189
+ statsByInstance.delete(instanceId);
190
+ targeting.clearAll(instanceId);
191
+ entityColliders.delete(instanceId);
192
+ onAfterDespawn(instanceId);
193
+ return existed;
194
+ }
195
+ function moveEntityTowardCommit(instanceId, target, options) {
196
+ const next = spatial.moveToward(instanceId, target, options);
197
+ if (next === null)
198
+ return null;
199
+ const current = entities.get(instanceId);
200
+ if (current === null)
201
+ return null;
202
+ let rotationY = current.rotationY;
203
+ if (options.face === true) {
204
+ const dx = next[0] - current.position[0];
205
+ const dz = next[2] - current.position[2];
206
+ if (Math.hypot(dx, dz) > 1e-9)
207
+ rotationY = Math.atan2(dx, dz);
208
+ }
209
+ entities.setPose(instanceId, { position: next, rotationY });
210
+ return next;
211
+ }
212
+ function resetAllToSpawn(filter) {
213
+ let count = 0;
214
+ for (const entity of entities.list()) {
215
+ if (filter !== undefined && !filter(entity))
216
+ continue;
217
+ if (entities.resetToSpawn(entity.id))
218
+ count += 1;
219
+ }
220
+ return count;
221
+ }
222
+ const bodyBinds = new Map();
223
+ function bind(key) {
224
+ const existing = bodyBinds.get(key);
225
+ if (existing !== undefined)
226
+ return existing;
227
+ const created = createBodyBind({
228
+ has: (id) => entities.get(id) !== null,
229
+ spawn: spawnEntity,
230
+ despawn: despawnEntity,
231
+ setPose: entities.setPose,
232
+ update: entities.update,
233
+ });
234
+ bodyBinds.set(key, created);
235
+ return created;
236
+ }
237
+ const objectSelection = notifyAfter(createSelectionSet(), ["add", "remove", "toggle", "replace", "clear"], signalNotify);
238
+ const sceneObjects = {
239
+ ...objects,
240
+ remove(instanceId) {
241
+ const existed = objects.remove(instanceId);
242
+ if (existed)
243
+ objectSelection.remove(instanceId);
244
+ return existed;
245
+ },
246
+ catalog: (instanceId) => catalogObject(instanceId) ?? null,
247
+ raycast: (input) => raycastObjects(objects, input),
248
+ raycastAll: (input) => raycastObjectsAll(objects, input),
249
+ setColliders(instanceId, colliders) {
250
+ if (colliders === null)
251
+ objectColliders.delete(instanceId);
252
+ else
253
+ objectColliders.set(instanceId, colliders);
254
+ signalNotify();
255
+ },
256
+ collidersOf: objectCollidersOf,
257
+ selection: objectSelection,
258
+ };
259
+ return {
260
+ entities,
261
+ objects,
262
+ statsByInstance,
263
+ entityStats,
264
+ ensureInstanceStats,
265
+ catalogEntry,
266
+ catalogObject,
267
+ spatial,
268
+ combatSpatial,
269
+ sceneRaycast,
270
+ targeting,
271
+ entityColliders,
272
+ objectColliders,
273
+ entityCollidersOf,
274
+ objectCollidersOf,
275
+ entityVisualScaleOf,
276
+ forms,
277
+ paintLayer,
278
+ spawnEntity,
279
+ despawnEntity,
280
+ moveEntityTowardCommit,
281
+ resetAllToSpawn,
282
+ bind,
283
+ sceneObjects,
284
+ setOnAfterSpawn(fn) {
285
+ onAfterSpawn = fn;
286
+ },
287
+ setOnAfterDespawn(fn) {
288
+ onAfterDespawn = fn;
289
+ },
290
+ };
291
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Shallow wire codecs for {@link SnapshotModule.decode} — reject obviously wrong shapes so hydrate
3
+ * never receives bare casts of garbage. Fail soft (return `null`); deeper field validation stays with
4
+ * each subsystem's hydrate.
5
+ * @internal
6
+ */
7
+ /** @internal Reject non-arrays; return a typed array or null. */
8
+ export declare function decodeArray<T = unknown>(raw: unknown): T[] | null;
9
+ /** @internal Reject non-plain objects; return a typed record or null. */
10
+ export declare function decodeRecord<T = unknown>(raw: unknown): Record<string, T> | null;
11
+ /** @internal Reject non-[string, T][] entry lists; return entries or null. */
12
+ export declare function decodeEntries<T = unknown>(raw: unknown): readonly (readonly [string, T])[] | null;
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Shallow wire codecs for {@link SnapshotModule.decode} — reject obviously wrong shapes so hydrate
3
+ * never receives bare casts of garbage. Fail soft (return `null`); deeper field validation stays with
4
+ * each subsystem's hydrate.
5
+ * @internal
6
+ */
7
+ /** @internal Reject non-arrays; return a typed array or null. */
8
+ export function decodeArray(raw) {
9
+ return Array.isArray(raw) ? raw : null;
10
+ }
11
+ /** @internal Reject non-plain objects; return a typed record or null. */
12
+ export function decodeRecord(raw) {
13
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
14
+ return null;
15
+ return raw;
16
+ }
17
+ /** @internal Reject non-[string, T][] entry lists; return entries or null. */
18
+ export function decodeEntries(raw) {
19
+ if (!Array.isArray(raw))
20
+ return null;
21
+ for (const entry of raw) {
22
+ if (!Array.isArray(entry) || entry.length < 2 || typeof entry[0] !== "string")
23
+ return null;
24
+ }
25
+ return raw;
26
+ }
@@ -1,4 +1,5 @@
1
1
  import { hydrateEntityStats, snapshotEntityStats, } from "../../scene/entityStats.js";
2
+ import { decodeArray, decodeEntries, decodeRecord } from "../context/snapshotCodecs.js";
2
3
  /** @internal */
3
4
  export const baselineDescriptors = [
4
5
  {
@@ -7,6 +8,7 @@ export const baselineDescriptors = [
7
8
  replicate: {
8
9
  key: "entities",
9
10
  snapshot: () => d.entities.snapshot(),
11
+ decode: (raw) => decodeArray(raw),
10
12
  hydrate: (data) => d.entities.hydrate(data),
11
13
  },
12
14
  }),
@@ -17,6 +19,7 @@ export const baselineDescriptors = [
17
19
  replicate: {
18
20
  key: "stats",
19
21
  snapshot: () => snapshotEntityStats(d.statsByInstance),
22
+ decode: (raw) => decodeRecord(raw),
20
23
  hydrate: (data) => hydrateEntityStats(d.statsByInstance, data),
21
24
  },
22
25
  }),
@@ -27,6 +30,7 @@ export const baselineDescriptors = [
27
30
  replicate: {
28
31
  key: "store",
29
32
  snapshot: () => d.store.snapshot(),
33
+ decode: (raw) => decodeEntries(raw),
30
34
  hydrate: (data) => d.store.hydrate(data),
31
35
  },
32
36
  }),
@@ -37,6 +41,7 @@ export const baselineDescriptors = [
37
41
  replicate: {
38
42
  key: "feed",
39
43
  snapshot: () => d.feed.snapshot(),
44
+ decode: (raw) => decodeRecord(raw),
40
45
  hydrate: (data) => d.feed.hydrate(data),
41
46
  },
42
47
  }),
@@ -56,8 +61,10 @@ export const baselineDescriptors = [
56
61
  }
57
62
  return byUser;
58
63
  },
64
+ decode: (raw) => decodeRecord(raw),
59
65
  hydrate: (data) => {
60
- for (const [userId, states] of Object.entries(data)) {
66
+ const byUser = data;
67
+ for (const [userId, states] of Object.entries(byUser)) {
61
68
  const set = d.inventoryFor(userId);
62
69
  for (const [inventoryId, state] of Object.entries(states))
63
70
  set.replaceState(inventoryId, state);
@@ -72,9 +79,11 @@ export const baselineDescriptors = [
72
79
  save: {
73
80
  key: "economy",
74
81
  snapshot: () => Object.fromEntries(d.wallets),
82
+ decode: (raw) => decodeRecord(raw),
75
83
  hydrate: (data) => {
84
+ const wallets = data;
76
85
  d.wallets.clear();
77
- for (const [userId, state] of Object.entries(data)) {
86
+ for (const [userId, state] of Object.entries(wallets)) {
78
87
  d.wallets.set(userId, state);
79
88
  }
80
89
  d.signalNotify();
@@ -88,6 +97,10 @@ export const baselineDescriptors = [
88
97
  save: {
89
98
  key: "time",
90
99
  snapshot: () => d.time.snapshot(),
100
+ decode: (raw) => {
101
+ const rec = decodeRecord(raw);
102
+ return rec === null ? null : rec;
103
+ },
91
104
  hydrate: (data) => d.time.hydrate(data),
92
105
  },
93
106
  }),
@@ -98,6 +111,10 @@ export const baselineDescriptors = [
98
111
  save: {
99
112
  key: "pose",
100
113
  snapshot: () => d.pose.snapshotAll(),
114
+ decode: (raw) => {
115
+ const rec = decodeRecord(raw);
116
+ return rec === null ? null : rec;
117
+ },
101
118
  hydrate: (data) => d.pose.hydrateAll(data),
102
119
  },
103
120
  }),
@@ -108,6 +125,10 @@ export const baselineDescriptors = [
108
125
  save: {
109
126
  key: "possession",
110
127
  snapshot: () => d.possession.snapshotAll(),
128
+ decode: (raw) => {
129
+ const rec = decodeRecord(raw);
130
+ return rec === null ? null : rec;
131
+ },
111
132
  hydrate: (data) => d.possession.hydrateAll(data),
112
133
  },
113
134
  }),
@@ -123,8 +144,10 @@ export const baselineDescriptors = [
123
144
  out[userId] = queue.snapshot();
124
145
  return out;
125
146
  },
147
+ decode: (raw) => decodeRecord(raw),
126
148
  hydrate: (data) => {
127
- for (const [userId, batch] of Object.entries(data)) {
149
+ const batches = data;
150
+ for (const [userId, batch] of Object.entries(batches)) {
128
151
  d.motionFor(userId).hydrate(batch);
129
152
  }
130
153
  },
@@ -63,6 +63,11 @@ export interface FeatureBuild {
63
63
  * {@link FeatureDeps}, and whether it replicates or persists. `createGameContext` iterates the
64
64
  * descriptor list instead of hand-wiring each `features.x ? create : undefined` branch — the seam a
65
65
  * new feature extends through.
66
+ *
67
+ * Feature activation has **one path**: `definition.features` after `defineGame` has already
68
+ * OR-merged explicit `features: { quest: true }` sugar with system-implied flags via
69
+ * `mergeSystemFeatures`. Descriptors only call `enabled(features)` against that final map — never
70
+ * re-check systems or a second flag source.
66
71
  * @internal
67
72
  */
68
73
  export interface FeatureDescriptor {
@@ -10,6 +10,7 @@ import { createUnlocks } from "../../game/unlocks.js";
10
10
  import { createRoster } from "../../scene/roster.js";
11
11
  import { createConnectedPlayers } from "../../game/connectedPlayers.js";
12
12
  import { notifyAfter } from "../../store/changeSignal.js";
13
+ import { decodeArray, decodeRecord } from "../context/snapshotCodecs.js";
13
14
  /** @internal */
14
15
  export const featureDescriptors = [
15
16
  {
@@ -22,6 +23,7 @@ export const featureDescriptors = [
22
23
  save: {
23
24
  key: "unlocks",
24
25
  snapshot: () => unlocks.snapshotAll(),
26
+ decode: (raw) => decodeRecord(raw),
25
27
  hydrate: (data) => unlocks.hydrateAll(data),
26
28
  },
27
29
  };
@@ -49,6 +51,10 @@ export const featureDescriptors = [
49
51
  replicate: {
50
52
  key: "social",
51
53
  snapshot: () => social.snapshot(),
54
+ decode: (rawVal) => {
55
+ const rec = decodeRecord(rawVal);
56
+ return rec === null ? null : rec;
57
+ },
52
58
  hydrate: (data) => social.hydrate(data),
53
59
  },
54
60
  };
@@ -74,6 +80,10 @@ export const featureDescriptors = [
74
80
  replicate: {
75
81
  key: "chat",
76
82
  snapshot: () => chat.snapshot(),
83
+ decode: (rawVal) => {
84
+ const rec = decodeRecord(rawVal);
85
+ return rec === null ? null : rec;
86
+ },
77
87
  hydrate: (data) => chat.hydrate(data),
78
88
  },
79
89
  };
@@ -89,6 +99,7 @@ export const featureDescriptors = [
89
99
  replicate: {
90
100
  key: "leaderboard",
91
101
  snapshot: () => leaderboard.snapshot(),
102
+ decode: (raw) => decodeArray(raw),
92
103
  hydrate: (data) => leaderboard.hydrate(data),
93
104
  },
94
105
  };
@@ -104,6 +115,7 @@ export const featureDescriptors = [
104
115
  save: {
105
116
  key: "roster",
106
117
  snapshot: () => roster.snapshotAll(),
118
+ decode: (raw) => decodeRecord(raw),
107
119
  hydrate: (data) => roster.hydrateAll(data),
108
120
  },
109
121
  };
@@ -119,6 +131,7 @@ export const featureDescriptors = [
119
131
  save: {
120
132
  key: "cosmetics",
121
133
  snapshot: () => cosmetics.snapshotAll(),
134
+ decode: (raw) => decodeRecord(raw),
122
135
  hydrate: (data) => cosmetics.hydrateAll(data),
123
136
  },
124
137
  };
@@ -193,6 +206,7 @@ export const featureDescriptors = [
193
206
  save: {
194
207
  key: "quest",
195
208
  snapshot: () => quest.snapshotAll(),
209
+ decode: (raw) => decodeRecord(raw),
196
210
  hydrate: (data) => quest.hydrateAll(data),
197
211
  },
198
212
  };
@@ -239,8 +253,10 @@ export const featureDescriptors = [
239
253
  out[id] = cardPile.state();
240
254
  return out;
241
255
  },
256
+ decode: (raw) => decodeRecord(raw),
242
257
  hydrate: (data) => {
243
- for (const [id, state] of Object.entries(data)) {
258
+ const piles = data;
259
+ for (const [id, state] of Object.entries(piles)) {
244
260
  d.cardPiles.get(id)?.reset(state);
245
261
  }
246
262
  },
@@ -262,8 +278,10 @@ export const featureDescriptors = [
262
278
  out[id] = turnLoop.capture();
263
279
  return out;
264
280
  },
281
+ decode: (raw) => decodeRecord(raw),
265
282
  hydrate: (data) => {
266
- for (const [id, state] of Object.entries(data)) {
283
+ const loops = data;
284
+ for (const [id, state] of Object.entries(loops)) {
267
285
  d.turnLoops.get(id)?.restore(state);
268
286
  }
269
287
  },