@series-inc/rundot-syncplay 5.26.0-beta.6 → 5.26.0-beta.7

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 (69) hide show
  1. package/README.md +125 -44
  2. package/core/index.d.ts +0 -54
  3. package/core/native/fixed_math.cpp +11 -4
  4. package/core/runtime.d.ts +0 -304
  5. package/core/scripts/emit-product-digests.mjs +0 -14
  6. package/core/src/index.mjs +0 -40
  7. package/core/src/kinetix-deterministic-stdlib.mjs +30 -13
  8. package/core/src/runtime.mjs +0 -40
  9. package/dist/browser.d.ts +1 -3
  10. package/dist/browser.js +0 -1
  11. package/dist/effects-adapter.d.ts +1 -0
  12. package/dist/effects-adapter.js +9 -0
  13. package/dist/index.d.ts +1 -3
  14. package/dist/index.js +0 -1
  15. package/dist/math.js +30 -5
  16. package/dist/modules/actors-ai.d.ts +111 -0
  17. package/dist/modules/actors-ai.js +808 -0
  18. package/dist/modules/character-3d.d.ts +129 -0
  19. package/dist/modules/character-3d.js +844 -0
  20. package/dist/modules/combat.d.ts +183 -0
  21. package/dist/modules/combat.js +861 -0
  22. package/dist/modules/component-model.d.ts +95 -0
  23. package/dist/modules/component-model.js +466 -0
  24. package/dist/modules/entity-registry.d.ts +40 -0
  25. package/dist/modules/entity-registry.js +177 -0
  26. package/dist/modules/firearm.d.ts +72 -0
  27. package/dist/modules/firearm.js +367 -0
  28. package/dist/modules/gameplay-effects.d.ts +157 -0
  29. package/dist/modules/gameplay-effects.js +795 -0
  30. package/dist/modules/identity.d.ts +143 -0
  31. package/dist/modules/identity.js +147 -0
  32. package/dist/modules/inventory-equipment.d.ts +171 -0
  33. package/dist/modules/inventory-equipment.js +984 -0
  34. package/dist/modules/module-contract.d.ts +20 -0
  35. package/dist/modules/module-contract.js +129 -0
  36. package/dist/modules/module-event.d.ts +15 -0
  37. package/dist/modules/module-event.js +84 -0
  38. package/dist/modules/platformer-2d.d.ts +107 -0
  39. package/dist/modules/platformer-2d.js +566 -0
  40. package/dist/modules/rts.d.ts +192 -0
  41. package/dist/modules/rts.js +1446 -0
  42. package/dist/modules/spatial-interaction.d.ts +44 -0
  43. package/dist/modules/spatial-interaction.js +248 -0
  44. package/dist/modules/stats-abilities.d.ts +235 -0
  45. package/dist/modules/stats-abilities.js +1488 -0
  46. package/dist/modules/unit-control.d.ts +101 -0
  47. package/dist/modules/unit-control.js +387 -0
  48. package/dist/networked-client.d.ts +9 -0
  49. package/dist/networked-client.js +132 -12
  50. package/dist/run-room-transport.d.ts +2 -0
  51. package/dist/run-room-transport.js +1 -0
  52. package/dist/runner.d.ts +9 -1
  53. package/dist/runner.js +7 -0
  54. package/dist/tools/index.d.ts +1 -0
  55. package/dist/tools/index.js +1 -0
  56. package/dist/tools/module-certification.d.ts +41 -0
  57. package/dist/tools/module-certification.js +456 -0
  58. package/dist/tools/static-checker.d.ts +2 -1
  59. package/dist/tools/static-checker.js +540 -48
  60. package/package.json +101 -6
  61. package/core/scripts/generate-platformer2d-manifest.mjs +0 -82
  62. package/core/src/kinetix-platformer2d-errors.mjs +0 -25
  63. package/core/src/kinetix-platformer2d-level.mjs +0 -323
  64. package/core/src/kinetix-platformer2d-mechanics.mjs +0 -616
  65. package/core/src/kinetix-platformer2d-registry.mjs +0 -154
  66. package/core/src/kinetix-platformer2d-runtime.mjs +0 -1266
  67. package/core/src/kinetix-platformer2d-source-manifest.generated.mjs +0 -16
  68. package/dist/platformer-content.d.ts +0 -49
  69. package/dist/platformer-content.js +0 -100
package/README.md CHANGED
@@ -7,6 +7,123 @@ streams, stepping, opaque checkpoints, serialization, hydration, and checksums.
7
7
  Syncplay owns inputs, prediction, confirmation, rollback policy, replay logs,
8
8
  late join, secrets, and transport.
9
9
 
10
+ ## Game module entrypoints
11
+
12
+ Generic game-layer modules use explicit `./modules/*` package subpaths. The
13
+ module tree is code-only: binary art, audio, meshes, and other game content
14
+ belong in complete examples, not this published package. Each entrypoint
15
+ exports and documents its own `stable` or `provisional` API status.
16
+
17
+ Every new module subpath must be present in the export-surface golden and the
18
+ module bundle-budget golden. The guards derive their entry set from
19
+ `package.json#exports`, so an unclassified export or a module without a budget
20
+ fails CI.
21
+
22
+ The first real entrypoint is
23
+ `@series-inc/rundot-syncplay/modules/component-model`. It defines pure
24
+ `step(slice, input, context)` modules, canonical state-slice validation,
25
+ rollback-safe one-shot presentation, a KayKit-backed character clip vocabulary,
26
+ and one third-person KCC3D reference component. V1 module context has no random
27
+ generator: random and secret outcomes arrive as explicit input or authority
28
+ commands so adding or reordering components cannot change a shared RNG stream.
29
+ V1 is function-only; Kinetix descriptor adapters remain outside this arc. The
30
+ component-model API is stable.
31
+
32
+ `@series-inc/rundot-syncplay/modules/character-3d` composes the existing
33
+ deterministic KCC3D and top-down mover into kinematic and top-down controllers,
34
+ with crouch, slide, mantle, dash, wall-run, canonical animation parameters, and
35
+ first-person, orbit, or top-down camera projections. Collision and traversal
36
+ facts are explicit context input; render libraries, assets, and module-owned
37
+ randomness remain outside the module. Its API is stable.
38
+
39
+ `@series-inc/rundot-syncplay/modules/combat` composes deterministic combatant
40
+ health/resistance/i-frame/hitstop resolution, active-frame melee arcs,
41
+ travelling projectiles through the existing 3D shape-cast path, and hitscan
42
+ authority through the existing lag-compensation validator. Hits, hurtboxes,
43
+ body ownership, targets, occluders, and server timing are explicit input or
44
+ context; the module owns no random stream, renderer, or physics engine. Its API
45
+ is stable.
46
+
47
+ `@series-inc/rundot-syncplay/modules/firearm` owns canonical semi-automatic or
48
+ automatic cadence, magazine, reload, recoil, spread-sequence, and trigger-edge
49
+ state. Its inventory helper consumes only the authored ammo item atomically.
50
+ Aim, hits, damage, switching, attachments, and presentation remain caller-owned.
51
+ Its API is provisional because the FPS is its only production example consumer.
52
+
53
+ `@series-inc/rundot-syncplay/modules/gameplay-effects` emits ordered typed
54
+ damage, healing, resource, status, and interrupt commands from immediate or
55
+ periodic definitions. The composition helper applies combat and stats changes
56
+ atomically in target/command order. Targeting, balance, crits, randomness, and
57
+ presentation remain caller-owned. Its API is stable.
58
+
59
+ `@series-inc/rundot-syncplay/modules/actors-ai` adds deterministic authored
60
+ encounters, wave spawning, threat-based aggro, squad target sharing, and crowd
61
+ movement over the existing bot and cooked-navmesh primitives. It accepts
62
+ defeats and threat changes as explicit input, emits rollback-safe semantic
63
+ events, and keeps render, combat, loot, and random encounter generation outside
64
+ the module. Its API is provisional until the promotion gate.
65
+
66
+ `@series-inc/rundot-syncplay/modules/stats-abilities` adds deterministic stat
67
+ blocks, source-replaced modifiers, cast/channel/cooldown ability state, stacked
68
+ statuses, experience levels, and ticket-resolved loot. Random authority remains
69
+ outside the module: loot consumes an explicit integer ticket carried in synced
70
+ input. Equipment integrates through source IDs such as
71
+ `equipment:<ownerId>:<slotId>` and atomic modifier replacement. Its API is
72
+ provisional until the promotion gate.
73
+
74
+ `@series-inc/rundot-syncplay/modules/inventory-equipment` owns deterministic
75
+ inventory slots, stacks, loot containers, pickups, drops, and equipment
76
+ references. Equipped items remain in canonical inventory slots, and successful
77
+ equipment transitions become complete source-replacement input for the real
78
+ stats/abilities step. It owns no loot randomness, persistence, trading,
79
+ rendering, or binary content. Its API is stable.
80
+
81
+ `@series-inc/rundot-syncplay/modules/spatial-interaction` owns deterministic
82
+ range/cone selection, focus, hold progress, cancellation, and one-shot
83
+ completion. The caller owns the completed action and presentation. Its API is
84
+ stable.
85
+
86
+ `@series-inc/rundot-syncplay/modules/platformer-2d` composes the deterministic
87
+ 2D KCC with manual and autonomous movement, pause/resume/reverse control,
88
+ edge/wall reversal latches, rising-edge jump input, one-way drop priority, and
89
+ canonical render bindings. Level geometry and environmental facts remain
90
+ explicit game-owned context. Its API is provisional until the promotion gate.
91
+
92
+ `@series-inc/rundot-syncplay/modules/rts` declares the provisional deterministic
93
+ RTS state, command, configuration, event, and projection boundary. The module
94
+ delegates unit movement to the existing public deterministic navmesh step;
95
+ rendering, content, health resolution, room lifecycle, and win rules remain
96
+ game-owned. Its exact-shape implementation includes bounded selection/orders,
97
+ formations, live-hostile target validation, explicit attack-move acquisition
98
+ provenance, fog, economy, placement, production, and stable attack requests;
99
+ certification remains a required provisional-module gate. Hydrated and issued
100
+ authored `attack-target` orders must name a live opposing unit or building;
101
+ self, friendly, queued, retired, and missing targets fail closed.
102
+
103
+ The normal runner emits side effects only from authority-confirmed frames.
104
+ Predicted/canceled lifecycle callbacks are available to lower-level speculative
105
+ presentations that feed their `createDeterministicEventTimeline` records into
106
+ the module presentation adapter; prediction state never enters deterministic
107
+ module context.
108
+
109
+ `./tools` is a supported public Node/build-tooling surface and is governed by
110
+ the export golden. The import-safe `./core/build-native` and
111
+ `./core/build-wasm-physics` callables are governed as well. Only
112
+ `./core/preflight` is excluded from public-symbol comparison because it exports
113
+ nothing and is a command that executes on import.
114
+
115
+ `certifyModule(component, fixture)` on `./tools` is the provisional conformance
116
+ gate for `./modules/*`. It delegates straight, rollback, replay, and hydration
117
+ equivalence to `runSyncplaySynctest`, then adds module-only source rules,
118
+ permutation equivalence, p99 step time, and retained heap per step. Run it under
119
+ Node with `--expose-gc`; missing GC support fails closed instead of reporting a
120
+ zero allocation.
121
+
122
+ The published JavaScript graph has a committed exhaustive side-effect audit.
123
+ `core/scripts/preflight.mjs` and
124
+ `core/scripts/emit-product-digests.mjs` are the only import-time command
125
+ effects and are preserved through the package's precise `sideEffects` allowlist.
126
+
10
127
  > **Upgrading an existing game?** See
11
128
  > [MIGRATING-SESSION-PRIMITIVES.md](./MIGRATING-SESSION-PRIMITIVES.md). Two
12
129
  > changes are not additive: the runner now rejects an unsafe input codec at
@@ -44,14 +161,8 @@ There is one session factory:
44
161
 
45
162
  ```ts
46
163
  import { createSyncplaySession } from '@series-inc/rundot-syncplay/browser';
47
- import { createInstalledPlatformer2dRuntime } from '@series-inc/rundot-syncplay/core/runtime';
48
164
 
49
- const runtime = createInstalledPlatformer2dRuntime({
50
- pack,
51
- levelBytes,
52
- identity,
53
- sessionConfigBytes,
54
- });
165
+ const runtime = runtimeFactory(runtimeIdentity, sessionConfigBytes);
55
166
 
56
167
  const session = createSyncplaySession(runtime, {
57
168
  playerCount: 4,
@@ -64,8 +175,8 @@ const session = createSyncplaySession(runtime, {
64
175
 
65
176
  The runtime must implement `@series-inc/rundot-syncplay/core/runtime`. Syncplay never
66
177
  accepts an initial state, reducer, or creator-authored step callback. There is no
67
- compatibility mode for the removed reducer API. A non-platformer game supplies
68
- the same ABI through `createInstalledRuntimeAdapter` and binds its source with
178
+ compatibility mode for the removed reducer API. A game supplies the ABI through
179
+ `createInstalledRuntimeAdapter` and binds its source with
69
180
  `deriveCustomRuntimeIdentity` plus `kinetixRuntimeModuleManifest`.
70
181
 
71
182
  ## Runtime and snapshot contract
@@ -78,7 +189,8 @@ destination validates all of those values before mutating its live session.
78
189
 
79
190
  The networked client receives the authority's exact runtime identity and
80
191
  session-config bytes, asks the game for a matching runtime through its runtime
81
- factory, and rejects a locally loaded pack mismatch before frame 0. Replays use
192
+ factory, and rejects a local runtime or content identity mismatch before frame 0.
193
+ Replays use
82
194
  the same identity/config/factory path.
83
195
 
84
196
  ### Capability requirements
@@ -235,29 +347,9 @@ async preparation, then replays every buffered message once.
235
347
 
236
348
  The descriptor is exactly `{ slot, playerCount, seed, tickRateHz,
237
349
  hardToleranceTicks, role, runtimeIdentity, sessionConfigBytes,
238
- sessionConfigDigest }`. It does **not** carry `levelEntryId`, a level digest, or a
239
- level length those are game-specific fields inside `sessionConfigBytes`, and you
240
- decode them with your own session-config schema:
241
-
242
- ```ts
243
- import { decodeKinetixSessionConfig } from '@series-inc/rundot-syncplay/core/runtime';
244
- import { kinetixPlatformer2dSessionConfigSchema } from '@series-inc/rundot-syncplay/core/runtime';
245
-
246
- const prepareNetworkRuntime = async (descriptor, signal) => {
247
- const session = decodeKinetixSessionConfig(
248
- kinetixPlatformer2dSessionConfigSchema,
249
- descriptor.sessionConfigBytes,
250
- );
251
- // session.levelEntryId / session.levelDigest / session.levelByteLength
252
- const levelBytes = await preparePlatformerLevel(api.ugc, {
253
- levelEntryId: session.levelEntryId,
254
- levelDigest: session.levelDigest,
255
- levelByteLength: session.levelByteLength,
256
- encodeLevel: (level) => encodeKinetixPlatformer2dLevel(level, { maxBytes: 102_400 }),
257
- });
258
- registerPreparedLevel(descriptor.sessionConfigDigest, levelBytes);
259
- };
260
- ```
350
+ sessionConfigDigest }`. Game-specific asset declarations belong inside canonical
351
+ `sessionConfigBytes`; resolve and verify those assets before constructing the
352
+ network runtime.
261
353
 
262
354
  ## Safe input codecs
263
355
 
@@ -274,17 +366,6 @@ For an existing hand-written codec, `validateSyncplayInputCodec({ encodeInput,
274
366
  decodeInput, samples })` probes round-trip stability, `null → neutral`, and a
275
367
  battery of hostile payloads, failing with `SYNCPLAY_INPUT_CODEC_UNSAFE`.
276
368
 
277
- ## Version-safe content references
278
-
279
- `publishPlatformerLevel(api, level, options)` always calls `ugc.create` — never
280
- `ugc.update` on a referenced entry, which would make every live room pinned to it
281
- permanently unjoinable. `preparePlatformerLevel(api, declaration)` resolves a pin
282
- and raises the typed `SYNCPLAY_LEVEL_CONTENT_CHANGED` (naming the entry id and the
283
- edit-vs-pin cause) rather than an opaque `KINETIX_AUX_ASSET_MISMATCH` deep inside
284
- runtime construction. `declaration.encodeLevel` is an **injected callback**, not a
285
- static core import, so Syncplay's compiled output never hard-binds a specific
286
- core build.
287
-
288
369
  **Capacity and late-join budgets.** Snapshot transfer defaults are **256 KiB**
289
370
  chunks and a **4 MiB** total ceiling; the immutable level payload is fetched and
290
371
  verified separately and never rides a snapshot. `maxPredictionTicks` is a
package/core/index.d.ts CHANGED
@@ -23,61 +23,7 @@ export type {
23
23
  KinetixRuntimeIdentity,
24
24
  KinetixSessionConfigEnvelope,
25
25
  } from './runtime';
26
- export {
27
- KINETIX_PLATFORMER2D_LEVEL_FORMAT,
28
- KINETIX_PLATFORMER2D_PACK_FORMAT,
29
- KINETIX_PLATFORMER2D_PACK_FORMAT_V1,
30
- KINETIX_PLATFORMER2D_LEVEL_FORMAT_V1,
31
- KINETIX_PLATFORMER2D_SPENT_TILE_ID,
32
- KinetixPlatformerLevelError,
33
- KinetixPlatformerRegistryError,
34
- MAX_KINETIX_PLATFORMER2D_LEVEL_BYTES,
35
- applyKinetixPlatformer2dTileOverrides,
36
- createInstalledPlatformer2dRuntime,
37
- createKinetixPlatformer2dRegistry,
38
- createKinetixPlatformer2dSession,
39
- decodeKinetixPlatformer2dLevel,
40
- defineKinetixPlatformer2dPack,
41
- deriveKinetixPlatformer2dRuntimeIdentity,
42
- describeKinetixPlatformer2dLevel,
43
- encodeKinetixPlatformer2dLevel,
44
- kinetixPlatformer2dBuiltInEntityTypes,
45
- kinetixPlatformer2dDeterministicAbi,
46
- kinetixPlatformer2dLevelBonusBlocks,
47
- kinetixPlatformer2dLevelBreakableTileIds,
48
- kinetixPlatformer2dLevelOpenBounds,
49
- kinetixPlatformer2dLevelSlopeTiles,
50
- kinetixPlatformer2dSessionConfigSchema,
51
- kinetixPlatformer2dSourceManifest,
52
- migrateKinetixPlatformer2dLevel,
53
- migrateKinetixPlatformer2dPack,
54
- nextRngUint32,
55
- rngInt,
56
- seedRng,
57
- } from './runtime';
58
26
  export type {
59
- KinetixPlatformer2dAbilities,
60
- KinetixPlatformer2dBonusBlock,
61
- KinetixPlatformer2dBonusContents,
62
- KinetixPlatformer2dEntityDescriptor,
63
- KinetixPlatformer2dPackV1,
64
- KinetixPlatformer2dEntityValidationContext,
65
- KinetixPlatformer2dIdentityOptions,
66
- KinetixPlatformer2dEvent,
67
- KinetixPlatformer2dGoalMode,
68
- KinetixPlatformer2dInput,
69
- KinetixPlatformer2dLevel,
70
- KinetixPlatformer2dLevelEntity,
71
- KinetixPlatformer2dPack,
72
- KinetixPlatformer2dPhase,
73
- KinetixPlatformer2dPlayerProjection,
74
- KinetixPlatformer2dPowerUpKind,
75
- KinetixPlatformer2dProjection,
76
- KinetixPlatformer2dRegistry,
77
- KinetixPlatformer2dSessionSetup,
78
- KinetixPlatformer2dSlopeTile,
79
- KinetixPlatformer2dTickRate,
80
- KinetixPlatformerLevelErrorCode,
81
27
  KinetixRuntimeModuleManifestEntry,
82
28
  } from './runtime';
83
29
  export type { KinetixSessionConfigSchema } from './authoring';
@@ -24,6 +24,13 @@ double jsRound(double x) {
24
24
  // Number.isInteger: finite and integral. Non-integers make the reference throw.
25
25
  bool isFixed(double v) { return std::isfinite(v) && std::floor(v) == v; }
26
26
 
27
+ // The reference restricts the domain so the over-2^53 divergence window is
28
+ // empty rather than merely unlikely (src/math.ts). An interpolant outside
29
+ // [0, fixedScale] is the one intermediate that restriction does not cover.
30
+ bool isInterpolant(const Config& c, double v) {
31
+ return isFixed(v) && v >= 0.0 && v <= c.fixedScale;
32
+ }
33
+
27
34
  bool isSafeInteger(double v) { return isFixed(v) && std::fabs(v) <= 9007199254740991.0; }
28
35
 
29
36
  // NOT std::fmin/std::fmax: those IGNORE a NaN operand and return the other one,
@@ -249,7 +256,7 @@ double maxFixed(const Config& c, double a, double b, bool& ok) {
249
256
  }
250
257
 
251
258
  double lerp(const Config& c, double a, double b, double t, bool& ok) {
252
- if (!isFixed(a) || !isFixed(b) || !isFixed(t)) {
259
+ if (!isFixed(a) || !isFixed(b) || !isInterpolant(c, t)) {
253
260
  ok = false;
254
261
  return 0.0;
255
262
  }
@@ -594,7 +601,7 @@ Vec2 vec2FromAngleTurns(const Config& c, double turns, double length, bool& ok)
594
601
  double vec2AngleTurns(const Config& c, Vec2 v, bool& ok) { return atan2Turns(c, v.y, v.x, ok); }
595
602
 
596
603
  Vec2 vec2Lerp(const Config& c, Vec2 a, Vec2 b, double t, bool& ok) {
597
- if (!isFixed(t)) {
604
+ if (!isInterpolant(c, t)) {
598
605
  ok = false;
599
606
  return Vec2{0, 0};
600
607
  }
@@ -686,7 +693,7 @@ Vec3 vec3Normalize(const Config& c, Vec3 v, bool& ok) {
686
693
  }
687
694
 
688
695
  Vec3 vec3Lerp(const Config& c, Vec3 a, Vec3 b, double t, bool& ok) {
689
- if (!isFixed(t)) {
696
+ if (!isInterpolant(c, t)) {
690
697
  ok = false;
691
698
  return Vec3{0, 0, 0};
692
699
  }
@@ -718,7 +725,7 @@ Quat quatFromYawTurns(const Config& c, double turns, bool& ok) {
718
725
  }
719
726
 
720
727
  Quat quatLerp(const Config& c, Quat a, Quat b, double t, bool& ok) {
721
- if (!isFixed(t)) {
728
+ if (!isInterpolant(c, t)) {
722
729
  ok = false;
723
730
  return Quat{0, 0, 0, 0};
724
731
  }
package/core/runtime.d.ts CHANGED
@@ -161,311 +161,7 @@ export function createKinetixSessionConfig(
161
161
  value: Readonly<Record<string, unknown>>,
162
162
  ): KinetixSessionConfigEnvelope;
163
163
 
164
- export type KinetixPlatformer2dTickRate = 10 | 20 | 30 | 60;
165
- export type KinetixPlatformer2dGoalMode = 'any-player' | 'all-players';
166
-
167
- export interface KinetixPlatformer2dAbilities {
168
- readonly crouch: boolean;
169
- readonly groundPound: boolean;
170
- readonly wallJump: boolean;
171
- readonly climb: boolean;
172
- readonly swim: boolean;
173
- readonly carry: boolean;
174
- }
175
-
176
- export interface KinetixPlatformer2dPack {
177
- readonly format: 'rundot.kinetix-platformer2d-pack.v2';
178
- readonly tickRate: KinetixPlatformer2dTickRate;
179
- readonly player: { readonly halfWidth: number; readonly halfHeight: number; readonly maxHealth: number };
180
- readonly movement: {
181
- readonly gravity: number; readonly maxFallSpeed: number;
182
- readonly runAcceleration: number; readonly runDeceleration: number; readonly maxRunSpeed: number;
183
- readonly jumpSpeed: number; readonly jumpCutGravityMultiplier: number;
184
- readonly coyoteFrames: number; readonly jumpBufferFrames: number; readonly dropThroughFrames: number;
185
- readonly crouchHalfHeight: number; readonly climbSpeed: number;
186
- readonly swimGravity: number; readonly swimStrokeSpeed: number; readonly maxSwimFallSpeed: number;
187
- readonly wallSlideSpeed: number; readonly wallJumpSpeedX: number; readonly wallJumpSpeedY: number;
188
- readonly groundPoundSpeed: number; readonly throwSpeedX: number; readonly throwSpeedY: number;
189
- };
190
- readonly damage: {
191
- readonly invulnerabilityFrames: number; readonly respawnFrames: number;
192
- readonly stompBounceSpeed: number; readonly respawnGraceFrames: number;
193
- };
194
- readonly match: { readonly countdownFrames: number; readonly resultsFrames: number; readonly goalMode: KinetixPlatformer2dGoalMode };
195
- readonly abilities: KinetixPlatformer2dAbilities;
196
- }
197
-
198
- /** A v1 pack, accepted by `defineKinetixPlatformer2dPack` and migrated with every v2 ability off. */
199
- export interface KinetixPlatformer2dPackV1 {
200
- readonly format: 'rundot.kinetix-platformer2d-pack.v1';
201
- readonly tickRate: KinetixPlatformer2dTickRate;
202
- readonly player: KinetixPlatformer2dPack['player'];
203
- readonly movement: Omit<KinetixPlatformer2dPack['movement'],
204
- 'crouchHalfHeight' | 'climbSpeed' | 'swimGravity' | 'swimStrokeSpeed' | 'maxSwimFallSpeed'
205
- | 'wallSlideSpeed' | 'wallJumpSpeedX' | 'wallJumpSpeedY' | 'groundPoundSpeed' | 'throwSpeedX' | 'throwSpeedY'>;
206
- readonly damage: Omit<KinetixPlatformer2dPack['damage'], 'respawnGraceFrames'>;
207
- readonly match: KinetixPlatformer2dPack['match'];
208
- }
209
-
210
- export const KINETIX_PLATFORMER2D_PACK_FORMAT: 'rundot.kinetix-platformer2d-pack.v2';
211
- export const KINETIX_PLATFORMER2D_PACK_FORMAT_V1: 'rundot.kinetix-platformer2d-pack.v1';
212
-
213
- export function migrateKinetixPlatformer2dPack(
214
- value: KinetixPlatformer2dPack | KinetixPlatformer2dPackV1,
215
- ): KinetixPlatformer2dPack;
216
-
217
- export interface KinetixPlatformer2dInput {
218
- readonly moveX: -1 | 0 | 1;
219
- readonly moveY: -1 | 0 | 1;
220
- readonly jumpPressed: boolean;
221
- readonly jumpHeld: boolean;
222
- readonly dropPressed: boolean;
223
- readonly actionPressed: boolean;
224
- readonly crouchHeld: boolean;
225
- }
226
-
227
- /** Folds a running session's `tileOverrides` back into a publishable level object. */
228
- export function applyKinetixPlatformer2dTileOverrides(
229
- level: KinetixPlatformer2dLevel,
230
- state: { readonly tileOverrides: readonly (readonly [number, number])[] },
231
- ): KinetixPlatformer2dLevel;
232
-
233
- interface KinetixPlatformer2dBox {
234
- readonly x: number; readonly y: number;
235
- readonly halfWidth: number; readonly halfHeight: number;
236
- }
237
- interface KinetixPlatformer2dPath {
238
- readonly halfWidth: number; readonly halfHeight: number;
239
- readonly framesPerLeg: number;
240
- readonly waypoints: readonly { readonly x: number; readonly y: number }[];
241
- }
242
-
243
- export type KinetixPlatformer2dLevelEntity =
244
- | { readonly id: string; readonly type: 'spawn'; readonly slot: number; readonly x: number; readonly y: number }
245
- | ({ readonly id: string; readonly type: 'oneWayPlatform' } & KinetixPlatformer2dBox)
246
- | ({ readonly id: string; readonly type: 'movingPlatform' } & KinetixPlatformer2dPath)
247
- | ({ readonly id: string; readonly type: 'fallingPlatform'; readonly triggerDelayFrames: number; readonly resetFrames: number } & KinetixPlatformer2dBox)
248
- | ({ readonly id: string; readonly type: 'hazard'; readonly damage: number } & KinetixPlatformer2dBox)
249
- | ({ readonly id: string; readonly type: 'stompable'; readonly respawnFrames: number } & KinetixPlatformer2dBox)
250
- | ({ readonly id: string; readonly type: 'goalTrigger' } & KinetixPlatformer2dBox)
251
- | ({ readonly id: string; readonly type: 'patrolEnemy'; readonly speed: number; readonly turnAtLedge: boolean; readonly damage: number; readonly respawnFrames: number } & KinetixPlatformer2dBox)
252
- | ({ readonly id: string; readonly type: 'waypointEnemy'; readonly damage: number; readonly respawnFrames: number } & KinetixPlatformer2dPath)
253
- | ({ readonly id: string; readonly type: 'movingHazard'; readonly damage: number } & KinetixPlatformer2dPath)
254
- | ({ readonly id: string; readonly type: 'spring'; readonly launchSpeed: number } & KinetixPlatformer2dBox)
255
- | ({ readonly id: string; readonly type: 'conveyor'; readonly beltSpeed: number } & KinetixPlatformer2dBox)
256
- | ({ readonly id: string; readonly type: 'checkpoint' } & KinetixPlatformer2dBox)
257
- | ({ readonly id: string; readonly type: 'coin' } & KinetixPlatformer2dBox)
258
- | ({ readonly id: string; readonly type: 'powerUp'; readonly kind: KinetixPlatformer2dPowerUpKind } & KinetixPlatformer2dBox)
259
- | ({ readonly id: string; readonly type: 'carryItem'; readonly damage: number } & KinetixPlatformer2dBox)
260
- | ({ readonly id: string; readonly type: 'warpPipe'; readonly targetId: string } & KinetixPlatformer2dBox)
261
- | ({ readonly id: string; readonly type: 'climbRegion' } & KinetixPlatformer2dBox)
262
- | ({ readonly id: string; readonly type: 'waterRegion' } & KinetixPlatformer2dBox)
263
- | { readonly id: string; readonly type: string; readonly [key: string]: unknown };
264
-
265
- export type KinetixPlatformer2dPowerUpKind = 'heal' | 'maxHealthUp' | 'doubleJump';
266
- export type KinetixPlatformer2dBonusContents = 'coin' | 'random' | KinetixPlatformer2dPowerUpKind;
267
-
268
- export interface KinetixPlatformer2dSlopeTile {
269
- readonly tileId: number;
270
- readonly dir: 'up-right' | 'up-left';
271
- }
272
- export interface KinetixPlatformer2dBonusBlock {
273
- readonly x: number; readonly y: number;
274
- readonly contents: KinetixPlatformer2dBonusContents;
275
- readonly count: number;
276
- }
277
-
278
- export interface KinetixPlatformer2dLevel extends Readonly<Record<string, unknown>> {
279
- readonly format: 'rundot.kinetix-platformer2d-level.v2';
280
- readonly playerCapacity: number;
281
- readonly tileGrid: {
282
- readonly width: number; readonly height: number; readonly tileSize: number;
283
- readonly tilesBase64: string; readonly solidTileIds: readonly number[];
284
- };
285
- readonly entities: readonly KinetixPlatformer2dLevelEntity[];
286
- /** Absent means `false`: implicit solid walls at the level's left and right edges. */
287
- readonly openBounds?: boolean;
288
- readonly slopeTiles?: readonly KinetixPlatformer2dSlopeTile[];
289
- readonly breakableTileIds?: readonly number[];
290
- readonly bonusBlocks?: readonly KinetixPlatformer2dBonusBlock[];
291
- }
292
-
293
- export type KinetixPlatformerLevelErrorCode =
294
- | 'KINETIX_PLATFORMER_LEVEL_INVALID'
295
- | 'KINETIX_PLATFORMER_LEVEL_NONCANONICAL'
296
- | 'KINETIX_PLATFORMER_LEVEL_SIZE_LIMIT';
297
-
298
- export class KinetixPlatformerLevelError extends Error {
299
- readonly code: KinetixPlatformerLevelErrorCode;
300
- /** JSON-pointer-style path to the offending field, `''` for whole-level failures. */
301
- readonly path: string;
302
- readonly expected: string;
303
- readonly actual: unknown;
304
- }
305
-
306
- export class KinetixPlatformerRegistryError extends Error {
307
- readonly code:
308
- | 'KINETIX_PLATFORMER_REGISTRY_INVALID'
309
- | 'KINETIX_PLATFORMER_REGISTRY_UNBOUND'
310
- | 'KINETIX_PLATFORMER_SOURCE_MANIFEST_INVALID';
311
- }
312
-
313
- export interface KinetixPlatformer2dEntityValidationContext {
314
- /** Throws a structured error; `path` is relative to this entity (e.g. `'/pushX'`). */
315
- readonly fail: (path: string, expected: string, actual: unknown) => never;
316
- readonly level: KinetixPlatformer2dLevel;
317
- readonly grid: KinetixPlatformer2dLevel['tileGrid'];
318
- }
319
-
320
- export interface KinetixPlatformer2dEntityDescriptor {
321
- readonly type: string;
322
- readonly keys: readonly string[];
323
- readonly validate: (entity: never, ctx: KinetixPlatformer2dEntityValidationContext) => void;
324
- readonly behavior: {
325
- readonly init?: (entity: never, ctx: never) => unknown;
326
- readonly step?: (entityState: never, ctx: never) => void;
327
- };
328
- }
329
-
330
- export interface KinetixPlatformer2dRegistry {
331
- readonly hasExtensions: boolean;
332
- readonly extensionTypes: readonly string[];
333
- readonly byType: ReadonlyMap<string, KinetixPlatformer2dEntityDescriptor>;
334
- readonly descriptors: readonly KinetixPlatformer2dEntityDescriptor[];
335
- }
336
-
337
- export function createKinetixPlatformer2dRegistry(
338
- extensions?: readonly KinetixPlatformer2dEntityDescriptor[],
339
- ): KinetixPlatformer2dRegistry;
340
- export const kinetixPlatformer2dBuiltInEntityTypes: readonly string[];
341
-
342
- export const KINETIX_PLATFORMER2D_LEVEL_FORMAT: 'rundot.kinetix-platformer2d-level.v2';
343
- export const KINETIX_PLATFORMER2D_LEVEL_FORMAT_V1: 'rundot.kinetix-platformer2d-level.v1';
344
- export const KINETIX_PLATFORMER2D_SPENT_TILE_ID: 255;
345
-
346
- export function migrateKinetixPlatformer2dLevel(level: unknown): KinetixPlatformer2dLevel;
347
- export function kinetixPlatformer2dLevelOpenBounds(level: KinetixPlatformer2dLevel): boolean;
348
- export function kinetixPlatformer2dLevelSlopeTiles(
349
- level: KinetixPlatformer2dLevel,
350
- ): readonly KinetixPlatformer2dSlopeTile[];
351
- export function kinetixPlatformer2dLevelBreakableTileIds(
352
- level: KinetixPlatformer2dLevel,
353
- ): readonly number[];
354
- export function kinetixPlatformer2dLevelBonusBlocks(
355
- level: KinetixPlatformer2dLevel,
356
- ): readonly KinetixPlatformer2dBonusBlock[];
357
-
358
- export interface KinetixPlatformer2dSessionSetup {
359
- readonly runtimeIdentity: KinetixRuntimeIdentity;
360
- readonly sessionConfigBytes: Uint8Array;
361
- readonly sessionConfigDigest: string;
362
- readonly level: KinetixAuxiliaryAssetDeclaration;
363
- }
364
-
365
- export const kinetixPlatformer2dDeterministicAbi: 'rundot.kinetix-platformer2d-js.v2';
366
- export const MAX_KINETIX_PLATFORMER2D_LEVEL_BYTES: 102_400;
367
- export const kinetixPlatformer2dSessionConfigSchema: import('./authoring').KinetixSessionConfigSchema;
368
-
369
- export function defineKinetixPlatformer2dPack(
370
- value: KinetixPlatformer2dPack | KinetixPlatformer2dPackV1,
371
- ): Readonly<KinetixPlatformer2dPack>;
372
164
  export interface KinetixRuntimeModuleManifestEntry {
373
165
  readonly path: string;
374
- /** Lowercase sha256 hex of the module's bytes. */
375
166
  readonly digest: string;
376
167
  }
377
-
378
- export interface KinetixPlatformer2dIdentityOptions {
379
- readonly registry?: KinetixPlatformer2dRegistry;
380
- /**
381
- * Required when `registry` carries extensions — custom entity behavior must be
382
- * folded into identity or peers running different code would silently diverge.
383
- * Omitting it throws `KINETIX_PLATFORMER_REGISTRY_UNBOUND`.
384
- */
385
- readonly registryManifest?: readonly KinetixRuntimeModuleManifestEntry[];
386
- readonly sourceManifest?: readonly KinetixRuntimeModuleManifestEntry[];
387
- }
388
-
389
- export const kinetixPlatformer2dSourceManifest: readonly KinetixRuntimeModuleManifestEntry[];
390
-
391
- /**
392
- * The reducer's xorshift32 stream, exported so presentation can re-derive sim
393
- * rolls (predicted particles, cosmetic effects) without forking constants.
394
- * Seeded and stepped exactly like syncplay's `DeterministicRandom`; the parity
395
- * vector is golden-pinned in tests. Wrap the seed as `{ rng: seedRng(seed) }` —
396
- * the steppers mutate `state.rng` in place.
397
- */
398
- export function seedRng(seed?: number): number;
399
- export function nextRngUint32(state: { rng: number }): number;
400
- export function rngInt(state: { rng: number }, minInclusive: number, maxInclusive: number): number;
401
-
402
- export function deriveKinetixPlatformer2dRuntimeIdentity(
403
- pack: KinetixPlatformer2dPack,
404
- options?: KinetixPlatformer2dIdentityOptions,
405
- ): KinetixRuntimeIdentity;
406
- export function encodeKinetixPlatformer2dLevel(
407
- level: unknown,
408
- options: { readonly maxBytes: number; readonly registry?: KinetixPlatformer2dRegistry },
409
- ): Uint8Array;
410
- export function decodeKinetixPlatformer2dLevel(
411
- bytes: Uint8Array,
412
- options?: { readonly registry?: KinetixPlatformer2dRegistry },
413
- ): Readonly<KinetixPlatformer2dLevel>;
414
- export function describeKinetixPlatformer2dLevel(
415
- bytes: Uint8Array,
416
- options?: { readonly registry?: KinetixPlatformer2dRegistry },
417
- ): Readonly<KinetixAuxiliaryAssetDeclaration>;
418
- export function createKinetixPlatformer2dSession(options: {
419
- readonly pack: KinetixPlatformer2dPack;
420
- readonly levelEntryId: string;
421
- readonly levelBytes: Uint8Array;
422
- readonly playerCount: number;
423
- readonly seed: number;
424
- readonly registry?: KinetixPlatformer2dRegistry;
425
- readonly registryManifest?: readonly KinetixRuntimeModuleManifestEntry[];
426
- }): Readonly<KinetixPlatformer2dSessionSetup>;
427
-
428
- export type KinetixPlatformer2dPhase = 'countdown' | 'playing' | 'results';
429
- export interface KinetixPlatformer2dEvent {
430
- readonly eventId: string; readonly frame: number; readonly sequence: number;
431
- readonly payload: Readonly<Record<string, string | number | boolean>>;
432
- readonly predictionState: 'synced'; readonly dedupeKey: string;
433
- readonly notHashedKeys: readonly string[]; readonly deliveryScope: 'all';
434
- }
435
- export interface KinetixPlatformer2dPlayerProjection {
436
- readonly slot: number; readonly x: number; readonly y: number;
437
- readonly vx: number; readonly vy: number; readonly health: number;
438
- readonly grounded: boolean; readonly groundId: string | null;
439
- readonly dead: boolean; readonly finished: boolean;
440
- readonly maxHealth: number; readonly score: number; readonly coins: number;
441
- readonly facing: -1 | 1; readonly crouching: boolean; readonly climbing: boolean;
442
- readonly swimming: boolean; readonly groundPounding: boolean;
443
- readonly carryingId: string | null; readonly checkpointId: string | null;
444
- readonly doubleJumpCharges: number;
445
- }
446
- export interface KinetixPlatformer2dProjection {
447
- readonly frame: number; readonly phase: KinetixPlatformer2dPhase; readonly phaseFrame: number;
448
- readonly players: readonly KinetixPlatformer2dPlayerProjection[];
449
- readonly platforms: readonly Readonly<Record<string, unknown>>[];
450
- readonly stompables: readonly Readonly<Record<string, unknown>>[];
451
- readonly enemies: readonly Readonly<Record<string, unknown>>[];
452
- readonly hazards: readonly Readonly<Record<string, unknown>>[];
453
- readonly pickups: readonly Readonly<Record<string, unknown>>[];
454
- readonly spawnedPickups: readonly Readonly<Record<string, unknown>>[];
455
- readonly carryItems: readonly Readonly<Record<string, unknown>>[];
456
- /** Per-extension-entity behavior state (`behavior.init` return values), sorted by entity id. */
457
- readonly extensions: readonly { readonly id: string; readonly state: unknown }[];
458
- /** Sparse `[tileIndex, tileId]` pairs; apply with `applyKinetixPlatformer2dTileOverrides`. */
459
- readonly tileOverrides: readonly (readonly [number, number])[];
460
- readonly events: readonly KinetixPlatformer2dEvent[];
461
- }
462
- export function createInstalledPlatformer2dRuntime(options: {
463
- readonly pack: KinetixPlatformer2dPack;
464
- readonly levelBytes: Uint8Array;
465
- readonly identity: KinetixRuntimeIdentity;
466
- readonly sessionConfigBytes: Uint8Array;
467
- readonly registry?: KinetixPlatformer2dRegistry;
468
- readonly registryManifest?: readonly KinetixRuntimeModuleManifestEntry[];
469
- /** How often the full-state finiteness walk runs, in frames. Defaults to 60. */
470
- readonly validateStateEveryFrames?: number;
471
- }): KinetixRuntime<Readonly<Record<string, unknown>>, KinetixPlatformer2dInput, unknown, KinetixPlatformer2dProjection>;
@@ -2,7 +2,6 @@
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { readFileSync, writeFileSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
- import { deriveKinetixPlatformer2dRuntimeIdentity } from '../src/kinetix-platformer2d-runtime.mjs';
6
5
 
7
6
  function option(name) {
8
7
  const index = process.argv.indexOf(`--${name}`);
@@ -32,18 +31,6 @@ function checkGeneratedProduct(root, game) {
32
31
  }
33
32
  }
34
33
 
35
- const platformerPack = {
36
- format: 'rundot.kinetix-platformer2d-pack.v1',
37
- tickRate: 60,
38
- player: { halfWidth: 6, halfHeight: 12, maxHealth: 2 },
39
- movement: {
40
- gravity: 900, maxFallSpeed: 420, runAcceleration: 1200, runDeceleration: 1600,
41
- maxRunSpeed: 150, jumpSpeed: 330, jumpCutGravityMultiplier: 2.5,
42
- coyoteFrames: 6, jumpBufferFrames: 6, dropThroughFrames: 8,
43
- },
44
- damage: { invulnerabilityFrames: 45, respawnFrames: 60, stompBounceSpeed: 220 },
45
- match: { countdownFrames: 60, resultsFrames: 180, goalMode: 'all-players' },
46
- };
47
34
  const racingRoot = resolve(option('racing-root'));
48
35
  const latticeRoot = resolve(option('lattice-root'));
49
36
  checkGeneratedProduct(racingRoot, 'racing');
@@ -51,7 +38,6 @@ checkGeneratedProduct(latticeRoot, 'lattice');
51
38
  const digests = {
52
39
  racing: runtimeIdentity(racingRoot, 'src/kinetix/racing.product.generated.json'),
53
40
  lattice: runtimeIdentity(latticeRoot, 'src/kinetix/lattice.product.generated.json'),
54
- platformerEngineIdentity: deriveKinetixPlatformer2dRuntimeIdentity(platformerPack).engineIdentityHash,
55
41
  };
56
42
 
57
43
  writeFileSync(option('out'), `${JSON.stringify(digests, null, 2)}\n`);