@jgengine/core 0.5.0 → 0.7.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 (244) hide show
  1. package/CHANGELOG.md +191 -2
  2. package/dist/ai/crowd.d.ts +36 -0
  3. package/dist/ai/crowd.js +213 -0
  4. package/dist/ai/jobBoard.d.ts +49 -0
  5. package/dist/ai/jobBoard.js +155 -0
  6. package/dist/ai/spawnDirector.d.ts +56 -0
  7. package/dist/ai/spawnDirector.js +150 -0
  8. package/dist/ai/threat.d.ts +25 -0
  9. package/dist/ai/threat.js +67 -0
  10. package/dist/audio/audioFalloff.d.ts +38 -0
  11. package/dist/audio/audioFalloff.js +32 -0
  12. package/dist/board/laneBoard.d.ts +63 -0
  13. package/dist/board/laneBoard.js +114 -0
  14. package/dist/board/timelineBoard.d.ts +36 -0
  15. package/dist/board/timelineBoard.js +70 -0
  16. package/dist/cards/cardPile.d.ts +63 -0
  17. package/dist/cards/cardPile.js +166 -0
  18. package/dist/cards/modifierPipeline.d.ts +33 -0
  19. package/dist/cards/modifierPipeline.js +48 -0
  20. package/dist/combat/abilityKit.d.ts +42 -0
  21. package/dist/combat/abilityKit.js +129 -0
  22. package/dist/combat/animationState.d.ts +47 -0
  23. package/dist/combat/animationState.js +101 -0
  24. package/dist/combat/attackTags.d.ts +14 -0
  25. package/dist/combat/attackTags.js +26 -0
  26. package/dist/combat/breakMeters.d.ts +32 -0
  27. package/dist/combat/breakMeters.js +51 -0
  28. package/dist/combat/comboString.d.ts +35 -0
  29. package/dist/combat/comboString.js +59 -0
  30. package/dist/combat/death.d.ts +8 -0
  31. package/dist/combat/death.js +5 -2
  32. package/dist/combat/defensiveWindow.d.ts +34 -0
  33. package/dist/combat/defensiveWindow.js +66 -0
  34. package/dist/combat/downed.d.ts +35 -0
  35. package/dist/combat/downed.js +104 -0
  36. package/dist/combat/effects.d.ts +11 -0
  37. package/dist/combat/effects.js +22 -13
  38. package/dist/combat/hitReaction.d.ts +23 -0
  39. package/dist/combat/hitReaction.js +20 -0
  40. package/dist/combat/resistance.d.ts +13 -0
  41. package/dist/combat/resistance.js +46 -0
  42. package/dist/combat/telegraph.d.ts +27 -0
  43. package/dist/combat/telegraph.js +37 -0
  44. package/dist/crafting/crop.d.ts +59 -0
  45. package/dist/crafting/crop.js +181 -0
  46. package/dist/crafting/production.d.ts +64 -0
  47. package/dist/crafting/production.js +130 -0
  48. package/dist/crafting/recipe.d.ts +62 -0
  49. package/dist/crafting/recipe.js +101 -0
  50. package/dist/economy/sharedWallet.d.ts +28 -0
  51. package/dist/economy/sharedWallet.js +62 -0
  52. package/dist/economy/techTree.d.ts +40 -0
  53. package/dist/economy/techTree.js +66 -0
  54. package/dist/game/cosmetics.d.ts +25 -0
  55. package/dist/game/cosmetics.js +51 -0
  56. package/dist/game/defineGame.d.ts +5 -0
  57. package/dist/game/events.d.ts +62 -0
  58. package/dist/game/lootFilter.d.ts +33 -0
  59. package/dist/game/lootFilter.js +48 -0
  60. package/dist/game/ping.d.ts +67 -0
  61. package/dist/game/ping.js +86 -0
  62. package/dist/game/playableGame.d.ts +291 -5
  63. package/dist/game/race.d.ts +95 -0
  64. package/dist/game/race.js +216 -0
  65. package/dist/game/runDraft.d.ts +34 -0
  66. package/dist/game/runDraft.js +97 -0
  67. package/dist/game/social.d.ts +27 -0
  68. package/dist/game/social.js +16 -0
  69. package/dist/game/worldItem.d.ts +92 -0
  70. package/dist/game/worldItem.js +132 -0
  71. package/dist/input/axisInput.d.ts +43 -0
  72. package/dist/input/axisInput.js +70 -0
  73. package/dist/input/pointer.d.ts +24 -0
  74. package/dist/input/pointer.js +17 -0
  75. package/dist/interaction/contextMenu.d.ts +30 -0
  76. package/dist/interaction/contextMenu.js +22 -0
  77. package/dist/interaction/qte.d.ts +20 -0
  78. package/dist/interaction/qte.js +24 -0
  79. package/dist/interaction/skillCheck.d.ts +20 -0
  80. package/dist/interaction/skillCheck.js +26 -0
  81. package/dist/inventory/shapedGrid.d.ts +56 -0
  82. package/dist/inventory/shapedGrid.js +176 -0
  83. package/dist/inventory/storageTier.d.ts +52 -0
  84. package/dist/inventory/storageTier.js +77 -0
  85. package/dist/item/affix.d.ts +54 -0
  86. package/dist/item/affix.js +133 -0
  87. package/dist/item/durability.d.ts +51 -0
  88. package/dist/item/durability.js +78 -0
  89. package/dist/item/modularItem.d.ts +46 -0
  90. package/dist/item/modularItem.js +75 -0
  91. package/dist/meta/changelog.d.ts +1 -1
  92. package/dist/meta/changelog.js +53 -1
  93. package/dist/movement/dash.d.ts +37 -0
  94. package/dist/movement/dash.js +65 -0
  95. package/dist/multiplayer/combatSnapshot.d.ts +45 -0
  96. package/dist/multiplayer/combatSnapshot.js +89 -0
  97. package/dist/multiplayer/lagCompensation.d.ts +43 -0
  98. package/dist/multiplayer/lagCompensation.js +154 -0
  99. package/dist/multiplayer/matchmaking.d.ts +33 -0
  100. package/dist/multiplayer/matchmaking.js +60 -0
  101. package/dist/multiplayer/simultaneousCommit.d.ts +34 -0
  102. package/dist/multiplayer/simultaneousCommit.js +53 -0
  103. package/dist/nav/navGrid.d.ts +46 -0
  104. package/dist/nav/navGrid.js +248 -0
  105. package/dist/nav/pathFollow.d.ts +29 -0
  106. package/dist/nav/pathFollow.js +67 -0
  107. package/dist/physics/buoyancy.d.ts +49 -0
  108. package/dist/physics/buoyancy.js +112 -0
  109. package/dist/physics/carryable.d.ts +46 -0
  110. package/dist/physics/carryable.js +110 -0
  111. package/dist/physics/damageZones.d.ts +56 -0
  112. package/dist/physics/damageZones.js +107 -0
  113. package/dist/physics/forceVolume.d.ts +48 -0
  114. package/dist/physics/forceVolume.js +119 -0
  115. package/dist/physics/physicsWorld.d.ts +219 -0
  116. package/dist/physics/physicsWorld.js +749 -0
  117. package/dist/physics/ragdoll.d.ts +49 -0
  118. package/dist/physics/ragdoll.js +103 -0
  119. package/dist/physics/spatialGrid.d.ts +41 -0
  120. package/dist/physics/spatialGrid.js +138 -0
  121. package/dist/physics/structure.d.ts +83 -0
  122. package/dist/physics/structure.js +0 -0
  123. package/dist/physics/traversal.d.ts +75 -0
  124. package/dist/physics/traversal.js +138 -0
  125. package/dist/physics/vehicleBody.d.ts +93 -0
  126. package/dist/physics/vehicleBody.js +234 -0
  127. package/dist/runtime/gameContext.d.ts +66 -2
  128. package/dist/runtime/gameContext.js +157 -4
  129. package/dist/runtime/hostPersistence.d.ts +18 -0
  130. package/dist/runtime/hostPersistence.js +4 -0
  131. package/dist/runtime/persistenceScope.d.ts +32 -0
  132. package/dist/runtime/persistenceScope.js +64 -0
  133. package/dist/scene/assetCatalog.d.ts +13 -0
  134. package/dist/scene/autoTarget.d.ts +15 -0
  135. package/dist/scene/autoTarget.js +54 -0
  136. package/dist/scene/behaviors.d.ts +13 -1
  137. package/dist/scene/behaviors.js +3 -0
  138. package/dist/scene/captureCheck.d.ts +7 -0
  139. package/dist/scene/captureCheck.js +11 -0
  140. package/dist/scene/entityStore.d.ts +7 -1
  141. package/dist/scene/entityStore.js +15 -1
  142. package/dist/scene/form.d.ts +40 -0
  143. package/dist/scene/form.js +56 -0
  144. package/dist/scene/mount.d.ts +62 -0
  145. package/dist/scene/mount.js +107 -0
  146. package/dist/scene/possession.d.ts +30 -0
  147. package/dist/scene/possession.js +51 -0
  148. package/dist/scene/roster.d.ts +25 -0
  149. package/dist/scene/roster.js +57 -0
  150. package/dist/scene/selection.d.ts +30 -0
  151. package/dist/scene/selection.js +38 -0
  152. package/dist/scene/stationClaim.d.ts +43 -0
  153. package/dist/scene/stationClaim.js +109 -0
  154. package/dist/sensor/frustumSensor.d.ts +60 -0
  155. package/dist/sensor/frustumSensor.js +104 -0
  156. package/dist/sensor/hiddenStateProbe.d.ts +31 -0
  157. package/dist/sensor/hiddenStateProbe.js +40 -0
  158. package/dist/sensor/recordingBuffer.d.ts +29 -0
  159. package/dist/sensor/recordingBuffer.js +61 -0
  160. package/dist/sensor/revealQuery.d.ts +22 -0
  161. package/dist/sensor/revealQuery.js +36 -0
  162. package/dist/session/contestedChannel.d.ts +36 -0
  163. package/dist/session/contestedChannel.js +100 -0
  164. package/dist/session/extraction.d.ts +54 -0
  165. package/dist/session/extraction.js +108 -0
  166. package/dist/session/ring.d.ts +36 -0
  167. package/dist/session/ring.js +88 -0
  168. package/dist/session/roundState.d.ts +55 -0
  169. package/dist/session/roundState.js +129 -0
  170. package/dist/stats/accumulatorMeter.d.ts +31 -0
  171. package/dist/stats/accumulatorMeter.js +81 -0
  172. package/dist/stats/eventMeter.d.ts +34 -0
  173. package/dist/stats/eventMeter.js +76 -0
  174. package/dist/stats/rollCheck.d.ts +15 -0
  175. package/dist/stats/rollCheck.js +15 -0
  176. package/dist/survival/decayMeter.d.ts +59 -0
  177. package/dist/survival/decayMeter.js +95 -0
  178. package/dist/survival/moodle.d.ts +48 -0
  179. package/dist/survival/moodle.js +91 -0
  180. package/dist/survival/regionHealth.d.ts +72 -0
  181. package/dist/survival/regionHealth.js +129 -0
  182. package/dist/tactics/predictiveQuery.d.ts +34 -0
  183. package/dist/tactics/predictiveQuery.js +44 -0
  184. package/dist/tactics/snapshot.d.ts +18 -0
  185. package/dist/tactics/snapshot.js +65 -0
  186. package/dist/tactics/surface.d.ts +52 -0
  187. package/dist/tactics/surface.js +135 -0
  188. package/dist/tactics/tacticalGrid.d.ts +57 -0
  189. package/dist/tactics/tacticalGrid.js +244 -0
  190. package/dist/time/beatClock.d.ts +40 -0
  191. package/dist/time/beatClock.js +73 -0
  192. package/dist/time/simClock.d.ts +62 -0
  193. package/dist/time/simClock.js +132 -0
  194. package/dist/turn/commit.d.ts +27 -0
  195. package/dist/turn/commit.js +72 -0
  196. package/dist/turn/turnLoop.d.ts +58 -0
  197. package/dist/turn/turnLoop.js +216 -0
  198. package/dist/world/buildPermissions.d.ts +47 -0
  199. package/dist/world/buildPermissions.js +134 -0
  200. package/dist/world/buildingIndex.d.ts +16 -0
  201. package/dist/world/buildingIndex.js +63 -0
  202. package/dist/world/buildings.d.ts +98 -0
  203. package/dist/world/buildings.js +320 -0
  204. package/dist/world/carve.d.ts +105 -0
  205. package/dist/world/carve.js +175 -0
  206. package/dist/world/connectors.d.ts +40 -0
  207. package/dist/world/connectors.js +67 -0
  208. package/dist/world/envField.d.ts +71 -0
  209. package/dist/world/envField.js +96 -0
  210. package/dist/world/features.d.ts +105 -1
  211. package/dist/world/features.js +95 -0
  212. package/dist/world/fog.d.ts +40 -0
  213. package/dist/world/fog.js +137 -0
  214. package/dist/world/markers.d.ts +52 -0
  215. package/dist/world/markers.js +96 -0
  216. package/dist/world/minimap.d.ts +42 -0
  217. package/dist/world/minimap.js +72 -0
  218. package/dist/world/placedStructureStore.d.ts +39 -0
  219. package/dist/world/placedStructureStore.js +95 -0
  220. package/dist/world/placementController.d.ts +48 -0
  221. package/dist/world/placementController.js +97 -0
  222. package/dist/world/realm.d.ts +61 -0
  223. package/dist/world/realm.js +66 -0
  224. package/dist/world/regions.d.ts +66 -0
  225. package/dist/world/regions.js +158 -0
  226. package/dist/world/scatter.d.ts +24 -0
  227. package/dist/world/scatter.js +126 -0
  228. package/dist/world/scatterItems.d.ts +38 -0
  229. package/dist/world/scatterItems.js +85 -0
  230. package/dist/world/support.d.ts +27 -0
  231. package/dist/world/support.js +75 -0
  232. package/dist/world/terraform.d.ts +54 -0
  233. package/dist/world/terraform.js +186 -0
  234. package/dist/world/terrain.d.ts +54 -0
  235. package/dist/world/terrain.js +181 -0
  236. package/dist/world/walls.d.ts +61 -0
  237. package/dist/world/walls.js +221 -0
  238. package/dist/world/water.d.ts +27 -0
  239. package/dist/world/water.js +125 -0
  240. package/dist/world/weather.d.ts +92 -0
  241. package/dist/world/weather.js +169 -0
  242. package/dist/world/wind.d.ts +17 -0
  243. package/dist/world/wind.js +40 -0
  244. package/package.json +1 -1
@@ -1,14 +1,18 @@
1
- import { createDeathSystem, deathReasonFromEffect } from "../combat/death.js";
1
+ import { createDeathSystem, deathReasonFromEffect, normalizeOnDeath } from "../combat/death.js";
2
2
  import { createEffectSystem, } from "../combat/effects.js";
3
3
  import { createProjectileSystem } from "../combat/projectiles.js";
4
+ import { resolveHitReaction, } from "../combat/hitReaction.js";
5
+ import { pointInTelegraph, } from "../combat/telegraph.js";
4
6
  import { createCommandRegistry, } from "../commands/commandRegistry.js";
5
7
  import { balance as walletBalance, canAfford as walletCanAfford, charge as walletCharge, chargeAll as walletChargeAll, createEmptyWallet, grant as walletGrant, } from "../economy/wallet.js";
8
+ import { createCosmetics } from "../game/cosmetics.js";
6
9
  import { createGameEvents } from "../game/events.js";
7
10
  import { createGameFeed } from "../game/feed.js";
8
11
  import { createLeaderboard } from "../game/leaderboard.js";
9
12
  import { createLoadouts } from "../game/loadout.js";
10
13
  import { createLootRegistry, grantDrops } from "../game/lootTable.js";
11
14
  import { createQuestJournal } from "../game/quest.js";
15
+ import { createWorldItemStore, resolveDeathDrops, DEFAULT_RARITY, WORLD_ITEM_ENTITY_NAME, } from "../game/worldItem.js";
12
16
  import { createSocial } from "../game/social.js";
13
17
  import { createTradeSystem } from "../game/trade.js";
14
18
  import { createUnlocks } from "../game/unlocks.js";
@@ -17,15 +21,20 @@ import { createItemUse, } from "../item/use.js";
17
21
  import { createWeaponStats } from "../item/weapon.js";
18
22
  import { createPoseState } from "../movement/poseState.js";
19
23
  import { createEntityStatsApi, seedStatValues, setStatValue, } from "../scene/entityStats.js";
24
+ import { createForms } from "../scene/form.js";
20
25
  import { createObjectStore } from "../scene/objectStore.js";
26
+ import { createRoster } from "../scene/roster.js";
27
+ import { createPossession } from "../scene/possession.js";
21
28
  import { createSpatialApi } from "../scene/spatial.js";
22
29
  import { createTargeting } from "../scene/targeting.js";
23
30
  import { createStats } from "../stats/statModifiers.js";
24
31
  import { createChangeSignal, notifyAfter } from "../store/changeSignal.js";
32
+ import { createSimClock } from "../time/simClock.js";
25
33
  export function createGameContext(options) {
26
34
  const { definition, content, player } = options;
27
35
  const now = options.now ?? Date.now;
28
36
  const signal = createChangeSignal();
37
+ const time = createSimClock({ config: definition.time, onChange: signal.notify });
29
38
  const entities = definition.scene;
30
39
  const objects = createObjectStore();
31
40
  entities.subscribe(signal.notify);
@@ -78,17 +87,29 @@ export function createGameContext(options) {
78
87
  const feed = createGameFeed();
79
88
  const lootRegistry = createLootRegistry();
80
89
  const unlocks = notifyAfter(createUnlocks(), ["grant", "hydrate"], signal.notify);
81
- const rawSocial = createSocial({ events, now });
90
+ const rawSocial = createSocial({
91
+ events,
92
+ now,
93
+ emotes: {
94
+ entities: { get: (id) => entities.get(id) },
95
+ spatial: { inRadius: (center, radius, filter) => spatial.inRadius(center, radius, filter) },
96
+ },
97
+ });
82
98
  const social = {
83
99
  friends: notifyAfter(rawSocial.friends, ["request", "accept", "remove", "block", "hydrate"], signal.notify),
84
100
  party: notifyAfter(rawSocial.party, ["invite", "accept", "kick", "leave", "promote"], signal.notify),
85
101
  presence: rawSocial.presence,
102
+ emotes: rawSocial.emotes,
86
103
  };
87
104
  const leaderboard = notifyAfter(createLeaderboard(), ["increment", "hydrate"], signal.notify);
105
+ const roster = notifyAfter(createRoster({ now }), ["capture", "release", "setEquipped", "hydrate"], signal.notify);
88
106
  const playerStats = createStats({});
89
107
  const pose = createPoseState((instanceId) => catalogEntry(instanceId)?.movement);
90
108
  const commandRegistry = createCommandRegistry();
91
109
  const itemUse = createItemUse((itemId) => content.itemById?.(itemId)?.use);
110
+ const possession = notifyAfter(createPossession({ entities, events }), ["possess", "own", "disown"], signal.notify);
111
+ const forms = notifyAfter(createForms({ entities, time, events }), ["shapeshift", "revert"], signal.notify);
112
+ const cosmetics = notifyAfter(createCosmetics({ events }), ["apply", "equip", "hydrate"], signal.notify);
92
113
  const inventoryDeclarations = definition.inventories ?? {};
93
114
  const inventoryIds = Object.keys(inventoryDeclarations);
94
115
  const layouts = {};
@@ -240,6 +261,37 @@ export function createGameContext(options) {
240
261
  pose.clear(instanceId);
241
262
  return existed;
242
263
  }
264
+ const worldItems = notifyAfter(createWorldItemStore({
265
+ spawnEntity: (position) => entities.spawn(WORLD_ITEM_ENTITY_NAME, { position, role: "prop" }),
266
+ despawnEntity,
267
+ resolvePosition: (instanceId) => entities.get(instanceId)?.position,
268
+ }), ["spawn", "take"], signal.notify);
269
+ function spawnWorldItem(input) {
270
+ const record = worldItems.spawn(input);
271
+ events.emit("worldItem.dropped", {
272
+ instanceId: record.instanceId,
273
+ itemId: record.itemId,
274
+ rarity: record.rarity,
275
+ count: record.count,
276
+ position: [input.position[0], input.position[1], input.position[2]],
277
+ ...(record.source !== undefined ? { source: record.source } : {}),
278
+ });
279
+ return record;
280
+ }
281
+ function pickupWorldItem(instanceId, userId) {
282
+ const record = worldItems.take(instanceId);
283
+ if (record === null)
284
+ return { status: "rejected", reason: "not-found" };
285
+ loot.grantToPlayer(userId, [{ item: record.itemId, count: record.count }], "worldItem.pickup");
286
+ events.emit("worldItem.picked_up", {
287
+ instanceId,
288
+ userId,
289
+ itemId: record.itemId,
290
+ rarity: record.rarity,
291
+ count: record.count,
292
+ });
293
+ return { status: "ok", record };
294
+ }
243
295
  const death = createDeathSystem({
244
296
  resolveOnDeath: (instanceId) => catalogEntry(instanceId)?.onDeath,
245
297
  resolveIdentity(instanceId) {
@@ -266,7 +318,10 @@ export function createGameContext(options) {
266
318
  getStat: weapon.getStat,
267
319
  spatial: combatSpatial,
268
320
  onLethal(instanceId, lethalCtx) {
269
- const catalogId = entities.get(instanceId)?.name;
321
+ const dyingEntity = entities.get(instanceId);
322
+ const catalogId = dyingEntity?.name;
323
+ const position = dyingEntity?.position;
324
+ const normalizedOnDeath = normalizeOnDeath(catalogEntry(instanceId)?.onDeath);
270
325
  const reason = deathReasonFromEffect({
271
326
  ...lethalCtx,
272
327
  userIdOf: (id) => (id === player.userId ? player.userId : undefined),
@@ -276,7 +331,23 @@ export function createGameContext(options) {
276
331
  resolution.drops.length > 0 &&
277
332
  reason.kind === "player_kill" &&
278
333
  reason.killerUserId === player.userId) {
279
- loot.grantToPlayer(player.userId, resolution.drops, catalogId);
334
+ if (normalizedOnDeath.dropMode === "world" && position !== undefined) {
335
+ const resolved = resolveDeathDrops(resolution.drops, {
336
+ mode: "world",
337
+ origin: position,
338
+ resolveRarity: (itemId) => content.itemById?.(itemId)?.rarity ?? DEFAULT_RARITY,
339
+ resolveBaseType: (itemId) => content.itemById?.(itemId)?.baseType ?? itemId,
340
+ scatter: normalizedOnDeath.scatter,
341
+ ...(catalogId !== undefined ? { source: catalogId } : {}),
342
+ });
343
+ for (const spawn of resolved.worldSpawns)
344
+ spawnWorldItem(spawn);
345
+ if (resolved.grants.length > 0)
346
+ loot.grantToPlayer(player.userId, resolved.grants, catalogId);
347
+ }
348
+ else {
349
+ loot.grantToPlayer(player.userId, resolution.drops, catalogId);
350
+ }
280
351
  }
281
352
  },
282
353
  }), ["applyEffect"], signal.notify);
@@ -295,8 +366,76 @@ export function createGameContext(options) {
295
366
  event.instanceId = input.instanceId;
296
367
  if (input.amount !== undefined)
297
368
  event.amount = input.amount;
369
+ if (input.hitType !== undefined)
370
+ event.hitType = input.hitType;
371
+ if (input.element !== undefined)
372
+ event.element = input.element;
373
+ if (input.crit !== undefined)
374
+ event.crit = input.crit;
375
+ if (input.scale !== undefined)
376
+ event.scale = input.scale;
298
377
  events.emit("entity.floatText", event);
299
378
  }
379
+ let telegraphSeq = 0;
380
+ function fireTelegraph(input) {
381
+ const id = telegraphSeq++;
382
+ const telegraphEvent = {
383
+ id,
384
+ shape: input.shape,
385
+ position: [input.at[0], input.at[1], input.at[2]],
386
+ windupMs: input.windupMs,
387
+ kind: input.kind ?? "danger",
388
+ };
389
+ if (input.dir !== undefined)
390
+ telegraphEvent.dir = input.dir;
391
+ events.emit("combat.telegraph", telegraphEvent);
392
+ const bound = input.effect;
393
+ if (bound === undefined)
394
+ return () => { };
395
+ const config = { shape: input.shape, at: input.at, windupMs: input.windupMs };
396
+ if (input.dir !== undefined)
397
+ config.dir = input.dir;
398
+ return time.after(input.windupMs / 1000, () => {
399
+ const targets = entities.list().filter((entity) => pointInTelegraph(config, entity.position));
400
+ for (const target of targets) {
401
+ applyEffectAndFloat({
402
+ from: input.from,
403
+ to: target.id,
404
+ effect: bound.effect,
405
+ ...(bound.via === undefined ? {} : { via: bound.via }),
406
+ });
407
+ }
408
+ });
409
+ }
410
+ function applyHitReaction(input) {
411
+ const attacker = entities.get(input.from);
412
+ const target = entities.get(input.to);
413
+ if (target === null)
414
+ return null;
415
+ const attackerPos = attacker?.position ?? target.position;
416
+ const reaction = resolveHitReaction(input.config, {
417
+ attackerPos,
418
+ targetPos: target.position,
419
+ ...(input.power === undefined ? {} : { power: input.power }),
420
+ });
421
+ entities.setPose(input.to, {
422
+ position: [
423
+ target.position[0] + reaction.impulse[0],
424
+ target.position[1] + reaction.impulse[1],
425
+ target.position[2] + reaction.impulse[2],
426
+ ],
427
+ rotationY: target.rotationY,
428
+ });
429
+ const reactionEvent = {
430
+ instanceId: input.to,
431
+ position: [target.position[0], target.position[1], target.position[2]],
432
+ hitstopMs: reaction.hitstopMs,
433
+ };
434
+ if (reaction.shake !== null)
435
+ reactionEvent.shake = reaction.shake;
436
+ events.emit("combat.hitReaction", reactionEvent);
437
+ return reaction;
438
+ }
300
439
  function applyEffectAndFloat(input) {
301
440
  const positionsBefore = new Map();
302
441
  for (const entity of entities.list())
@@ -357,6 +496,8 @@ export function createGameContext(options) {
357
496
  list: entities.list,
358
497
  stats: entityStats,
359
498
  floatText: emitFloatText,
499
+ telegraph: fireTelegraph,
500
+ hitReaction: applyHitReaction,
360
501
  setTarget: targeting.setTarget,
361
502
  getTarget: targeting.getTarget,
362
503
  cycleTarget: targeting.cycleTarget,
@@ -371,6 +512,14 @@ export function createGameContext(options) {
371
512
  hasLineOfSight: spatial.hasLineOfSight,
372
513
  queryArc: spatial.queryArc,
373
514
  moveToward: spatial.moveToward,
515
+ form: forms,
516
+ },
517
+ worldItem: {
518
+ spawn: spawnWorldItem,
519
+ get: worldItems.get,
520
+ list: worldItems.list,
521
+ nearestInRadius: worldItems.nearestInRadius,
522
+ pickup: pickupWorldItem,
374
523
  },
375
524
  },
376
525
  game: {
@@ -406,6 +555,7 @@ export function createGameContext(options) {
406
555
  unlocks,
407
556
  economy,
408
557
  leaderboard,
558
+ roster,
409
559
  },
410
560
  player: {
411
561
  userId: player.userId,
@@ -415,6 +565,8 @@ export function createGameContext(options) {
415
565
  loadout: loadouts,
416
566
  applyLoadout: loadouts.applyLoadout,
417
567
  movement: pose,
568
+ possession,
569
+ cosmetics,
418
570
  },
419
571
  item: {
420
572
  use: {
@@ -429,6 +581,7 @@ export function createGameContext(options) {
429
581
  },
430
582
  weapon,
431
583
  },
584
+ time,
432
585
  subscribe: signal.subscribe,
433
586
  version: signal.version,
434
587
  };
@@ -1,13 +1,26 @@
1
1
  import type { LeaderboardScope } from "../game/leaderboard.js";
2
+ import type { NormalizedScenarioReset } from "./persistenceScope.js";
2
3
  import type { SaveConfig } from "./save.js";
3
4
  import type { GameRuntimeSnapshot, RuntimeChunkRow, RuntimePlayerRow, RuntimeServerRow } from "./snapshot.js";
4
5
  export type GameServerStatus = "open" | "running" | "closed";
6
+ export type SessionVisibility = "public" | "private";
7
+ export type SessionAttributes = {
8
+ label?: string;
9
+ mode?: string;
10
+ visibility?: SessionVisibility;
11
+ joinCode?: string;
12
+ tags?: string[];
13
+ };
5
14
  export type GameServerRecord = {
6
15
  serverId: string;
7
16
  gameId: string;
8
17
  status: GameServerStatus;
9
18
  mode?: string;
10
19
  modeConfig?: unknown;
20
+ label?: string;
21
+ visibility?: SessionVisibility;
22
+ joinCode?: string;
23
+ tags?: string[];
11
24
  memberUserIds: string[];
12
25
  slotsPerServer: number;
13
26
  save: SaveConfig;
@@ -58,11 +71,16 @@ export type ServerListing = {
58
71
  memberCount: number;
59
72
  slotsPerServer: number;
60
73
  mode?: string;
74
+ label?: string;
75
+ visibility?: SessionVisibility;
76
+ joinCode?: string;
77
+ tags?: string[];
61
78
  updatedAt: number;
62
79
  };
63
80
  export declare function toServerListing(record: GameServerRecord): ServerListing;
64
81
  export type HostPersistence = {
65
82
  savePlan?: (plan: ServerPersistPlan) => Promise<void>;
83
+ resetScenario?: (reset: NormalizedScenarioReset) => Promise<void>;
66
84
  loadServer: (serverId: string) => Promise<GameServerRecord | null>;
67
85
  saveServer: (record: GameServerRecord) => Promise<void>;
68
86
  listServers: (gameId: string) => Promise<GameServerRecord[]>;
@@ -42,6 +42,10 @@ export function toServerListing(record) {
42
42
  memberCount: record.memberUserIds.length,
43
43
  slotsPerServer: record.slotsPerServer,
44
44
  mode: record.mode,
45
+ label: record.label,
46
+ visibility: record.visibility,
47
+ joinCode: record.joinCode,
48
+ tags: record.tags,
45
49
  updatedAt: record.updatedAt,
46
50
  };
47
51
  }
@@ -0,0 +1,32 @@
1
+ import type { PlayerProfileRecord } from "./hostPersistence.js";
2
+ import type { RuntimePlayerRow } from "./snapshot.js";
3
+ export type PersistenceScope = "run" | "meta";
4
+ export interface ScopeSchema {
5
+ run: readonly string[];
6
+ }
7
+ export interface ScopedState<T = Record<string, unknown>> {
8
+ meta: Partial<T>;
9
+ run: Partial<T>;
10
+ }
11
+ export declare function partitionScopes<T extends Record<string, unknown>>(state: T, schema: ScopeSchema): ScopedState<T>;
12
+ export declare function resetRun<T extends Record<string, unknown>>(scoped: ScopedState<T>): ScopedState<T>;
13
+ export declare function mergeScopes<T extends Record<string, unknown>>(scoped: ScopedState<T>): Partial<T>;
14
+ export declare function clearRunFields(player: RuntimePlayerRow, runFields: readonly string[]): RuntimePlayerRow;
15
+ export declare function applyRunReset(profile: PlayerProfileRecord, runFields: readonly string[], now: number): PlayerProfileRecord;
16
+ export interface ScenarioReset {
17
+ gameId: string;
18
+ serverId?: string;
19
+ wipeChunks?: boolean;
20
+ wipeServerSession?: boolean;
21
+ resetPlayers?: PersistenceScope | "none";
22
+ runFields?: readonly string[];
23
+ }
24
+ export interface NormalizedScenarioReset {
25
+ gameId: string;
26
+ serverId: string | null;
27
+ wipeChunks: boolean;
28
+ wipeServerSession: boolean;
29
+ resetPlayers: "run" | "none";
30
+ runFields: readonly string[];
31
+ }
32
+ export declare function planScenarioReset(reset: ScenarioReset): NormalizedScenarioReset;
@@ -0,0 +1,64 @@
1
+ export function partitionScopes(state, schema) {
2
+ const runKeys = new Set(schema.run);
3
+ const meta = {};
4
+ const run = {};
5
+ for (const key of Object.keys(state)) {
6
+ if (runKeys.has(key))
7
+ run[key] = state[key];
8
+ else
9
+ meta[key] = state[key];
10
+ }
11
+ return { meta, run };
12
+ }
13
+ export function resetRun(scoped) {
14
+ return { meta: scoped.meta, run: {} };
15
+ }
16
+ export function mergeScopes(scoped) {
17
+ return { ...scoped.meta, ...scoped.run };
18
+ }
19
+ const RUN_FIELD_DEFAULTS = {
20
+ inventories: {},
21
+ economy: {},
22
+ unlocks: [],
23
+ quests: undefined,
24
+ social: undefined,
25
+ leaderboard: {},
26
+ session: {},
27
+ };
28
+ export function clearRunFields(player, runFields) {
29
+ const next = { ...player };
30
+ for (const field of runFields) {
31
+ if (field === "userId")
32
+ continue;
33
+ const key = field;
34
+ if (key in RUN_FIELD_DEFAULTS) {
35
+ const template = RUN_FIELD_DEFAULTS[key];
36
+ if (template === undefined)
37
+ delete next[field];
38
+ else
39
+ next[field] = Array.isArray(template) ? [...template] : { ...template };
40
+ }
41
+ else {
42
+ delete next[field];
43
+ }
44
+ }
45
+ return next;
46
+ }
47
+ export function applyRunReset(profile, runFields, now) {
48
+ return {
49
+ ...profile,
50
+ playerState: clearRunFields(profile.playerState, runFields),
51
+ revision: profile.revision + 1,
52
+ updatedAt: now,
53
+ };
54
+ }
55
+ export function planScenarioReset(reset) {
56
+ return {
57
+ gameId: reset.gameId,
58
+ serverId: reset.serverId ?? null,
59
+ wipeChunks: reset.wipeChunks ?? true,
60
+ wipeServerSession: reset.wipeServerSession ?? true,
61
+ resetPlayers: reset.resetPlayers === "run" ? "run" : "none",
62
+ runFields: reset.runFields ?? [],
63
+ };
64
+ }
@@ -1,5 +1,18 @@
1
+ export interface ModelDims {
2
+ footprint: {
3
+ w: number;
4
+ d: number;
5
+ };
6
+ center: {
7
+ x: number;
8
+ z: number;
9
+ };
10
+ minY: number;
11
+ }
1
12
  export interface ModelAssetRef {
2
13
  url: string;
14
+ /** Measured at asset reindex: horizontal footprint, footprint center, and lowest Y in model space (pre-scale). Lets the shell auto-center and ground-snap corner-pivot kit models. */
15
+ dims?: ModelDims;
3
16
  }
4
17
  export interface AssetCatalog<TMeta extends ModelAssetRef = ModelAssetRef> {
5
18
  register(id: string, asset: TMeta): void;
@@ -0,0 +1,15 @@
1
+ export type AutoTargetPolicy = "nearest" | "farthest" | "random" | "strongest" | "weakest" | "first" | "last";
2
+ export interface AutoTargetDeps {
3
+ candidates: (fromId: string) => readonly string[];
4
+ distance: (fromId: string, toId: string) => number | null;
5
+ strength?: (toId: string) => number;
6
+ progress?: (toId: string) => number;
7
+ rng?: () => number;
8
+ }
9
+ export declare function selectAutoTarget(policy: AutoTargetPolicy, fromId: string, deps: AutoTargetDeps): string | null;
10
+ export interface AutoTargeter {
11
+ policy(): AutoTargetPolicy;
12
+ setPolicy(policy: AutoTargetPolicy): void;
13
+ pick(fromId: string): string | null;
14
+ }
15
+ export declare function createAutoTargeter(policy: AutoTargetPolicy, deps: AutoTargetDeps): AutoTargeter;
@@ -0,0 +1,54 @@
1
+ function metric(policy, fromId, toId, deps) {
2
+ switch (policy) {
3
+ case "nearest":
4
+ case "farthest":
5
+ return deps.distance(fromId, toId);
6
+ case "strongest":
7
+ case "weakest":
8
+ return deps.strength?.(toId) ?? 0;
9
+ case "first":
10
+ case "last":
11
+ return deps.progress?.(toId) ?? 0;
12
+ case "random":
13
+ return 0;
14
+ }
15
+ }
16
+ function prefersHigher(policy) {
17
+ return policy === "farthest" || policy === "strongest" || policy === "first";
18
+ }
19
+ export function selectAutoTarget(policy, fromId, deps) {
20
+ const candidates = deps.candidates(fromId).filter((id) => id !== fromId);
21
+ if (candidates.length === 0)
22
+ return null;
23
+ if (policy === "random") {
24
+ const rng = deps.rng ?? Math.random;
25
+ return candidates[Math.min(candidates.length - 1, Math.floor(rng() * candidates.length))];
26
+ }
27
+ const higher = prefersHigher(policy);
28
+ let best = null;
29
+ let bestScore = higher ? Number.NEGATIVE_INFINITY : Number.POSITIVE_INFINITY;
30
+ for (const id of candidates) {
31
+ const score = metric(policy, fromId, id, deps);
32
+ if (score === null)
33
+ continue;
34
+ if (best === null || (higher ? score > bestScore : score < bestScore)) {
35
+ best = id;
36
+ bestScore = score;
37
+ }
38
+ }
39
+ return best;
40
+ }
41
+ export function createAutoTargeter(policy, deps) {
42
+ let current = policy;
43
+ return {
44
+ policy() {
45
+ return current;
46
+ },
47
+ setPolicy(next) {
48
+ current = next;
49
+ },
50
+ pick(fromId) {
51
+ return selectAutoTarget(current, fromId, deps);
52
+ },
53
+ };
54
+ }
@@ -1,8 +1,15 @@
1
1
  import { type ProximityPrompt } from "../interaction/proximityPrompt.js";
2
+ import type { Waypoint } from "../nav/pathFollow.js";
2
3
  export interface WanderBehavior {
3
4
  kind: "wander";
4
5
  radius: number;
5
6
  }
7
+ export interface PatrolBehavior {
8
+ kind: "patrol";
9
+ waypoints: readonly Waypoint[];
10
+ speed: number;
11
+ loop: boolean;
12
+ }
6
13
  export interface PromptableBehavior {
7
14
  kind: "promptable";
8
15
  prompt: ProximityPrompt;
@@ -10,10 +17,15 @@ export interface PromptableBehavior {
10
17
  export interface PlayerBehavior {
11
18
  kind: "player";
12
19
  }
13
- export type BehaviorDescriptor = WanderBehavior | PromptableBehavior | PlayerBehavior;
20
+ export type BehaviorDescriptor = WanderBehavior | PatrolBehavior | PromptableBehavior | PlayerBehavior;
14
21
  export declare function wander({ radius }: {
15
22
  radius: number;
16
23
  }): WanderBehavior;
24
+ export declare function patrol({ waypoints, speed, loop, }: {
25
+ waypoints: readonly Waypoint[];
26
+ speed: number;
27
+ loop?: boolean;
28
+ }): PatrolBehavior;
17
29
  export declare function promptable(prompt: ProximityPrompt): PromptableBehavior;
18
30
  export declare function talkable(dialogueId: string): PromptableBehavior;
19
31
  export declare function player(): PlayerBehavior;
@@ -2,6 +2,9 @@ import { command, keybind, proximityPrompt, } from "../interaction/proximityProm
2
2
  export function wander({ radius }) {
3
3
  return { kind: "wander", radius };
4
4
  }
5
+ export function patrol({ waypoints, speed, loop = true, }) {
6
+ return { kind: "patrol", waypoints, speed, loop };
7
+ }
5
8
  export function promptable(prompt) {
6
9
  return { kind: "promptable", prompt };
7
10
  }
@@ -0,0 +1,7 @@
1
+ export interface CaptureCheckInput {
2
+ hpFraction: number;
3
+ catchPower: number;
4
+ difficulty?: number;
5
+ }
6
+ export declare function captureChance(input: CaptureCheckInput): number;
7
+ export declare function rollCapture(input: CaptureCheckInput, rng?: () => number): boolean;
@@ -0,0 +1,11 @@
1
+ function clamp01(value) {
2
+ return Math.min(1, Math.max(0, value));
3
+ }
4
+ export function captureChance(input) {
5
+ const difficulty = input.difficulty ?? 1;
6
+ const hpFactor = 1 - clamp01(input.hpFraction) * 0.6;
7
+ return clamp01((input.catchPower * hpFactor) / Math.max(difficulty, 0.0001));
8
+ }
9
+ export function rollCapture(input, rng = Math.random) {
10
+ return rng() < captureChance(input);
11
+ }
@@ -16,11 +16,15 @@ export interface SceneEntity<TMeta = undefined> {
16
16
  rotationY: number;
17
17
  rotationX: number;
18
18
  rotationZ: number;
19
+ /** World units per second, derived from consecutive setPose calls that carry `dt`. Zero until the entity moves under a dt. */
20
+ velocity: EntityPosition;
19
21
  role: EntityRole;
20
22
  movement: EntityMovement;
21
23
  behaviors: readonly BehaviorDescriptor[];
22
24
  meta: TMeta;
23
25
  }
26
+ /** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
27
+ export declare function groundSpeed(entity: SceneEntity<unknown>): number;
24
28
  export interface SpawnOptions<TMeta = undefined> {
25
29
  id?: string;
26
30
  position?: SpawnPositionInput;
@@ -37,11 +41,13 @@ export interface EntityPose {
37
41
  rotationY?: number;
38
42
  rotationX?: number;
39
43
  rotationZ?: number;
44
+ /** Seconds since the previous pose; when > 0 the store derives `velocity` from the position delta. Omit for teleports (velocity unchanged). */
45
+ dt?: number;
40
46
  }
41
47
  export interface EntityStore<TMeta = undefined> {
42
48
  spawn(name: string, options?: SpawnOptions<TMeta>): string;
43
49
  despawn(id: string): boolean;
44
- update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
50
+ update(id: string, patch: Partial<Pick<SceneEntity<TMeta>, "name" | "position" | "rotationY" | "rotationX" | "rotationZ" | "role" | "movement" | "behaviors" | "meta">>): boolean;
45
51
  setPose(id: string, pose: EntityPose): boolean;
46
52
  get(id: string): SceneEntity<TMeta> | null;
47
53
  list(): readonly SceneEntity<TMeta>[];
@@ -1,4 +1,8 @@
1
1
  import { createObservableKeyedStore } from "../store/observableKeyedStore.js";
2
+ /** Ground speed (horizontal magnitude of velocity) in world units per second. Scale to km/h or mph in game code. */
3
+ export function groundSpeed(entity) {
4
+ return Math.hypot(entity.velocity[0], entity.velocity[2]);
5
+ }
2
6
  function toEntityPosition(position) {
3
7
  if (position === undefined)
4
8
  return [0, 0, 0];
@@ -31,6 +35,7 @@ export function createEntityStore() {
31
35
  rotationY: options.rotationY ?? 0,
32
36
  rotationX: options.rotationX ?? 0,
33
37
  rotationZ: options.rotationZ ?? 0,
38
+ velocity: [0, 0, 0],
34
39
  role: options.role ?? "prop",
35
40
  movement: options.movement ?? {},
36
41
  behaviors: options.behaviors ?? [],
@@ -54,9 +59,18 @@ export function createEntityStore() {
54
59
  const current = store.get(id);
55
60
  if (!current)
56
61
  return false;
62
+ const position = toEntityPosition(pose.position);
63
+ const velocity = pose.dt !== undefined && pose.dt > 0
64
+ ? [
65
+ (position[0] - current.position[0]) / pose.dt,
66
+ (position[1] - current.position[1]) / pose.dt,
67
+ (position[2] - current.position[2]) / pose.dt,
68
+ ]
69
+ : current.velocity;
57
70
  store.set(id, {
58
71
  ...current,
59
- position: toEntityPosition(pose.position),
72
+ position,
73
+ velocity,
60
74
  rotationY: pose.rotationY ?? current.rotationY,
61
75
  rotationX: pose.rotationX ?? current.rotationX,
62
76
  rotationZ: pose.rotationZ ?? current.rotationZ,