@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,303 @@
1
+ import { applyEditorDocumentOverlay, createEmptyEditorDocument, editorDocumentExtras, extractEditorFragment, mergeEditorDocuments, } from "./document.js";
2
+ /**
3
+ * A sharded **world**: a scene that is no longer one monolithic `editor.scene.json` but a manifest
4
+ * plus spatially-partitioned shard documents. A single-file game is the degenerate one-shard case,
5
+ * so nothing pays for scale it does not have. The shard/overlay documents are the exact
6
+ * {@link EditorDocument} format loaders already consume — this module is composition over
7
+ * `mergeEditorDocuments` / `extractEditorFragment` / `editorDocumentBounds`, not a new format.
8
+ */
9
+ /** The manifest `kind` discriminator — a world folder's `world.json`. */
10
+ export const WORLD_MANIFEST_KIND = "world";
11
+ /** True when a shard is resident regardless of any spatial query (unbounded or explicitly `"always"`). */
12
+ function isAlwaysResident(shard) {
13
+ return shard.residency === "always" || shard.bounds === undefined;
14
+ }
15
+ /** Closest-point disc↔AABB overlap test on the XZ plane. */
16
+ function discIntersectsBounds(cx, cz, radius, bounds) {
17
+ const qx = Math.max(bounds.min[0], Math.min(cx, bounds.max[0]));
18
+ const qz = Math.max(bounds.min[1], Math.min(cz, bounds.max[1]));
19
+ const dx = cx - qx;
20
+ const dz = cz - qz;
21
+ return dx * dx + dz * dz <= radius * radius;
22
+ }
23
+ /**
24
+ * True when a shard should be resident for a query: always-resident shards always, bounded shards
25
+ * only when the query disc (`center` ± `radius`) overlaps their footprint.
26
+ * @capability world-shards select shards for a neighborhood by spatial query
27
+ */
28
+ export function shardMatchesQuery(shard, query) {
29
+ if (isAlwaysResident(shard))
30
+ return true;
31
+ return discIntersectsBounds(query.center.x, query.center.z, query.radius ?? 0, shard.bounds);
32
+ }
33
+ /**
34
+ * The shards a query needs — always-resident shards plus every bounded shard whose footprint the
35
+ * query disc overlaps, in manifest order. With no query every shard is selected (load the planet),
36
+ * so a one-shard game and an unqueried load behave exactly as a monolithic document does today.
37
+ * @capability world-shards select shards for a neighborhood by spatial query
38
+ */
39
+ export function selectWorldShards(manifest, query) {
40
+ if (query === undefined)
41
+ return [...manifest.shards];
42
+ return manifest.shards.filter((shard) => shardMatchesQuery(shard, query));
43
+ }
44
+ /**
45
+ * Loads the live neighborhood document for a query: reads each selected shard through `resolve`,
46
+ * applies its overlay (if any) on top, then merges them into one {@link EditorDocument} downstream
47
+ * consumers (`placeAuthoredObjects`, `collectAuthoredTriggers`, scatter, …) read unchanged. The
48
+ * monolith becomes an internal detail of the loader; a one-shard world yields exactly that shard.
49
+ * @capability world-shards merge the shards for a neighborhood into one live document
50
+ */
51
+ export function loadWorldDocument(manifest, resolve, query) {
52
+ const shards = selectWorldShards(manifest, query);
53
+ const docs = [];
54
+ for (const shard of shards) {
55
+ const base = resolve(shard.file);
56
+ if (base === null)
57
+ continue;
58
+ if (shard.overlay === undefined) {
59
+ docs.push(base);
60
+ continue;
61
+ }
62
+ const overlay = resolve(shard.overlay);
63
+ docs.push(overlay === null ? base : applyEditorDocumentOverlay(base, overlay));
64
+ }
65
+ if (docs.length === 0)
66
+ return createEmptyEditorDocument();
67
+ return mergeEditorDocuments(...docs);
68
+ }
69
+ /** The always-resident base shard id used by {@link splitEditorDocumentIntoShards}. */
70
+ export const WORLD_BASE_SHARD_ID = "base";
71
+ /** Representative XZ point for an object, used to assign it to a grid cell. */
72
+ function objectCell(x, z, cellSize) {
73
+ return { gx: Math.floor(x / cellSize), gz: Math.floor(z / cellSize) };
74
+ }
75
+ function pathCentroid(points) {
76
+ if (points.length === 0)
77
+ return { x: 0, z: 0 };
78
+ let x = 0;
79
+ let z = 0;
80
+ for (const point of points) {
81
+ x += point.x;
82
+ z += point.z;
83
+ }
84
+ return { x: x / points.length, z: z / points.length };
85
+ }
86
+ /**
87
+ * Splits a monolithic document into a sharded world: placeable objects (markers, volumes, paths,
88
+ * notes) are bucketed into `cellSize`-meter grid cells by a representative point and extracted into
89
+ * per-cell shard documents; the non-placeable extras (terrain, catalogs, prefabs, collections,
90
+ * grids, HUD layout) go into one always-resident `base` shard. Round-trips through
91
+ * {@link loadWorldDocument} to the same object set — the reverse of monolith authoring, so a large
92
+ * scene can be sharded once and streamed thereafter. Cell shards carry their exact object footprint
93
+ * as bounds; empty cells are omitted; order is deterministic (row-major by grid coordinate).
94
+ * @capability world-shards split a monolithic scene document into spatial shards
95
+ */
96
+ export function splitEditorDocumentIntoShards(doc, options = {}) {
97
+ const cellSize = options.cellSize !== undefined && options.cellSize > 0 ? options.cellSize : 256;
98
+ const shardDir = options.shardDir ?? "shards";
99
+ // Bucket every placeable object id by the grid cell containing its representative point.
100
+ const cellIds = new Map();
101
+ const addToCell = (gx, gz, id) => {
102
+ const key = `${gx}:${gz}`;
103
+ const bucket = cellIds.get(key);
104
+ if (bucket === undefined)
105
+ cellIds.set(key, [id]);
106
+ else
107
+ bucket.push(id);
108
+ };
109
+ for (const marker of doc.markers) {
110
+ const { gx, gz } = objectCell(marker.position.x, marker.position.z, cellSize);
111
+ addToCell(gx, gz, marker.id);
112
+ }
113
+ for (const volume of doc.volumes) {
114
+ const { gx, gz } = objectCell(volume.center.x, volume.center.z, cellSize);
115
+ addToCell(gx, gz, volume.id);
116
+ }
117
+ for (const path of doc.paths) {
118
+ const c = pathCentroid(path.points);
119
+ const { gx, gz } = objectCell(c.x, c.z, cellSize);
120
+ addToCell(gx, gz, path.id);
121
+ }
122
+ for (const note of doc.annotations) {
123
+ const { gx, gz } = objectCell(note.position.x, note.position.z, cellSize);
124
+ addToCell(gx, gz, note.id);
125
+ }
126
+ // Always-resident base shard carries the non-placeable extras verbatim.
127
+ const baseFile = `${shardDir}/${WORLD_BASE_SHARD_ID}.json`;
128
+ const baseDoc = {
129
+ ...createEmptyEditorDocument(),
130
+ ...editorDocumentExtras(doc),
131
+ };
132
+ const shards = { [baseFile]: baseDoc };
133
+ const manifestShards = [
134
+ { id: WORLD_BASE_SHARD_ID, file: baseFile, residency: "always" },
135
+ ];
136
+ const sortedKeys = [...cellIds.keys()].sort((a, b) => {
137
+ const [ax, az] = a.split(":").map(Number);
138
+ const [bx, bz] = b.split(":").map(Number);
139
+ return az === bz ? ax - bx : az - bz;
140
+ });
141
+ for (const key of sortedKeys) {
142
+ const [gx, gz] = key.split(":").map(Number);
143
+ const ids = cellIds.get(key);
144
+ const shardDoc = extractEditorFragment(doc, ids);
145
+ const id = `cell_${gx}_${gz}`;
146
+ const file = `${shardDir}/${id}.json`;
147
+ shards[file] = shardDoc;
148
+ manifestShards.push({
149
+ id,
150
+ file,
151
+ residency: "streamed",
152
+ bounds: {
153
+ min: [gx * cellSize, gz * cellSize],
154
+ max: [(gx + 1) * cellSize, (gz + 1) * cellSize],
155
+ },
156
+ });
157
+ }
158
+ return {
159
+ manifest: {
160
+ kind: WORLD_MANIFEST_KIND,
161
+ version: 1,
162
+ grid: { cellSize },
163
+ shards: manifestShards,
164
+ },
165
+ shards,
166
+ };
167
+ }
168
+ /**
169
+ * The backward-compatible degenerate manifest: one always-resident shard pointing at a single scene
170
+ * document file. `loadWorldDocument(singleShardWorldManifest(file), resolve)` returns exactly that
171
+ * document — the migration path for a game that has one `editor.scene.json` and wants the world API
172
+ * without sharding yet.
173
+ * @capability world-shards wrap a single scene document as a one-shard world
174
+ */
175
+ export function singleShardWorldManifest(file, shardId = WORLD_BASE_SHARD_ID) {
176
+ return {
177
+ kind: WORLD_MANIFEST_KIND,
178
+ version: 1,
179
+ shards: [{ id: shardId, file, residency: "always" }],
180
+ };
181
+ }
182
+ function isPlainObject(value) {
183
+ return typeof value === "object" && value !== null && !Array.isArray(value);
184
+ }
185
+ function decodeBounds(value, path, errors) {
186
+ if (value === undefined)
187
+ return undefined;
188
+ if (!isPlainObject(value)) {
189
+ errors.push({ path, message: "expected an object" });
190
+ return undefined;
191
+ }
192
+ const pair = (v, p) => {
193
+ if (!Array.isArray(v) || v.length !== 2 || typeof v[0] !== "number" || typeof v[1] !== "number") {
194
+ errors.push({ path: p, message: "expected [number, number]" });
195
+ return null;
196
+ }
197
+ return [v[0], v[1]];
198
+ };
199
+ const min = pair(value.min, `${path}.min`);
200
+ const max = pair(value.max, `${path}.max`);
201
+ if (min === null || max === null)
202
+ return undefined;
203
+ return { min, max };
204
+ }
205
+ function decodeShard(value, path, errors) {
206
+ if (!isPlainObject(value)) {
207
+ errors.push({ path, message: "expected an object" });
208
+ return null;
209
+ }
210
+ if (typeof value.id !== "string")
211
+ errors.push({ path: `${path}.id`, message: "expected a string" });
212
+ if (typeof value.file !== "string")
213
+ errors.push({ path: `${path}.file`, message: "expected a string" });
214
+ const bounds = decodeBounds(value.bounds, `${path}.bounds`, errors);
215
+ let residency;
216
+ if (value.residency !== undefined) {
217
+ if (value.residency === "always" || value.residency === "streamed")
218
+ residency = value.residency;
219
+ else
220
+ errors.push({ path: `${path}.residency`, message: "expected \"always\" | \"streamed\"" });
221
+ }
222
+ if (value.overlay !== undefined && typeof value.overlay !== "string") {
223
+ errors.push({ path: `${path}.overlay`, message: "expected a string" });
224
+ }
225
+ if (typeof value.id !== "string" || typeof value.file !== "string")
226
+ return null;
227
+ const shard = { id: value.id, file: value.file };
228
+ if (bounds !== undefined)
229
+ shard.bounds = bounds;
230
+ if (residency !== undefined)
231
+ shard.residency = residency;
232
+ if (typeof value.overlay === "string")
233
+ shard.overlay = value.overlay;
234
+ return shard;
235
+ }
236
+ /**
237
+ * The authoritative decoder for a `world.json` manifest arriving from disk or an agent: validates
238
+ * `kind`, the shard list, and each shard's fields with a path-specific diagnostic on failure, so a
239
+ * malformed manifest fails loudly at the boundary instead of streaming a broken world.
240
+ * @capability world-shards decode an untrusted world manifest with per-field diagnostics
241
+ */
242
+ export function decodeWorldManifest(raw) {
243
+ if (!isPlainObject(raw)) {
244
+ return { ok: false, errors: [{ path: "$", message: "world manifest must be an object" }] };
245
+ }
246
+ const errors = [];
247
+ if (raw.kind !== WORLD_MANIFEST_KIND) {
248
+ errors.push({ path: "$.kind", message: `expected "${WORLD_MANIFEST_KIND}"` });
249
+ }
250
+ if (raw.version !== undefined && typeof raw.version !== "number") {
251
+ errors.push({ path: "$.version", message: "expected a number" });
252
+ }
253
+ let grid;
254
+ if (raw.grid !== undefined) {
255
+ if (isPlainObject(raw.grid) && typeof raw.grid.cellSize === "number")
256
+ grid = { cellSize: raw.grid.cellSize };
257
+ else
258
+ errors.push({ path: "$.grid", message: "expected { cellSize: number }" });
259
+ }
260
+ let streaming;
261
+ if (raw.streaming !== undefined) {
262
+ if (isPlainObject(raw.streaming) &&
263
+ typeof raw.streaming.loadRadius === "number" &&
264
+ typeof raw.streaming.keepRadius === "number") {
265
+ streaming = { loadRadius: raw.streaming.loadRadius, keepRadius: raw.streaming.keepRadius };
266
+ }
267
+ else {
268
+ errors.push({ path: "$.streaming", message: "expected { loadRadius: number, keepRadius: number }" });
269
+ }
270
+ }
271
+ const shards = [];
272
+ if (!Array.isArray(raw.shards)) {
273
+ errors.push({ path: "$.shards", message: "expected an array" });
274
+ }
275
+ else {
276
+ raw.shards.forEach((item, index) => {
277
+ const shard = decodeShard(item, `$.shards[${index}]`, errors);
278
+ if (shard !== null)
279
+ shards.push(shard);
280
+ });
281
+ const seen = new Set();
282
+ shards.forEach((shard, index) => {
283
+ if (seen.has(shard.id))
284
+ errors.push({ path: `$.shards[${index}].id`, message: `duplicate shard id "${shard.id}"` });
285
+ else
286
+ seen.add(shard.id);
287
+ });
288
+ }
289
+ if (errors.length > 0)
290
+ return { ok: false, errors };
291
+ const manifest = { kind: WORLD_MANIFEST_KIND, shards };
292
+ if (typeof raw.version === "number")
293
+ manifest.version = raw.version;
294
+ if (grid !== undefined)
295
+ manifest.grid = grid;
296
+ if (streaming !== undefined)
297
+ manifest.streaming = streaming;
298
+ return { ok: true, manifest };
299
+ }
300
+ /** Serializes a world manifest to pretty JSON for writing `world.json`. */
301
+ export function exportWorldManifestJson(manifest, pretty = true) {
302
+ return JSON.stringify(manifest, null, pretty ? 2 : undefined);
303
+ }
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Deterministic creature breeding & genetics — a genre-agnostic engine primitive for
3
+ * taming/husbandry games. Everything here operates over plain serializable records so a
4
+ * genome round-trips through save/load and multiplayer sync, and every random decision is
5
+ * driven by an injected `rng: () => number` (values in `[0, 1)`) so identical inputs and an
6
+ * identical rng stream always produce the identical offspring.
7
+ *
8
+ * The lifecycle it models:
9
+ * - **inheritance** — each stat is drawn independently from the higher or lower parent,
10
+ * - **mutation** — a bounded number of rolls that boost a stat, bump a heritable lineage
11
+ * counter, and optionally flag a cosmetic/color mutation, gated by a soft cap,
12
+ * - **imprint** — a rearing bonus accrued by fulfilling care requests over maturation,
13
+ * - **incubation & maturation** — temperature-gated gestation viability and stage resolution.
14
+ *
15
+ * No species tables, coordinates, or game content live here; callers supply their own stat
16
+ * ids and tuning.
17
+ */
18
+ /** A stat block: stat id → numeric point value. Plain serializable record, no ordering assumed. */
19
+ export type StatBlock = Record<string, number>;
20
+ /**
21
+ * A creature's heritable genome. `stats` are the inherited/mutated point values;
22
+ * `mutationCount` and `colorMutationCount` are the lineage counters that carry across
23
+ * generations and drive the mutation soft cap. Fully serializable — no methods or closures.
24
+ */
25
+ export interface Genome {
26
+ stats: StatBlock;
27
+ /** Accumulated point mutations in this creature's lineage. Heritable, monotonic. */
28
+ mutationCount: number;
29
+ /** Accumulated cosmetic/color mutations in this creature's lineage. Heritable, monotonic. */
30
+ colorMutationCount: number;
31
+ }
32
+ /** One mutation that occurred while breeding a single offspring. */
33
+ export interface MutationEvent {
34
+ /** Stat id that received the point delta. */
35
+ stat: string;
36
+ /** Points added to that stat (the configured `mutationDelta`). */
37
+ delta: number;
38
+ /** True when this mutation was also flagged cosmetic/color. */
39
+ color: boolean;
40
+ }
41
+ /**
42
+ * Soft-cap rule for mutation eligibility. When a parent's `mutationCount` exceeds
43
+ * `threshold` its lineage is considered "capped"; the per-roll mutation chance is then
44
+ * either zeroed (`mode: "block"`) or scaled by `reducedFactor` (`mode: "reduce"`).
45
+ * `scope` decides whether it takes `both` parents capped, or `either` one, to trigger.
46
+ */
47
+ export interface MutationSoftCap {
48
+ /** Counter value a parent must exceed to count as capped. Default `20`. */
49
+ threshold?: number;
50
+ /** Trigger when `both` parents are capped (default) or `either` one is. */
51
+ scope?: "both" | "either";
52
+ /** Zero the chance (`block`, default) or scale it by `reducedFactor` (`reduce`). */
53
+ mode?: "block" | "reduce";
54
+ /** Multiplier applied to the base chance when `mode: "reduce"` and capped. Default `0.5`. */
55
+ reducedFactor?: number;
56
+ }
57
+ /** Tuning for {@link breedOffspring}. Every field has an ARK-style default; all are optional. */
58
+ export interface BreedConfig {
59
+ /** Per-stat probability the offspring inherits the higher parent's value. Default `0.55`. */
60
+ higherStatChance?: number;
61
+ /** Number of independent mutation rolls per offspring. Default `3`. */
62
+ mutationRolls?: number;
63
+ /** Probability each mutation roll succeeds (before the soft cap). Default `0.025`. */
64
+ mutationChance?: number;
65
+ /** Points added to a random stat by a successful mutation. Default `2`. */
66
+ mutationDelta?: number;
67
+ /** Probability a successful mutation is additionally flagged cosmetic. Default `0.5`. */
68
+ colorMutationChance?: number;
69
+ /** Mutation-count soft cap rule. Defaults to `{ threshold: 20, scope: "both", mode: "block" }`. */
70
+ mutationSoftCap?: MutationSoftCap;
71
+ }
72
+ /** Result of {@link breedOffspring}: the new genome plus the mutations that produced it. */
73
+ export interface BreedResult {
74
+ genome: Genome;
75
+ mutations: readonly MutationEvent[];
76
+ }
77
+ /**
78
+ * Breed one offspring from two parents. For each stat present in either parent (iterated in
79
+ * sorted id order for a stable rng stream) the offspring independently inherits the higher
80
+ * parent's value with probability `higherStatChance` (default `0.55`), else the lower — a
81
+ * per-stat coin flip, so a strong parent does not guarantee a strong child. Then up to
82
+ * `mutationRolls` mutation rolls fire at the soft-capped chance; each success adds
83
+ * `mutationDelta` to a random stat, bumps the heritable `mutationCount`, and is flagged
84
+ * cosmetic with probability `colorMutationChance`. Lineage counters carry as the max of the
85
+ * two parents plus the new mutations, keeping them monotonic and heritable.
86
+ *
87
+ * Deterministic: the same parents, config, and rng sequence yield byte-identical results.
88
+ *
89
+ * @capability creature-breeding deterministic stat inheritance, mutation, and lineage genetics for taming games
90
+ */
91
+ export declare function breedOffspring(parentA: Genome, parentB: Genome, rng: () => number, config?: BreedConfig): BreedResult;
92
+ /** Options controlling how an imprint bonus is applied. */
93
+ export interface ImprintBonusConfig {
94
+ /** Stat multiplier at full imprint, applied to everyone. Default `0.20` (+20%). */
95
+ statBonusAtFull?: number;
96
+ /** Extra multiplier at full imprint, applied only for the imprinting owner. Default `0.30` (+30%). */
97
+ handlerBonusAtFull?: number;
98
+ }
99
+ /**
100
+ * Imprint gained per fulfilled care request when `requestCount` requests are scheduled over
101
+ * the maturation window: `1 / requestCount`, so fulfilling every request reaches full (1.0)
102
+ * imprint. Returns `0` for a non-positive request count.
103
+ *
104
+ * @capability imprint-increment per-care-request imprint gain across a maturation schedule
105
+ */
106
+ export declare function imprintIncrementPerRequest(requestCount: number): number;
107
+ /**
108
+ * Apply the imprint rearing bonus to a stat block. Every stat is scaled by
109
+ * `1 + statBonusAtFull * imprint`; when `asOwner` is set, the imprinting handler additionally
110
+ * gets `1 + handlerBonusAtFull * imprint`. `imprint` is a 0..1 fraction (clamped) and scales
111
+ * both bonuses linearly, so half-imprint yields half the bonus. Returns a fresh stat block.
112
+ *
113
+ * @capability imprint-bonus apply imprint stat and handler bonuses at an imprint fraction
114
+ */
115
+ export declare function applyImprintBonus(stats: StatBlock, imprint: number, config?: ImprintBonusConfig, options?: {
116
+ asOwner?: boolean;
117
+ }): StatBlock;
118
+ /** Serializable incubation/gestation state advanced by {@link tickIncubation}. */
119
+ export interface IncubationState {
120
+ /** Remaining egg/embryo health; incubation fails at `0`. */
121
+ health: number;
122
+ /** Accumulated in-range incubation time; only advances while temperature is viable. */
123
+ elapsed: number;
124
+ }
125
+ /** Temperature gate for {@link tickIncubation}. */
126
+ export interface IncubationConfig {
127
+ minTemp: number;
128
+ maxTemp: number;
129
+ /** Health lost per unit `dt` while temperature is outside `[minTemp, maxTemp]`. Default `1`. */
130
+ healthLossPerTick?: number;
131
+ }
132
+ /**
133
+ * Advance incubation one tick. While `temperature` is within `[minTemp, maxTemp]` the embryo
134
+ * makes progress (`elapsed += dt`) and keeps its health; outside the range it makes no
135
+ * progress and loses `healthLossPerTick * dt` health (clamped at `0`). Pure — returns a new
136
+ * state, never mutates the input.
137
+ *
138
+ * @capability incubation-tick advance egg incubation health inside/outside its temperature range
139
+ */
140
+ export declare function tickIncubation(state: IncubationState, temperature: number, dt: number, config: IncubationConfig): IncubationState;
141
+ /**
142
+ * True while the incubation still has health to hatch.
143
+ *
144
+ * @capability incubation-viability test whether an egg incubation still has health to hatch
145
+ */
146
+ export declare function incubationViable(state: IncubationState): boolean;
147
+ /** A maturation stage keyed by the elapsed fraction (0..1) at which it begins. */
148
+ export interface MaturationStage {
149
+ id: string;
150
+ /** Fraction of the maturation duration at which this stage starts (inclusive). */
151
+ at: number;
152
+ }
153
+ /**
154
+ * Resolve the maturation stage for an elapsed time against a duration. Computes the clamped
155
+ * `elapsed / duration` fraction and returns the id of the last stage whose `at` threshold has
156
+ * been reached (boundaries inclusive). Stages need not be pre-sorted. Returns `""` when no
157
+ * stage qualifies (e.g. an empty list or all thresholds above the current fraction).
158
+ *
159
+ * @capability maturation-stage resolve the baby/juvenile/adolescent/adult stage from elapsed maturation
160
+ */
161
+ export declare function maturationStage(elapsed: number, duration: number, stages: readonly MaturationStage[]): string;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Deterministic creature breeding & genetics — a genre-agnostic engine primitive for
3
+ * taming/husbandry games. Everything here operates over plain serializable records so a
4
+ * genome round-trips through save/load and multiplayer sync, and every random decision is
5
+ * driven by an injected `rng: () => number` (values in `[0, 1)`) so identical inputs and an
6
+ * identical rng stream always produce the identical offspring.
7
+ *
8
+ * The lifecycle it models:
9
+ * - **inheritance** — each stat is drawn independently from the higher or lower parent,
10
+ * - **mutation** — a bounded number of rolls that boost a stat, bump a heritable lineage
11
+ * counter, and optionally flag a cosmetic/color mutation, gated by a soft cap,
12
+ * - **imprint** — a rearing bonus accrued by fulfilling care requests over maturation,
13
+ * - **incubation & maturation** — temperature-gated gestation viability and stage resolution.
14
+ *
15
+ * No species tables, coordinates, or game content live here; callers supply their own stat
16
+ * ids and tuning.
17
+ */
18
+ const DEFAULT_HIGHER_STAT_CHANCE = 0.55;
19
+ const DEFAULT_MUTATION_ROLLS = 3;
20
+ const DEFAULT_MUTATION_CHANCE = 0.025;
21
+ const DEFAULT_MUTATION_DELTA = 2;
22
+ const DEFAULT_COLOR_MUTATION_CHANCE = 0.5;
23
+ const DEFAULT_SOFT_CAP_THRESHOLD = 20;
24
+ const DEFAULT_REDUCED_FACTOR = 0.5;
25
+ function clamp01(x) {
26
+ return x < 0 ? 0 : x > 1 ? 1 : x;
27
+ }
28
+ /**
29
+ * Per-roll mutation chance after applying the soft cap to both parents. Exposed so callers
30
+ * and tests can inspect the exact gating rule without breeding.
31
+ *
32
+ * @internal
33
+ */
34
+ function effectiveMutationChance(a, b, config) {
35
+ const base = config.mutationChance ?? DEFAULT_MUTATION_CHANCE;
36
+ const cap = config.mutationSoftCap;
37
+ const threshold = cap?.threshold ?? DEFAULT_SOFT_CAP_THRESHOLD;
38
+ const scope = cap?.scope ?? "both";
39
+ const mode = cap?.mode ?? "block";
40
+ const aOver = a.mutationCount > threshold;
41
+ const bOver = b.mutationCount > threshold;
42
+ const capped = scope === "both" ? aOver && bOver : aOver || bOver;
43
+ if (!capped)
44
+ return base;
45
+ return mode === "reduce" ? base * (cap?.reducedFactor ?? DEFAULT_REDUCED_FACTOR) : 0;
46
+ }
47
+ /**
48
+ * Breed one offspring from two parents. For each stat present in either parent (iterated in
49
+ * sorted id order for a stable rng stream) the offspring independently inherits the higher
50
+ * parent's value with probability `higherStatChance` (default `0.55`), else the lower — a
51
+ * per-stat coin flip, so a strong parent does not guarantee a strong child. Then up to
52
+ * `mutationRolls` mutation rolls fire at the soft-capped chance; each success adds
53
+ * `mutationDelta` to a random stat, bumps the heritable `mutationCount`, and is flagged
54
+ * cosmetic with probability `colorMutationChance`. Lineage counters carry as the max of the
55
+ * two parents plus the new mutations, keeping them monotonic and heritable.
56
+ *
57
+ * Deterministic: the same parents, config, and rng sequence yield byte-identical results.
58
+ *
59
+ * @capability creature-breeding deterministic stat inheritance, mutation, and lineage genetics for taming games
60
+ */
61
+ export function breedOffspring(parentA, parentB, rng, config = {}) {
62
+ const higherChance = config.higherStatChance ?? DEFAULT_HIGHER_STAT_CHANCE;
63
+ const rolls = Math.max(0, Math.floor(config.mutationRolls ?? DEFAULT_MUTATION_ROLLS));
64
+ const delta = config.mutationDelta ?? DEFAULT_MUTATION_DELTA;
65
+ const colorChance = config.colorMutationChance ?? DEFAULT_COLOR_MUTATION_CHANCE;
66
+ // Stable union of stat ids so the rng stream does not depend on object key order.
67
+ const statIds = Array.from(new Set([...Object.keys(parentA.stats), ...Object.keys(parentB.stats)])).sort();
68
+ const stats = {};
69
+ for (const id of statIds) {
70
+ const av = parentA.stats[id];
71
+ const bv = parentB.stats[id];
72
+ // A stat missing on one side behaves as if both parents shared the present value:
73
+ // one rng draw is still consumed so adding a stat to a parent cannot shift the stream.
74
+ const present = av ?? bv ?? 0;
75
+ const high = av === undefined || bv === undefined ? present : Math.max(av, bv);
76
+ const low = av === undefined || bv === undefined ? present : Math.min(av, bv);
77
+ stats[id] = rng() < higherChance ? high : low;
78
+ }
79
+ const chance = effectiveMutationChance(parentA, parentB, config);
80
+ const mutations = [];
81
+ let newMutations = 0;
82
+ let newColor = 0;
83
+ for (let i = 0; i < rolls; i++) {
84
+ if (rng() >= chance)
85
+ continue;
86
+ newMutations += 1;
87
+ let stat = "";
88
+ if (statIds.length > 0) {
89
+ const index = Math.min(statIds.length - 1, Math.floor(rng() * statIds.length));
90
+ stat = statIds[index];
91
+ stats[stat] = (stats[stat] ?? 0) + delta;
92
+ }
93
+ const color = rng() < colorChance;
94
+ if (color)
95
+ newColor += 1;
96
+ mutations.push({ stat, delta: stat === "" ? 0 : delta, color });
97
+ }
98
+ const genome = {
99
+ stats,
100
+ mutationCount: Math.max(parentA.mutationCount, parentB.mutationCount) + newMutations,
101
+ colorMutationCount: Math.max(parentA.colorMutationCount, parentB.colorMutationCount) + newColor,
102
+ };
103
+ return { genome, mutations };
104
+ }
105
+ const DEFAULT_STAT_BONUS = 0.2;
106
+ const DEFAULT_HANDLER_BONUS = 0.3;
107
+ /**
108
+ * Imprint gained per fulfilled care request when `requestCount` requests are scheduled over
109
+ * the maturation window: `1 / requestCount`, so fulfilling every request reaches full (1.0)
110
+ * imprint. Returns `0` for a non-positive request count.
111
+ *
112
+ * @capability imprint-increment per-care-request imprint gain across a maturation schedule
113
+ */
114
+ export function imprintIncrementPerRequest(requestCount) {
115
+ return requestCount > 0 ? 1 / requestCount : 0;
116
+ }
117
+ /**
118
+ * Apply the imprint rearing bonus to a stat block. Every stat is scaled by
119
+ * `1 + statBonusAtFull * imprint`; when `asOwner` is set, the imprinting handler additionally
120
+ * gets `1 + handlerBonusAtFull * imprint`. `imprint` is a 0..1 fraction (clamped) and scales
121
+ * both bonuses linearly, so half-imprint yields half the bonus. Returns a fresh stat block.
122
+ *
123
+ * @capability imprint-bonus apply imprint stat and handler bonuses at an imprint fraction
124
+ */
125
+ export function applyImprintBonus(stats, imprint, config = {}, options = {}) {
126
+ const fraction = clamp01(imprint);
127
+ const statBonus = config.statBonusAtFull ?? DEFAULT_STAT_BONUS;
128
+ const handlerBonus = config.handlerBonusAtFull ?? DEFAULT_HANDLER_BONUS;
129
+ let multiplier = 1 + statBonus * fraction;
130
+ if (options.asOwner)
131
+ multiplier *= 1 + handlerBonus * fraction;
132
+ const out = {};
133
+ for (const id of Object.keys(stats))
134
+ out[id] = stats[id] * multiplier;
135
+ return out;
136
+ }
137
+ /**
138
+ * Advance incubation one tick. While `temperature` is within `[minTemp, maxTemp]` the embryo
139
+ * makes progress (`elapsed += dt`) and keeps its health; outside the range it makes no
140
+ * progress and loses `healthLossPerTick * dt` health (clamped at `0`). Pure — returns a new
141
+ * state, never mutates the input.
142
+ *
143
+ * @capability incubation-tick advance egg incubation health inside/outside its temperature range
144
+ */
145
+ export function tickIncubation(state, temperature, dt, config) {
146
+ if (dt <= 0)
147
+ return { health: state.health, elapsed: state.elapsed };
148
+ const inRange = temperature >= config.minTemp && temperature <= config.maxTemp;
149
+ if (inRange)
150
+ return { health: state.health, elapsed: state.elapsed + dt };
151
+ const loss = (config.healthLossPerTick ?? 1) * dt;
152
+ return { health: Math.max(0, state.health - loss), elapsed: state.elapsed };
153
+ }
154
+ /**
155
+ * True while the incubation still has health to hatch.
156
+ *
157
+ * @capability incubation-viability test whether an egg incubation still has health to hatch
158
+ */
159
+ export function incubationViable(state) {
160
+ return state.health > 0;
161
+ }
162
+ /**
163
+ * Resolve the maturation stage for an elapsed time against a duration. Computes the clamped
164
+ * `elapsed / duration` fraction and returns the id of the last stage whose `at` threshold has
165
+ * been reached (boundaries inclusive). Stages need not be pre-sorted. Returns `""` when no
166
+ * stage qualifies (e.g. an empty list or all thresholds above the current fraction).
167
+ *
168
+ * @capability maturation-stage resolve the baby/juvenile/adolescent/adult stage from elapsed maturation
169
+ */
170
+ export function maturationStage(elapsed, duration, stages) {
171
+ const fraction = duration > 0 ? clamp01(elapsed / duration) : 1;
172
+ let bestId = "";
173
+ let bestAt = -Infinity;
174
+ for (const stage of stages) {
175
+ if (stage.at <= fraction && stage.at >= bestAt) {
176
+ bestAt = stage.at;
177
+ bestId = stage.id;
178
+ }
179
+ }
180
+ return bestId;
181
+ }