@jgengine/core 0.8.0 → 0.10.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 (303) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/ai/flock.d.ts +36 -0
  3. package/dist/ai/flock.js +182 -0
  4. package/dist/ai/groupAssist.d.ts +19 -0
  5. package/dist/ai/groupAssist.js +59 -0
  6. package/dist/ai/laneSelect.d.ts +21 -0
  7. package/dist/ai/laneSelect.js +37 -0
  8. package/dist/ai/mobBrain.d.ts +68 -0
  9. package/dist/ai/mobBrain.js +115 -0
  10. package/dist/ai/spawnDirector.d.ts +13 -0
  11. package/dist/ai/spawnDirector.js +41 -17
  12. package/dist/ai/spawnPoint.d.ts +5 -0
  13. package/dist/ai/spawnPoint.js +5 -0
  14. package/dist/ai/threat.d.ts +2 -0
  15. package/dist/ai/threat.js +34 -0
  16. package/dist/audio/audioFalloff.d.ts +5 -2
  17. package/dist/audio/music.d.ts +53 -0
  18. package/dist/audio/music.js +34 -0
  19. package/dist/audio/synth.d.ts +60 -0
  20. package/dist/audio/synth.js +8 -0
  21. package/dist/authoring.d.ts +14 -0
  22. package/dist/authoring.js +14 -0
  23. package/dist/cartridge/runtime.d.ts +15 -0
  24. package/dist/cartridge/runtime.js +556 -0
  25. package/dist/cartridge/spec.d.ts +188 -0
  26. package/dist/cartridge/spec.js +19 -0
  27. package/dist/cartridge/validate.d.ts +2 -0
  28. package/dist/cartridge/validate.js +78 -0
  29. package/dist/combat/abilityKit.d.ts +17 -2
  30. package/dist/combat/abilityKit.js +62 -13
  31. package/dist/combat/castRunner.d.ts +40 -0
  32. package/dist/combat/castRunner.js +56 -0
  33. package/dist/combat/comboPoints.d.ts +14 -0
  34. package/dist/combat/comboPoints.js +52 -0
  35. package/dist/combat/defensiveWindow.d.ts +1 -1
  36. package/dist/combat/defensiveWindow.js +5 -3
  37. package/dist/combat/dotField.d.ts +43 -0
  38. package/dist/combat/dotField.js +48 -0
  39. package/dist/combat/projectiles.d.ts +33 -12
  40. package/dist/combat/projectiles.js +214 -129
  41. package/dist/combat/regenShield.d.ts +35 -0
  42. package/dist/combat/regenShield.js +45 -0
  43. package/dist/combat/resistance.d.ts +16 -4
  44. package/dist/combat/resistance.js +43 -7
  45. package/dist/combat/resourcePool.d.ts +20 -0
  46. package/dist/combat/resourcePool.js +55 -0
  47. package/dist/combat/shotOrigin.d.ts +52 -0
  48. package/dist/combat/shotOrigin.js +86 -0
  49. package/dist/combat/telegraph.d.ts +25 -0
  50. package/dist/combat/telegraph.js +47 -0
  51. package/dist/crafting/recipe.js +4 -1
  52. package/dist/devtools/devtools.d.ts +72 -24
  53. package/dist/devtools/devtools.js +449 -101
  54. package/dist/devtools/transformTunables.js +3 -3
  55. package/dist/devtools/tunableSchema.d.ts +140 -0
  56. package/dist/devtools/tunableSchema.js +476 -0
  57. package/dist/editor/commands.d.ts +65 -0
  58. package/dist/editor/commands.js +193 -0
  59. package/dist/editor/document.d.ts +40 -0
  60. package/dist/editor/document.js +153 -0
  61. package/dist/editor/index.d.ts +30 -0
  62. package/dist/editor/index.js +6 -0
  63. package/dist/editor/types.d.ts +69 -0
  64. package/dist/editor/types.js +29 -0
  65. package/dist/faction/factions.d.ts +33 -0
  66. package/dist/faction/factions.js +76 -0
  67. package/dist/faction/reputation.d.ts +32 -0
  68. package/dist/faction/reputation.js +82 -0
  69. package/dist/game/chat.d.ts +2 -0
  70. package/dist/game/chat.js +9 -1
  71. package/dist/game/chatFilter.d.ts +15 -0
  72. package/dist/game/chatFilter.js +55 -0
  73. package/dist/game/connectedPlayers.d.ts +28 -0
  74. package/dist/game/connectedPlayers.js +21 -0
  75. package/dist/game/controlGate.d.ts +4 -0
  76. package/dist/game/controlGate.js +7 -0
  77. package/dist/game/defineGame.d.ts +44 -1
  78. package/dist/game/defineGame.js +1 -0
  79. package/dist/game/events.d.ts +25 -0
  80. package/dist/game/gamePhase.d.ts +20 -0
  81. package/dist/game/gamePhase.js +21 -0
  82. package/dist/game/keyValueStore.d.ts +25 -0
  83. package/dist/game/keyValueStore.js +61 -0
  84. package/dist/game/levelSequence.d.ts +24 -2
  85. package/dist/game/levelSequence.js +51 -2
  86. package/dist/game/lootTable.d.ts +14 -1
  87. package/dist/game/lootTable.js +28 -7
  88. package/dist/game/modelAnimation.d.ts +8 -0
  89. package/dist/game/modelAnimation.js +18 -0
  90. package/dist/game/playableGame.d.ts +116 -4
  91. package/dist/game/playableGame.js +1 -1
  92. package/dist/game/quest.d.ts +7 -3
  93. package/dist/game/quest.js +17 -8
  94. package/dist/game/race.d.ts +39 -1
  95. package/dist/game/race.js +96 -11
  96. package/dist/game/recordBook.d.ts +27 -0
  97. package/dist/game/recordBook.js +86 -0
  98. package/dist/game/talents.d.ts +47 -0
  99. package/dist/game/talents.js +140 -0
  100. package/dist/index.d.ts +8 -0
  101. package/dist/index.js +8 -0
  102. package/dist/input/actionBindings.d.ts +2 -0
  103. package/dist/input/actionBindings.js +1 -0
  104. package/dist/input/axisInput.d.ts +33 -3
  105. package/dist/input/axisInput.js +60 -4
  106. package/dist/input/bindingOverrides.d.ts +25 -0
  107. package/dist/input/bindingOverrides.js +95 -0
  108. package/dist/input/pointerAxis.d.ts +30 -0
  109. package/dist/input/pointerAxis.js +31 -0
  110. package/dist/input/touchScheme.d.ts +6 -0
  111. package/dist/input/touchScheme.js +45 -7
  112. package/dist/interaction/contextMenu.d.ts +1 -0
  113. package/dist/interaction/contextMenu.js +1 -0
  114. package/dist/interaction/proximityPrompt.d.ts +3 -1
  115. package/dist/interaction/proximityPrompt.js +3 -3
  116. package/dist/meta/changelog.d.ts +1 -1
  117. package/dist/meta/changelog.js +44 -1
  118. package/dist/movement/dash.d.ts +3 -0
  119. package/dist/movement/dash.js +19 -5
  120. package/dist/movement/glideModel.d.ts +44 -0
  121. package/dist/movement/glideModel.js +54 -0
  122. package/dist/movement/grappleSwing.d.ts +34 -0
  123. package/dist/movement/grappleSwing.js +56 -0
  124. package/dist/movement/leaderTrail.d.ts +29 -0
  125. package/dist/movement/leaderTrail.js +67 -0
  126. package/dist/movement/movementModel.d.ts +4 -0
  127. package/dist/movement/movementModel.js +13 -0
  128. package/dist/movement/playerMovement.d.ts +41 -0
  129. package/dist/movement/playerMovement.js +181 -0
  130. package/dist/movement/steering.d.ts +27 -0
  131. package/dist/movement/steering.js +40 -0
  132. package/dist/multiplayer/feedWriteGate.d.ts +10 -0
  133. package/dist/multiplayer/feedWriteGate.js +12 -0
  134. package/dist/multiplayer/presenceModel.d.ts +14 -2
  135. package/dist/multiplayer/presenceModel.js +15 -3
  136. package/dist/nav/corridors.d.ts +30 -0
  137. package/dist/nav/corridors.js +69 -0
  138. package/dist/nav/navGrid.js +30 -4
  139. package/dist/nav/railGraph.d.ts +66 -0
  140. package/dist/nav/railGraph.js +129 -0
  141. package/dist/nav/timetable.d.ts +41 -0
  142. package/dist/nav/timetable.js +94 -0
  143. package/dist/physics/buoyancy.d.ts +6 -0
  144. package/dist/physics/buoyancy.js +14 -0
  145. package/dist/physics/flowTube.d.ts +32 -0
  146. package/dist/physics/flowTube.js +64 -0
  147. package/dist/physics/forceVolume.d.ts +27 -0
  148. package/dist/physics/forceVolume.js +38 -0
  149. package/dist/physics/kinematicVehicle.d.ts +52 -0
  150. package/dist/physics/kinematicVehicle.js +81 -0
  151. package/dist/physics/physicsWorld.d.ts +13 -6
  152. package/dist/physics/physicsWorld.js +169 -40
  153. package/dist/physics/ragdoll.js +1 -1
  154. package/dist/physics/vehicleBody.js +2 -1
  155. package/dist/random/rng.d.ts +2 -0
  156. package/dist/random/rng.js +2 -0
  157. package/dist/random/seedLink.d.ts +5 -0
  158. package/dist/random/seedLink.js +35 -0
  159. package/dist/render/postProcessing.d.ts +56 -0
  160. package/dist/render/postProcessing.js +1 -0
  161. package/dist/runtime/adapter.d.ts +19 -0
  162. package/dist/runtime/adapter.js +11 -5
  163. package/dist/runtime/cameraDirector.d.ts +6 -1
  164. package/dist/runtime/cameraDirector.js +6 -0
  165. package/dist/runtime/commandRunner.d.ts +2 -2
  166. package/dist/runtime/commandRunner.js +2 -2
  167. package/dist/runtime/gameContext.d.ts +72 -8
  168. package/dist/runtime/gameContext.js +374 -61
  169. package/dist/runtime/gameRuntime.d.ts +5 -1
  170. package/dist/runtime/gameRuntime.js +13 -12
  171. package/dist/runtime/hostPersistence.d.ts +9 -1
  172. package/dist/runtime/hostPersistence.js +23 -8
  173. package/dist/runtime/hostedGameRunner.d.ts +49 -0
  174. package/dist/runtime/hostedGameRunner.js +62 -0
  175. package/dist/runtime/hostedWorldSession.d.ts +65 -0
  176. package/dist/runtime/hostedWorldSession.js +57 -0
  177. package/dist/runtime/inputSnapshot.d.ts +18 -0
  178. package/dist/runtime/inputSnapshot.js +7 -0
  179. package/dist/runtime/motionIntents.d.ts +16 -6
  180. package/dist/runtime/motionIntents.js +31 -2
  181. package/dist/runtime/snapshot.d.ts +1 -0
  182. package/dist/runtime/snapshot.js +1 -1
  183. package/dist/runtime/visibility.d.ts +74 -0
  184. package/dist/runtime/visibility.js +127 -0
  185. package/dist/runtime/worldChannel.d.ts +58 -0
  186. package/dist/runtime/worldChannel.js +69 -0
  187. package/dist/runtime/worldMirror.d.ts +24 -0
  188. package/dist/runtime/worldMirror.js +31 -0
  189. package/dist/runtime/worldReplication.d.ts +46 -0
  190. package/dist/runtime/worldReplication.js +185 -0
  191. package/dist/runtime/worldSnapshot.d.ts +17 -0
  192. package/dist/runtime/worldSnapshot.js +15 -0
  193. package/dist/scene/assetCatalog.d.ts +1 -0
  194. package/dist/scene/colliders.d.ts +50 -0
  195. package/dist/scene/colliders.js +101 -0
  196. package/dist/scene/entityStats.d.ts +4 -0
  197. package/dist/scene/entityStats.js +21 -0
  198. package/dist/scene/entityStore.d.ts +51 -5
  199. package/dist/scene/entityStore.js +119 -12
  200. package/dist/scene/objectQuery.d.ts +13 -2
  201. package/dist/scene/objectQuery.js +34 -2
  202. package/dist/scene/objectStore.d.ts +2 -0
  203. package/dist/scene/objectStore.js +31 -12
  204. package/dist/scene/sceneRaycast.d.ts +80 -0
  205. package/dist/scene/sceneRaycast.js +284 -0
  206. package/dist/scene/spatial.d.ts +11 -16
  207. package/dist/scene/spatial.js +99 -38
  208. package/dist/sensor/recordingBuffer.d.ts +6 -0
  209. package/dist/sensor/recordingBuffer.js +7 -0
  210. package/dist/sensor/replayLoop.d.ts +39 -0
  211. package/dist/sensor/replayLoop.js +64 -0
  212. package/dist/sensor/visionCone.d.ts +31 -0
  213. package/dist/sensor/visionCone.js +72 -0
  214. package/dist/session/extraction.d.ts +2 -2
  215. package/dist/session/extraction.js +13 -2
  216. package/dist/settings/settingsModel.d.ts +97 -0
  217. package/dist/settings/settingsModel.js +100 -0
  218. package/dist/stats/statModifiers.d.ts +5 -2
  219. package/dist/stats/statModifiers.js +32 -14
  220. package/dist/store/observableKeyedStore.d.ts +4 -0
  221. package/dist/store/observableKeyedStore.js +17 -2
  222. package/dist/survival/decayMeter.d.ts +1 -1
  223. package/dist/survival/decayMeter.js +0 -1
  224. package/dist/tactics/fallingGrid.d.ts +2 -2
  225. package/dist/tactics/fallingGrid.js +2 -2
  226. package/dist/time/beatClock.d.ts +18 -0
  227. package/dist/time/beatClock.js +24 -0
  228. package/dist/time/idleProgress.d.ts +29 -0
  229. package/dist/time/idleProgress.js +31 -0
  230. package/dist/time/serverTick.d.ts +7 -1
  231. package/dist/time/serverTick.js +27 -4
  232. package/dist/time/stateSchedule.d.ts +57 -0
  233. package/dist/time/stateSchedule.js +121 -0
  234. package/dist/turn/commit.d.ts +2 -2
  235. package/dist/turn/commit.js +2 -2
  236. package/dist/ui/gameLayout.d.ts +125 -0
  237. package/dist/ui/gameLayout.js +129 -0
  238. package/dist/ui/hudLayout.d.ts +58 -0
  239. package/dist/ui/hudLayout.js +207 -0
  240. package/dist/ui/hudScale.d.ts +44 -0
  241. package/dist/ui/hudScale.js +55 -0
  242. package/dist/ui/orientation.d.ts +39 -0
  243. package/dist/ui/orientation.js +49 -0
  244. package/dist/ui/swingTimer.d.ts +42 -0
  245. package/dist/ui/swingTimer.js +38 -0
  246. package/dist/visibility/assetStreaming.d.ts +66 -0
  247. package/dist/visibility/assetStreaming.js +201 -0
  248. package/dist/visibility/bounds.d.ts +66 -0
  249. package/dist/visibility/bounds.js +113 -0
  250. package/dist/visibility/camera.d.ts +21 -0
  251. package/dist/visibility/camera.js +3 -0
  252. package/dist/visibility/config.d.ts +21 -0
  253. package/dist/visibility/config.js +1 -0
  254. package/dist/visibility/diagnostics.d.ts +26 -0
  255. package/dist/visibility/diagnostics.js +36 -0
  256. package/dist/visibility/distance.d.ts +11 -0
  257. package/dist/visibility/distance.js +30 -0
  258. package/dist/visibility/frustum.d.ts +47 -0
  259. package/dist/visibility/frustum.js +186 -0
  260. package/dist/visibility/occlusion.d.ts +36 -0
  261. package/dist/visibility/occlusion.js +45 -0
  262. package/dist/visibility/settings.d.ts +75 -0
  263. package/dist/visibility/settings.js +49 -0
  264. package/dist/visibility/simulationCulling.d.ts +34 -0
  265. package/dist/visibility/simulationCulling.js +36 -0
  266. package/dist/visibility/spatialIndex.d.ts +45 -0
  267. package/dist/visibility/spatialIndex.js +212 -0
  268. package/dist/visibility/visibilitySystem.d.ts +90 -0
  269. package/dist/visibility/visibilitySystem.js +257 -0
  270. package/dist/world/buildings.d.ts +6 -0
  271. package/dist/world/buildings.js +152 -0
  272. package/dist/world/carve.d.ts +7 -2
  273. package/dist/world/carve.js +17 -7
  274. package/dist/world/cellStates.d.ts +45 -0
  275. package/dist/world/cellStates.js +103 -0
  276. package/dist/world/environmentSummary.d.ts +21 -4
  277. package/dist/world/environmentSummary.js +22 -3
  278. package/dist/world/features.d.ts +185 -8
  279. package/dist/world/features.js +64 -6
  280. package/dist/world/gridInstances.d.ts +7 -8
  281. package/dist/world/gridInstances.js +145 -7
  282. package/dist/world/lod.d.ts +17 -0
  283. package/dist/world/lod.js +66 -0
  284. package/dist/world/mapLayers.d.ts +60 -0
  285. package/dist/world/mapLayers.js +44 -0
  286. package/dist/world/massing.d.ts +45 -0
  287. package/dist/world/massing.js +581 -0
  288. package/dist/world/minimap.d.ts +13 -1
  289. package/dist/world/minimap.js +21 -2
  290. package/dist/world/roads.d.ts +47 -0
  291. package/dist/world/roads.js +146 -0
  292. package/dist/world/streets.d.ts +80 -0
  293. package/dist/world/streets.js +144 -0
  294. package/dist/world/terrain.d.ts +69 -3
  295. package/dist/world/terrain.js +309 -16
  296. package/dist/world/water.d.ts +7 -0
  297. package/dist/world/water.js +19 -4
  298. package/dist/world/weather.d.ts +3 -18
  299. package/dist/world/weather.js +4 -17
  300. package/dist/world/windZones.d.ts +47 -0
  301. package/dist/world/windZones.js +59 -0
  302. package/llms.txt +3013 -1726
  303. package/package.json +12 -2
@@ -0,0 +1,556 @@
1
+ import { advanceSpawnDirector, createSpawnDirectorState } from "../ai/spawnDirector.js";
2
+ import { createAbilityKit } from "../combat/abilityKit.js";
3
+ import { setGamePhase } from "../game/gamePhase.js";
4
+ import { leveling } from "../game/progression.js";
5
+ import { createRunDraft } from "../game/runDraft.js";
6
+ import { aimToPoint } from "../input/pointer.js";
7
+ import { seededRng } from "../item/affix.js";
8
+ import { selectAutoTarget } from "../scene/autoTarget.js";
9
+ import { HIT_EFFECT, XP_GEM_BASE_TYPE, leveled, } from "./spec.js";
10
+ function gemRarity(spec, value) {
11
+ for (const [threshold, rarity] of spec.xpGems.rarityThresholds) {
12
+ if (value >= threshold)
13
+ return rarity;
14
+ }
15
+ return spec.xpGems.defaultRarity;
16
+ }
17
+ function facing(fromX, fromZ, toX, toZ) {
18
+ return Math.atan2(toX - fromX, toZ - fromZ);
19
+ }
20
+ function buildKit(spec, levels) {
21
+ return createAbilityKit(Object.entries(spec.weapons).map(([id, weapon]) => ({
22
+ id,
23
+ cooldownMs: leveled(weapon.cooldownMs, levels[id] ?? 1),
24
+ })));
25
+ }
26
+ function compileEntities(spec) {
27
+ const out = {
28
+ [spec.player.kind]: {
29
+ role: "player",
30
+ movement: { walkSpeed: spec.player.walkSpeed },
31
+ stats: {
32
+ health: { max: spec.player.health, min: 0 },
33
+ xp: { max: 1, min: 0, current: 0 },
34
+ level: { max: spec.progression.maxLevel, min: 1, current: 1 },
35
+ },
36
+ receive: { hit: { order: ["health"] } },
37
+ },
38
+ };
39
+ for (const [id, enemy] of Object.entries(spec.enemies)) {
40
+ out[id] = {
41
+ role: "enemy",
42
+ movement: { walkSpeed: enemy.walkSpeed },
43
+ stats: { health: { max: enemy.health, min: 0 } },
44
+ receive: { hit: { order: ["health"] } },
45
+ };
46
+ }
47
+ return out;
48
+ }
49
+ export function createCartridge(spec) {
50
+ const track = leveling({ xpForLevel: spec.progression.xp, maxLevel: spec.progression.maxLevel });
51
+ const entities = compileEntities(spec);
52
+ const runs = new WeakMap();
53
+ function initialPhase() {
54
+ if (spec.flow?.start === "gate")
55
+ return "start";
56
+ if ((spec.flow?.countdownSeconds ?? 0) > 0)
57
+ return "countdown";
58
+ return "playing";
59
+ }
60
+ function seededState() {
61
+ const seed = spec.seed ?? "cartridge";
62
+ const weaponLevels = {};
63
+ for (const id of Object.keys(spec.weapons))
64
+ weaponLevels[id] = 1;
65
+ const offers = spec.progression.draft.upgrades.map((upgrade) => ({
66
+ id: upgrade.id,
67
+ label: upgrade.label,
68
+ weight: upgrade.weight,
69
+ maxStacks: upgrade.maxStacks,
70
+ }));
71
+ return {
72
+ rng: seededRng(seed),
73
+ spawn: createSpawnDirectorState(spec.spawning.director),
74
+ weaponLevels,
75
+ kit: buildKit(spec, weaponLevels),
76
+ fields: { ...spec.fields },
77
+ enemyNextHitAt: new Map(),
78
+ phase: initialPhase(),
79
+ playingSeconds: 0,
80
+ countdownRemaining: spec.flow?.countdownSeconds ?? 0,
81
+ kills: 0,
82
+ levelUpQueue: 0,
83
+ draft: createRunDraft({ offers, rng: seededRng(`${seed}-draft`) }),
84
+ pendingOffers: null,
85
+ bolts: [],
86
+ pulses: [],
87
+ fxCounter: 0,
88
+ };
89
+ }
90
+ function createRun() {
91
+ const run = {
92
+ ...seededState(),
93
+ listeners: new Set(),
94
+ view: {
95
+ get phase() {
96
+ return run.phase;
97
+ },
98
+ get playingSeconds() {
99
+ return run.playingSeconds;
100
+ },
101
+ get countdownRemaining() {
102
+ return run.countdownRemaining;
103
+ },
104
+ get kills() {
105
+ return run.kills;
106
+ },
107
+ weaponLevel: (weaponId) => run.weaponLevels[weaponId] ?? 1,
108
+ field: (name) => run.fields[name] ?? 0,
109
+ get pendingOffers() {
110
+ if (run.pendingOffers === null)
111
+ return null;
112
+ return run.pendingOffers.map((offer) => ({ id: offer.id, label: offer.label ?? offer.id }));
113
+ },
114
+ get bolts() {
115
+ return run.bolts;
116
+ },
117
+ get pulses() {
118
+ return run.pulses;
119
+ },
120
+ subscribe(listener) {
121
+ run.listeners.add(listener);
122
+ return () => run.listeners.delete(listener);
123
+ },
124
+ },
125
+ };
126
+ return run;
127
+ }
128
+ function getRun(ctx) {
129
+ let existing = runs.get(ctx);
130
+ if (existing === undefined) {
131
+ existing = createRun();
132
+ runs.set(ctx, existing);
133
+ }
134
+ return existing;
135
+ }
136
+ function notify(run) {
137
+ for (const listener of run.listeners)
138
+ listener();
139
+ }
140
+ function syncPhase(ctx, run) {
141
+ setGamePhase(ctx, run.phase === "playing" ? "playing" : run.phase === "won" || run.phase === "lost" ? "ended" : "menu");
142
+ }
143
+ function damageMultiplier(run) {
144
+ return run.fields["damageMultiplier"] ?? 1;
145
+ }
146
+ function spawnPoint(ctx, run) {
147
+ const placement = spec.spawning.placement;
148
+ if (placement.kind === "custom")
149
+ return placement.position(ctx, run.view);
150
+ const angle = run.rng() * Math.PI * 2;
151
+ const x = Math.cos(angle) * placement.radius;
152
+ const z = Math.sin(angle) * placement.radius;
153
+ return [x, ctx.world.groundHeightAt(x, z), z];
154
+ }
155
+ function advanceSpawning(ctx, run, dt) {
156
+ const alive = ctx.scene.entity.list().filter((entity) => spec.enemies[entity.name] !== undefined).length;
157
+ const step = advanceSpawnDirector(spec.spawning.director, run.spawn, dt, { alive, players: 1 });
158
+ run.spawn = step.state;
159
+ for (const request of step.spawns) {
160
+ if (spec.enemies[request.entryId] === undefined)
161
+ continue;
162
+ ctx.scene.entity.spawn(request.entryId, { position: spawnPoint(ctx, run) });
163
+ }
164
+ }
165
+ function advanceEnemies(ctx, run, dt) {
166
+ const playerId = ctx.player.userId;
167
+ const player = ctx.scene.entity.get(playerId);
168
+ if (player === null)
169
+ return;
170
+ const now = ctx.time.now();
171
+ for (const enemy of ctx.scene.entity.list()) {
172
+ const def = spec.enemies[enemy.name];
173
+ if (def === undefined)
174
+ continue;
175
+ const next = def.behavior === "none" ? null : ctx.scene.entity.moveToward(enemy.id, playerId, { speed: def.walkSpeed, dt });
176
+ if (next !== null) {
177
+ const grounded = [
178
+ next[0],
179
+ ctx.world.groundHeightAt(next[0], next[2]),
180
+ next[2],
181
+ ];
182
+ ctx.scene.entity.setPose(enemy.id, {
183
+ position: grounded,
184
+ rotationY: facing(enemy.position[0], enemy.position[2], player.position[0], player.position[2]),
185
+ });
186
+ }
187
+ const distance = ctx.scene.entity.distance(enemy.id, playerId);
188
+ if (distance === null || distance > spec.combat.contactRadius)
189
+ continue;
190
+ const readyAt = run.enemyNextHitAt.get(enemy.id) ?? 0;
191
+ if (now < readyAt)
192
+ continue;
193
+ run.enemyNextHitAt.set(enemy.id, now + def.contact.intervalSeconds);
194
+ ctx.scene.entity.effect({ from: enemy.id, to: playerId, effect: HIT_EFFECT, via: { amount: def.contact.damage } });
195
+ }
196
+ }
197
+ function enemyIds(ctx) {
198
+ return ctx.scene.entity
199
+ .list()
200
+ .filter((entity) => spec.enemies[entity.name] !== undefined)
201
+ .map((entity) => entity.id);
202
+ }
203
+ function fireProjectile(ctx, run, weaponId, weapon, playerId, playerPos, enemies) {
204
+ const targetId = selectAutoTarget("nearest", playerId, {
205
+ candidates: () => enemies,
206
+ distance: (from, to) => ctx.scene.entity.distance(from, to),
207
+ });
208
+ if (targetId === null)
209
+ return;
210
+ const target = ctx.scene.entity.get(targetId);
211
+ if (target === null)
212
+ return;
213
+ const dx = target.position[0] - playerPos[0];
214
+ const dz = target.position[2] - playerPos[2];
215
+ const distance = Math.hypot(dx, dz);
216
+ if (distance > weapon.range)
217
+ return;
218
+ run.kit.cast(weaponId);
219
+ const level = run.weaponLevels[weaponId] ?? 1;
220
+ const damage = leveled(weapon.damage, level) * damageMultiplier(run);
221
+ const aim = aimToPoint(playerPos, target.position);
222
+ const shotId = ctx.scene.entity.fireProjectile({ from: playerId, via: { amount: damage }, aim, effect: HIT_EFFECT });
223
+ const travelSeconds = Math.min(1.1, Math.max(0.08, distance / weapon.speed));
224
+ run.fxCounter += 1;
225
+ run.bolts.push({
226
+ id: run.fxCounter,
227
+ weaponId,
228
+ origin: playerPos,
229
+ target: target.position,
230
+ firedAt: ctx.time.now(),
231
+ travelSeconds,
232
+ });
233
+ ctx.time.after(travelSeconds, () => {
234
+ ctx.scene.entity.settleProjectile(shotId);
235
+ });
236
+ }
237
+ function fireOrbit(ctx, run, weaponId, weapon, playerId, playerPos, enemies) {
238
+ run.kit.cast(weaponId);
239
+ const level = run.weaponLevels[weaponId] ?? 1;
240
+ const damage = leveled(weapon.damage, level) * damageMultiplier(run);
241
+ const blades = leveled(weapon.blades, level);
242
+ const radius = leveled(weapon.radius, level);
243
+ const angle = ctx.time.now() * weapon.angularSpeed;
244
+ const alreadyHit = new Set();
245
+ for (let blade = 0; blade < blades; blade += 1) {
246
+ const theta = angle + (blade * Math.PI * 2) / blades;
247
+ const bladePos = [
248
+ playerPos[0] + Math.cos(theta) * radius,
249
+ playerPos[1],
250
+ playerPos[2] + Math.sin(theta) * radius,
251
+ ];
252
+ for (const instanceId of ctx.scene.entity.inRadius(bladePos, weapon.hitRadius, (id) => enemies.includes(id))) {
253
+ if (alreadyHit.has(instanceId))
254
+ continue;
255
+ alreadyHit.add(instanceId);
256
+ ctx.scene.entity.effect({ from: playerId, to: instanceId, effect: HIT_EFFECT, via: { amount: damage } });
257
+ }
258
+ }
259
+ }
260
+ function firePulse(ctx, run, weaponId, weapon, playerId, playerPos) {
261
+ run.kit.cast(weaponId);
262
+ const level = run.weaponLevels[weaponId] ?? 1;
263
+ const damage = leveled(weapon.damage, level) * damageMultiplier(run);
264
+ const radius = leveled(weapon.radius, level);
265
+ ctx.scene.entity.effect({ from: playerId, effect: HIT_EFFECT, via: { amount: damage }, at: playerPos, radius, falloff: "linear" });
266
+ run.fxCounter += 1;
267
+ run.pulses.push({
268
+ id: run.fxCounter,
269
+ weaponId,
270
+ at: playerPos,
271
+ firedAt: ctx.time.now(),
272
+ durationSeconds: weapon.durationSeconds,
273
+ maxRadius: radius,
274
+ });
275
+ }
276
+ function fireWeapons(ctx, run, dt) {
277
+ run.kit.tick(dt);
278
+ const playerId = ctx.player.userId;
279
+ const player = ctx.scene.entity.get(playerId);
280
+ if (player === null)
281
+ return;
282
+ const enemies = enemyIds(ctx);
283
+ for (const [weaponId, weapon] of Object.entries(spec.weapons)) {
284
+ const state = run.kit.state(weaponId);
285
+ if (state === null || !state.ready)
286
+ continue;
287
+ if (weapon.kind === "projectile") {
288
+ fireProjectile(ctx, run, weaponId, weapon, playerId, player.position, enemies);
289
+ }
290
+ else if (weapon.kind === "orbit") {
291
+ fireOrbit(ctx, run, weaponId, weapon, playerId, player.position, enemies);
292
+ }
293
+ else if (weapon.kind === "pulse") {
294
+ firePulse(ctx, run, weaponId, weapon, playerId, player.position);
295
+ }
296
+ else {
297
+ run.kit.cast(weaponId);
298
+ const level = run.weaponLevels[weaponId] ?? 1;
299
+ weapon.fire(ctx, run.view, {
300
+ weaponId,
301
+ level,
302
+ damage: leveled(weapon.damage, level) * damageMultiplier(run),
303
+ playerId,
304
+ playerPosition: player.position,
305
+ enemyIds: enemies,
306
+ dt,
307
+ });
308
+ }
309
+ }
310
+ }
311
+ function maybeOpenDraft(ctx, run) {
312
+ if (run.pendingOffers !== null || run.levelUpQueue <= 0 || run.phase !== "playing")
313
+ return;
314
+ run.levelUpQueue -= 1;
315
+ run.pendingOffers = run.draft.present(spec.progression.draft.choices);
316
+ ctx.time.pause();
317
+ notify(run);
318
+ }
319
+ function onLevelUp(ctx, run, level) {
320
+ ctx.game.events.emit("stat.levelUp", { userId: ctx.player.userId, stat: "level", level });
321
+ run.levelUpQueue += 1;
322
+ maybeOpenDraft(ctx, run);
323
+ }
324
+ function advancePickups(ctx, run, dt) {
325
+ const playerId = ctx.player.userId;
326
+ if (ctx.scene.entity.get(playerId) === null)
327
+ return;
328
+ const magnetRadius = run.fields["magnetRadius"] ?? 0;
329
+ for (const record of ctx.scene.worldItem.list()) {
330
+ if (record.baseType !== XP_GEM_BASE_TYPE)
331
+ continue;
332
+ const distance = ctx.scene.entity.distance(record.instanceId, playerId);
333
+ if (distance === null)
334
+ continue;
335
+ if (distance <= spec.xpGems.collectRadius) {
336
+ ctx.scene.worldItem.consume(record.instanceId);
337
+ track.grantXp({
338
+ get: (userId, statId) => ctx.scene.entity.stats.get(userId, statId),
339
+ set: (userId, statId, patch) => ctx.scene.entity.stats.set(userId, statId, patch),
340
+ }, playerId, record.count, (level) => onLevelUp(ctx, run, level));
341
+ continue;
342
+ }
343
+ if (distance > magnetRadius)
344
+ continue;
345
+ const next = ctx.scene.entity.moveToward(record.instanceId, playerId, { speed: spec.xpGems.pullSpeed, dt });
346
+ if (next !== null) {
347
+ const grounded = [
348
+ next[0],
349
+ ctx.world.groundHeightAt(next[0], next[2]),
350
+ next[2],
351
+ ];
352
+ ctx.scene.entity.setPose(record.instanceId, { position: grounded });
353
+ }
354
+ }
355
+ }
356
+ function pruneFx(ctx, run) {
357
+ const now = ctx.time.now();
358
+ run.bolts = run.bolts.filter((bolt) => now - bolt.firedAt <= bolt.travelSeconds + 0.15);
359
+ run.pulses = run.pulses.filter((pulse) => now - pulse.firedAt <= pulse.durationSeconds + 0.05);
360
+ }
361
+ function applyUpgrade(ctx, run, offerId) {
362
+ const upgrade = spec.progression.draft.upgrades.find((entry) => entry.id === offerId);
363
+ if (upgrade === undefined)
364
+ return;
365
+ const stacks = run.draft.stack().count(offerId);
366
+ const effect = upgrade.effect;
367
+ switch (effect.kind) {
368
+ case "weaponLevel": {
369
+ run.weaponLevels[effect.weapon] = 1 + stacks;
370
+ run.kit = buildKit(spec, run.weaponLevels);
371
+ return;
372
+ }
373
+ case "statBonus": {
374
+ const current = ctx.scene.entity.stats.get(ctx.player.userId, effect.stat);
375
+ const max = (current?.max ?? 0) + effect.amount;
376
+ const value = (current?.current ?? 0) + effect.amount;
377
+ ctx.scene.entity.stats.set(ctx.player.userId, effect.stat, { max, current: value });
378
+ return;
379
+ }
380
+ case "fieldAdd": {
381
+ const base = spec.fields?.[effect.field] ?? 0;
382
+ run.fields[effect.field] = base + effect.amount * stacks;
383
+ return;
384
+ }
385
+ case "fieldMultiply": {
386
+ const base = spec.fields?.[effect.field] ?? 1;
387
+ run.fields[effect.field] = base * effect.factor ** stacks;
388
+ return;
389
+ }
390
+ case "custom": {
391
+ effect.apply(ctx, run.view, stacks);
392
+ return;
393
+ }
394
+ }
395
+ }
396
+ function chooseUpgrade(ctx, offerId) {
397
+ const run = getRun(ctx);
398
+ if (run.pendingOffers === null)
399
+ return;
400
+ if (!run.draft.choose(offerId))
401
+ return;
402
+ applyUpgrade(ctx, run, offerId);
403
+ run.pendingOffers = null;
404
+ ctx.time.play();
405
+ notify(run);
406
+ maybeOpenDraft(ctx, run);
407
+ }
408
+ function registerEvents(ctx) {
409
+ const run = getRun(ctx);
410
+ syncPhase(ctx, run);
411
+ if (spec.rules.killLeaderboardStat !== undefined) {
412
+ ctx.game.leaderboard?.track({ stat: spec.rules.killLeaderboardStat, scope: "profile" });
413
+ }
414
+ run.disposeEvents?.();
415
+ run.disposeEvents = ctx.game.events.on("entity.died", (event) => {
416
+ if (event.instanceId === ctx.player.userId) {
417
+ if (spec.rules.lose?.kind === "playerDeath" && run.phase === "playing") {
418
+ run.phase = "lost";
419
+ syncPhase(ctx, run);
420
+ notify(run);
421
+ }
422
+ return;
423
+ }
424
+ if (event.reason.kind !== "player_kill")
425
+ return;
426
+ const def = spec.enemies[event.catalogId];
427
+ if (def === undefined)
428
+ return;
429
+ run.kills += 1;
430
+ if (spec.rules.killLeaderboardStat !== undefined) {
431
+ ctx.game.leaderboard?.increment(ctx.player.userId, spec.rules.killLeaderboardStat, { scope: "profile" });
432
+ }
433
+ ctx.scene.worldItem.spawn({
434
+ itemId: XP_GEM_BASE_TYPE,
435
+ baseType: XP_GEM_BASE_TYPE,
436
+ position: event.position,
437
+ count: def.xp,
438
+ rarity: gemRarity(spec, def.xp),
439
+ });
440
+ });
441
+ }
442
+ function checkOutcome(ctx, run) {
443
+ const win = spec.rules.win;
444
+ if (win !== undefined) {
445
+ const won = win.kind === "survive" ? run.playingSeconds >= win.seconds : win.check(ctx, run.view);
446
+ if (won) {
447
+ run.phase = "won";
448
+ syncPhase(ctx, run);
449
+ notify(run);
450
+ return;
451
+ }
452
+ }
453
+ const lose = spec.rules.lose;
454
+ if (lose?.kind === "custom" && lose.check(ctx, run.view)) {
455
+ run.phase = "lost";
456
+ syncPhase(ctx, run);
457
+ notify(run);
458
+ }
459
+ }
460
+ function begin(ctx) {
461
+ const run = getRun(ctx);
462
+ if (run.phase !== "start")
463
+ return;
464
+ run.phase = run.countdownRemaining > 0 ? "countdown" : "playing";
465
+ syncPhase(ctx, run);
466
+ notify(run);
467
+ }
468
+ function despawnCartridgeEntities(ctx) {
469
+ for (const entity of ctx.scene.entity.list()) {
470
+ if (spec.enemies[entity.name] !== undefined)
471
+ ctx.scene.entity.despawn(entity.id);
472
+ }
473
+ for (const record of ctx.scene.worldItem.list()) {
474
+ if (record.baseType === XP_GEM_BASE_TYPE)
475
+ ctx.scene.entity.despawn(record.instanceId);
476
+ }
477
+ }
478
+ function reset(ctx) {
479
+ const run = getRun(ctx);
480
+ Object.assign(run, seededState());
481
+ despawnCartridgeEntities(ctx);
482
+ if (ctx.scene.entity.get(ctx.player.userId) === null) {
483
+ ctx.scene.entity.spawn(spec.player.kind, {
484
+ id: ctx.player.userId,
485
+ position: spec.player.spawnAt ?? [0, 0, 0],
486
+ });
487
+ }
488
+ ctx.scene.entity.stats.set(ctx.player.userId, "health", { max: spec.player.health, current: spec.player.health });
489
+ ctx.scene.entity.stats.set(ctx.player.userId, "xp", { max: track.xpForLevel(1), current: 0 });
490
+ ctx.scene.entity.stats.set(ctx.player.userId, "level", { current: 1 });
491
+ ctx.scene.entity.setPose(ctx.player.userId, { position: spec.player.spawnAt ?? [0, 0, 0] });
492
+ ctx.time.play();
493
+ syncPhase(ctx, run);
494
+ notify(run);
495
+ }
496
+ function tick(ctx, dt) {
497
+ const run = getRun(ctx);
498
+ if (dt <= 0 || run.pendingOffers !== null)
499
+ return;
500
+ if (run.phase === "countdown") {
501
+ run.countdownRemaining -= dt;
502
+ if (run.countdownRemaining <= 0) {
503
+ run.countdownRemaining = 0;
504
+ run.phase = "playing";
505
+ syncPhase(ctx, run);
506
+ }
507
+ notify(run);
508
+ return;
509
+ }
510
+ if (run.phase !== "playing")
511
+ return;
512
+ run.playingSeconds += dt;
513
+ advanceSpawning(ctx, run, dt);
514
+ advanceEnemies(ctx, run, dt);
515
+ fireWeapons(ctx, run, dt);
516
+ advancePickups(ctx, run, dt);
517
+ pruneFx(ctx, run);
518
+ for (const system of spec.systems ?? [])
519
+ system(ctx, run.view, dt);
520
+ checkOutcome(ctx, run);
521
+ }
522
+ const loop = {
523
+ onInit(ctx) {
524
+ registerEvents(ctx);
525
+ if (spec.flow?.restart === true && !ctx.game.commands.has("restart")) {
526
+ ctx.game.commands.define("restart", {
527
+ apply(state) {
528
+ reset(ctx);
529
+ return state;
530
+ },
531
+ });
532
+ }
533
+ },
534
+ onNewPlayer(ctx) {
535
+ ctx.scene.entity.spawn(spec.player.kind, {
536
+ id: ctx.player.userId,
537
+ position: spec.player.spawnAt ?? [0, 0, 0],
538
+ });
539
+ ctx.scene.entity.stats.set(ctx.player.userId, "xp", { max: track.xpForLevel(1), current: 0 });
540
+ ctx.scene.entity.stats.set(ctx.player.userId, "level", { current: 1 });
541
+ },
542
+ onTick(ctx, dt) {
543
+ tick(ctx, dt);
544
+ },
545
+ };
546
+ return {
547
+ content: { entityById: (id) => entities[id] ?? null },
548
+ loop,
549
+ run: (ctx) => getRun(ctx).view,
550
+ weaponKit: (ctx) => getRun(ctx).kit,
551
+ chooseUpgrade,
552
+ begin,
553
+ reset,
554
+ spec,
555
+ };
556
+ }