@netforge/engine 0.0.0-stage → 0.1.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/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +147 -2
  5. package/dist/api/appearance.d.ts +37 -0
  6. package/dist/api/appearance.js +41 -0
  7. package/dist/api/cards.d.ts +105 -0
  8. package/dist/api/cards.js +133 -0
  9. package/dist/api/collector.d.ts +114 -0
  10. package/dist/api/collector.js +204 -0
  11. package/dist/api/dungeon.d.ts +58 -0
  12. package/dist/api/dungeon.js +216 -0
  13. package/dist/api/harness.d.ts +149 -0
  14. package/dist/api/harness.js +549 -0
  15. package/dist/api/idle.d.ts +71 -0
  16. package/dist/api/idle.js +104 -0
  17. package/dist/api/race-harness.d.ts +3 -0
  18. package/dist/api/race-harness.js +148 -0
  19. package/dist/api/registry.d.ts +241 -0
  20. package/dist/api/registry.js +394 -0
  21. package/dist/api/scheduler.d.ts +56 -0
  22. package/dist/api/scheduler.js +160 -0
  23. package/dist/api/schemas.d.ts +958 -0
  24. package/dist/api/schemas.js +859 -0
  25. package/dist/api/tower.d.ts +127 -0
  26. package/dist/api/tower.js +183 -0
  27. package/dist/game/adapter.d.ts +69 -0
  28. package/dist/game/adapter.js +341 -0
  29. package/dist/game/arena-bot.d.ts +66 -0
  30. package/dist/game/arena-bot.js +188 -0
  31. package/dist/game/bot.d.ts +95 -0
  32. package/dist/game/bot.js +285 -0
  33. package/dist/game/compile.d.ts +97 -0
  34. package/dist/game/compile.js +476 -0
  35. package/dist/game/director.d.ts +42 -0
  36. package/dist/game/director.js +179 -0
  37. package/dist/game/earn-source.d.ts +122 -0
  38. package/dist/game/earn-source.js +386 -0
  39. package/dist/game/earn.d.ts +49 -0
  40. package/dist/game/earn.js +55 -0
  41. package/dist/game/index.d.ts +37 -0
  42. package/dist/game/index.js +25 -0
  43. package/dist/game/platformer.d.ts +156 -0
  44. package/dist/game/platformer.js +619 -0
  45. package/dist/game/profile.d.ts +97 -0
  46. package/dist/game/profile.js +140 -0
  47. package/dist/game/race.d.ts +177 -0
  48. package/dist/game/race.js +386 -0
  49. package/dist/game/racer.d.ts +98 -0
  50. package/dist/game/racer.js +379 -0
  51. package/dist/game/runner.d.ts +286 -0
  52. package/dist/game/runner.js +824 -0
  53. package/dist/game/threats.d.ts +23 -0
  54. package/dist/game/threats.js +49 -0
  55. package/dist/game/view.d.ts +47 -0
  56. package/dist/game/view.js +123 -0
  57. package/dist/index.d.ts +33 -0
  58. package/dist/index.js +24 -0
  59. package/dist/net/bits.d.ts +33 -0
  60. package/dist/net/bits.js +135 -0
  61. package/dist/net/client.d.ts +75 -0
  62. package/dist/net/client.js +151 -0
  63. package/dist/net/index.d.ts +23 -0
  64. package/dist/net/index.js +23 -0
  65. package/dist/net/input.d.ts +49 -0
  66. package/dist/net/input.js +112 -0
  67. package/dist/net/interpolation.d.ts +37 -0
  68. package/dist/net/interpolation.js +97 -0
  69. package/dist/net/netsim.d.ts +22 -0
  70. package/dist/net/netsim.js +52 -0
  71. package/dist/net/prediction.d.ts +31 -0
  72. package/dist/net/prediction.js +101 -0
  73. package/dist/net/profile-client.d.ts +75 -0
  74. package/dist/net/profile-client.js +173 -0
  75. package/dist/net/protocol.d.ts +208 -0
  76. package/dist/net/protocol.js +8 -0
  77. package/dist/net/room-client.d.ts +57 -0
  78. package/dist/net/room-client.js +144 -0
  79. package/dist/net/schema.d.ts +78 -0
  80. package/dist/net/schema.js +121 -0
  81. package/dist/net/sim-adapter.d.ts +134 -0
  82. package/dist/net/sim-adapter.js +22 -0
  83. package/dist/net/snapshot.d.ts +61 -0
  84. package/dist/net/snapshot.js +301 -0
  85. package/dist/rules/adapter.d.ts +53 -0
  86. package/dist/rules/adapter.js +261 -0
  87. package/dist/rules/cards.d.ts +115 -0
  88. package/dist/rules/cards.js +547 -0
  89. package/dist/rules/collector.d.ts +161 -0
  90. package/dist/rules/collector.js +732 -0
  91. package/dist/rules/genres.d.ts +13 -0
  92. package/dist/rules/genres.js +38 -0
  93. package/dist/rules/harness.d.ts +16 -0
  94. package/dist/rules/harness.js +127 -0
  95. package/dist/rules/idle.d.ts +115 -0
  96. package/dist/rules/idle.js +370 -0
  97. package/dist/rules/index.d.ts +16 -0
  98. package/dist/rules/index.js +14 -0
  99. package/dist/rules/kit.d.ts +171 -0
  100. package/dist/rules/kit.js +88 -0
  101. package/dist/rules/tower.d.ts +214 -0
  102. package/dist/rules/tower.js +655 -0
  103. package/dist/runtime/arena.d.ts +86 -0
  104. package/dist/runtime/arena.js +269 -0
  105. package/dist/runtime/assets.d.ts +26 -0
  106. package/dist/runtime/assets.js +35 -0
  107. package/dist/runtime/camera.d.ts +113 -0
  108. package/dist/runtime/camera.js +247 -0
  109. package/dist/runtime/crowd.d.ts +18 -0
  110. package/dist/runtime/crowd.js +62 -0
  111. package/dist/runtime/feel/audio.d.ts +150 -0
  112. package/dist/runtime/feel/audio.js +327 -0
  113. package/dist/runtime/feel/clock.d.ts +52 -0
  114. package/dist/runtime/feel/clock.js +80 -0
  115. package/dist/runtime/feel/flash.d.ts +28 -0
  116. package/dist/runtime/feel/flash.js +60 -0
  117. package/dist/runtime/feel/index.d.ts +27 -0
  118. package/dist/runtime/feel/index.js +16 -0
  119. package/dist/runtime/feel/kit.d.ts +159 -0
  120. package/dist/runtime/feel/kit.js +226 -0
  121. package/dist/runtime/feel/knockback.d.ts +41 -0
  122. package/dist/runtime/feel/knockback.js +70 -0
  123. package/dist/runtime/feel/noise.d.ts +6 -0
  124. package/dist/runtime/feel/noise.js +30 -0
  125. package/dist/runtime/feel/numbers.d.ts +91 -0
  126. package/dist/runtime/feel/numbers.js +172 -0
  127. package/dist/runtime/feel/rumble.d.ts +40 -0
  128. package/dist/runtime/feel/rumble.js +58 -0
  129. package/dist/runtime/feel/sound.d.ts +96 -0
  130. package/dist/runtime/feel/sound.js +176 -0
  131. package/dist/runtime/feel/spring.d.ts +42 -0
  132. package/dist/runtime/feel/spring.js +95 -0
  133. package/dist/runtime/feel/trauma.d.ts +48 -0
  134. package/dist/runtime/feel/trauma.js +76 -0
  135. package/dist/runtime/feel/tween.d.ts +66 -0
  136. package/dist/runtime/feel/tween.js +180 -0
  137. package/dist/runtime/feel/types.d.ts +30 -0
  138. package/dist/runtime/feel/types.js +5 -0
  139. package/dist/runtime/index.d.ts +28 -0
  140. package/dist/runtime/index.js +26 -0
  141. package/dist/runtime/input.d.ts +38 -0
  142. package/dist/runtime/input.js +159 -0
  143. package/dist/runtime/interpolate.d.ts +6 -0
  144. package/dist/runtime/interpolate.js +13 -0
  145. package/dist/runtime/render/animation.d.ts +63 -0
  146. package/dist/runtime/render/animation.js +128 -0
  147. package/dist/runtime/render/atmosphere.d.ts +32 -0
  148. package/dist/runtime/render/atmosphere.js +59 -0
  149. package/dist/runtime/render/crowd-motion.d.ts +68 -0
  150. package/dist/runtime/render/crowd-motion.js +163 -0
  151. package/dist/runtime/render/decals.d.ts +64 -0
  152. package/dist/runtime/render/decals.js +222 -0
  153. package/dist/runtime/render/highlight.d.ts +29 -0
  154. package/dist/runtime/render/highlight.js +110 -0
  155. package/dist/runtime/render/index.d.ts +35 -0
  156. package/dist/runtime/render/index.js +22 -0
  157. package/dist/runtime/render/lighting.d.ts +50 -0
  158. package/dist/runtime/render/lighting.js +142 -0
  159. package/dist/runtime/render/particles.d.ts +117 -0
  160. package/dist/runtime/render/particles.js +289 -0
  161. package/dist/runtime/render/post.d.ts +92 -0
  162. package/dist/runtime/render/post.js +191 -0
  163. package/dist/runtime/render/presets.d.ts +57 -0
  164. package/dist/runtime/render/presets.js +116 -0
  165. package/dist/runtime/render/quality.d.ts +113 -0
  166. package/dist/runtime/render/quality.js +180 -0
  167. package/dist/runtime/render/rig.d.ts +29 -0
  168. package/dist/runtime/render/rig.js +107 -0
  169. package/dist/runtime/render/sprite.d.ts +34 -0
  170. package/dist/runtime/render/sprite.js +58 -0
  171. package/dist/runtime/render/stack.d.ts +74 -0
  172. package/dist/runtime/render/stack.js +132 -0
  173. package/dist/runtime/render/vat.d.ts +78 -0
  174. package/dist/runtime/render/vat.js +219 -0
  175. package/dist/runtime/renderer.d.ts +19 -0
  176. package/dist/runtime/renderer.js +18 -0
  177. package/dist/runtime/stats.d.ts +11 -0
  178. package/dist/runtime/stats.js +29 -0
  179. package/dist/runtime/styles/handdrawn/ground.d.ts +11 -0
  180. package/dist/runtime/styles/handdrawn/ground.js +282 -0
  181. package/dist/runtime/styles/handdrawn/index.d.ts +4 -0
  182. package/dist/runtime/styles/handdrawn/index.js +94 -0
  183. package/dist/runtime/styles/handdrawn/ink.d.ts +31 -0
  184. package/dist/runtime/styles/handdrawn/ink.js +68 -0
  185. package/dist/runtime/styles/handdrawn/looks.d.ts +18 -0
  186. package/dist/runtime/styles/handdrawn/looks.js +42 -0
  187. package/dist/runtime/styles/handdrawn/paint.d.ts +59 -0
  188. package/dist/runtime/styles/handdrawn/paint.js +129 -0
  189. package/dist/runtime/styles/handdrawn/palette.d.ts +49 -0
  190. package/dist/runtime/styles/handdrawn/palette.js +70 -0
  191. package/dist/runtime/styles/handdrawn/textures.d.ts +27 -0
  192. package/dist/runtime/styles/handdrawn/textures.js +150 -0
  193. package/dist/runtime/styles/handdrawn/vfx.d.ts +14 -0
  194. package/dist/runtime/styles/handdrawn/vfx.js +73 -0
  195. package/dist/runtime/styles/index.d.ts +9 -0
  196. package/dist/runtime/styles/index.js +32 -0
  197. package/dist/runtime/styles/kit.d.ts +193 -0
  198. package/dist/runtime/styles/kit.js +1 -0
  199. package/dist/runtime/styles/pixel/index.d.ts +46 -0
  200. package/dist/runtime/styles/pixel/index.js +167 -0
  201. package/dist/runtime/styles/ps1/index.d.ts +29 -0
  202. package/dist/runtime/styles/ps1/index.js +41 -0
  203. package/dist/runtime/styles/ps1/lights.d.ts +47 -0
  204. package/dist/runtime/styles/ps1/lights.js +146 -0
  205. package/dist/runtime/styles/ps1/materials.d.ts +62 -0
  206. package/dist/runtime/styles/ps1/materials.js +285 -0
  207. package/dist/runtime/styles/ps1/pipeline.d.ts +52 -0
  208. package/dist/runtime/styles/ps1/pipeline.js +111 -0
  209. package/dist/runtime/styles/ps1/surfaces.d.ts +44 -0
  210. package/dist/runtime/styles/ps1/surfaces.js +228 -0
  211. package/dist/runtime/styles/toon/index.d.ts +5 -0
  212. package/dist/runtime/styles/toon/index.js +178 -0
  213. package/dist/runtime/styles/toon/ink.d.ts +39 -0
  214. package/dist/runtime/styles/toon/ink.js +73 -0
  215. package/dist/runtime/styles/toon/look.d.ts +10 -0
  216. package/dist/runtime/styles/toon/look.js +112 -0
  217. package/dist/runtime/styles/toon/material.d.ts +74 -0
  218. package/dist/runtime/styles/toon/material.js +208 -0
  219. package/dist/runtime/styles/toon/normals.d.ts +26 -0
  220. package/dist/runtime/styles/toon/normals.js +127 -0
  221. package/dist/runtime/styles/toon/shadows.d.ts +24 -0
  222. package/dist/runtime/styles/toon/shadows.js +42 -0
  223. package/dist/runtime/styles/toon/vfx.d.ts +15 -0
  224. package/dist/runtime/styles/toon/vfx.js +47 -0
  225. package/dist/runtime/styles/view.d.ts +35 -0
  226. package/dist/runtime/styles/view.js +34 -0
  227. package/dist/runtime/styles/voxel/core.d.ts +114 -0
  228. package/dist/runtime/styles/voxel/core.js +577 -0
  229. package/dist/runtime/styles/voxel/geometry.d.ts +27 -0
  230. package/dist/runtime/styles/voxel/geometry.js +40 -0
  231. package/dist/runtime/styles/voxel/index.d.ts +21 -0
  232. package/dist/runtime/styles/voxel/index.js +23 -0
  233. package/dist/runtime/styles/voxel/kit.d.ts +6 -0
  234. package/dist/runtime/styles/voxel/kit.js +11 -0
  235. package/dist/runtime/styles/voxel/look.d.ts +10 -0
  236. package/dist/runtime/styles/voxel/look.js +16 -0
  237. package/dist/runtime/styles/voxel/props.d.ts +9 -0
  238. package/dist/runtime/styles/voxel/props.js +52 -0
  239. package/dist/runtime/styles/voxel/shapes.d.ts +41 -0
  240. package/dist/runtime/styles/voxel/shapes.js +83 -0
  241. package/dist/runtime/styles/voxel/world.d.ts +49 -0
  242. package/dist/runtime/styles/voxel/world.js +422 -0
  243. package/llms.txt +183 -0
  244. package/package.json +82 -3
@@ -0,0 +1,144 @@
1
+ // Browser and Node client for a room: asks the world service for a seat,
2
+ // connects, and wires the Colyseus room to a NetClient. Used by the Vite
3
+ // demo and the headless load bots.
4
+ import { Client } from '@colyseus/sdk';
5
+ import { DEFAULT_CLIENT, NetClient } from './client.js';
6
+ import { MSG_INPUT, MSG_SNAPSHOT, } from './protocol.js';
7
+ export async function joinRoom(sim, p) {
8
+ const res = await fetch(`${p.endpoint}/world/join`, {
9
+ method: 'POST',
10
+ headers: { 'content-type': 'application/json' },
11
+ body: JSON.stringify({
12
+ place: p.place,
13
+ template: p.template,
14
+ ...(p.wallet ? { wallet: p.wallet } : {}),
15
+ ...(p.session ? { session: p.session } : {}),
16
+ ...(p.guest ? { guest: p.guest } : {}),
17
+ ...(p.friendRoomId ? { friendRoomId: p.friendRoomId } : {}),
18
+ }),
19
+ });
20
+ if (!res.ok)
21
+ throw new Error(`world/join ${res.status}: ${await res.text()}`);
22
+ const body = (await res.json());
23
+ const reservation = body.reservations[0];
24
+ if (!reservation)
25
+ throw new Error('no reservation');
26
+ return connectReservation(sim, p.endpoint, reservation);
27
+ }
28
+ export async function connectReservation(sim, endpoint, reservation) {
29
+ const client = new Client(endpoint);
30
+ const room = await client.consumeSeatReservation(reservation);
31
+ let bytes = 0;
32
+ let msgs = 0;
33
+ const early = [];
34
+ let net = null;
35
+ room.onMessage(MSG_SNAPSHOT, (payload) => {
36
+ bytes += payload.length;
37
+ msgs++;
38
+ if (net)
39
+ net.receiveSnapshot(payload, performance.now());
40
+ else
41
+ early.push(payload);
42
+ });
43
+ const earnCbs = [];
44
+ room.onMessage('earn', (e) => earnCbs.forEach((cb) => cb(e)));
45
+ let run = null;
46
+ const runCbs = [];
47
+ room.onMessage('run', (info) => {
48
+ run = info;
49
+ runCbs.forEach((cb) => cb(info));
50
+ });
51
+ const runIdCbs = [];
52
+ room.onMessage('run-id', (m) => runIdCbs.forEach((cb) => cb(m)));
53
+ const rebaseCbs = [];
54
+ room.onMessage('rebase', (m) => {
55
+ // Same player, possibly a new entity; seqs continue (the server's queue ignores old ones).
56
+ if (net)
57
+ net = new NetClient(sim, net.player, m.entity, (b) => room.sendBytes(MSG_INPUT, b), { ...DEFAULT_CLIENT, firstSeq: net.nextSeq });
58
+ rebaseCbs.forEach((cb) => cb(m));
59
+ });
60
+ let profile = null;
61
+ const profileCbs = [];
62
+ room.onMessage('profile', (p) => {
63
+ profile = p;
64
+ profileCbs.forEach((cb) => cb(p));
65
+ });
66
+ let stakeMsg = null;
67
+ const stakeCbs = [];
68
+ room.onMessage('stake', (m) => {
69
+ stakeMsg = m;
70
+ stakeCbs.forEach((cb) => cb(m));
71
+ });
72
+ let potMsg = null;
73
+ const potCbs = [];
74
+ room.onMessage('pot', (m) => {
75
+ potMsg = m;
76
+ potCbs.forEach((cb) => cb(m));
77
+ });
78
+ const signIns = [];
79
+ room.onMessage('sign-in-result', (r) => signIns.shift()?.(r));
80
+ const pending = [];
81
+ room.onMessage('command-result', (r) => pending.shift()?.(r));
82
+ room.onMessage('pong', () => undefined);
83
+ const welcome = await new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => reject(new Error('no welcome')), 5000);
85
+ room.onMessage('welcome', (w) => {
86
+ clearTimeout(timer);
87
+ resolve(w);
88
+ });
89
+ });
90
+ const nc = new NetClient(sim, welcome.player, welcome.entity, (b) => room.sendBytes(MSG_INPUT, b));
91
+ net = nc;
92
+ for (const b of early)
93
+ nc.receiveSnapshot(b, performance.now());
94
+ return {
95
+ room,
96
+ get net() {
97
+ return net ?? nc;
98
+ },
99
+ welcome,
100
+ roomId: room.roomId,
101
+ bytesIn: () => bytes,
102
+ messagesIn: () => msgs,
103
+ onEarn: (cb) => earnCbs.push(cb),
104
+ onRun: (cb) => runCbs.push(cb),
105
+ latestRun: () => run,
106
+ onRunId: (cb) => runIdCbs.push(cb),
107
+ onRebase: (cb) => rebaseCbs.push(cb),
108
+ signIn: (session) => new Promise((resolve) => {
109
+ signIns.push(resolve);
110
+ room.send('sign-in', { session });
111
+ }),
112
+ onProfile: (cb) => {
113
+ profileCbs.push(cb);
114
+ if (profile)
115
+ cb(profile);
116
+ },
117
+ latestProfile: () => profile,
118
+ stake: (amount) => room.send('stake', amount !== undefined ? { amount } : {}),
119
+ onStake: (cb) => {
120
+ stakeCbs.push(cb);
121
+ if (stakeMsg)
122
+ cb(stakeMsg);
123
+ },
124
+ latestStake: () => stakeMsg,
125
+ pot: () => room.send('pot', {}),
126
+ onPot: (cb) => {
127
+ potCbs.push(cb);
128
+ if (potMsg)
129
+ cb(potMsg);
130
+ },
131
+ command: (cmd) => new Promise((resolve) => {
132
+ pending.push(resolve);
133
+ room.send('command', cmd);
134
+ }),
135
+ claim: () => new Promise((resolve) => {
136
+ room.onMessage('attest', resolve);
137
+ room.onMessage('attest-error', resolve);
138
+ room.send('claim');
139
+ }),
140
+ leave: async () => {
141
+ await room.leave(true);
142
+ },
143
+ };
144
+ }
@@ -0,0 +1,78 @@
1
+ export type FieldSpec =
2
+ /** Integer in [0, 2^bits). Positions in fixed-point sub-units use this. */
3
+ {
4
+ readonly name: string;
5
+ readonly kind: 'uint';
6
+ readonly bits: number;
7
+ readonly interp?: Interp;
8
+ readonly deltaBits?: number;
9
+ }
10
+ /** Signed integer, zigzag-packed in `bits` bits. */
11
+ | {
12
+ readonly name: string;
13
+ readonly kind: 'int';
14
+ readonly bits: number;
15
+ readonly interp?: Interp;
16
+ readonly deltaBits?: number;
17
+ }
18
+ /** Float quantized linearly into [min, max] with `bits` bits of precision. */
19
+ | {
20
+ readonly name: string;
21
+ readonly kind: 'quant';
22
+ readonly min: number;
23
+ readonly max: number;
24
+ readonly bits: number;
25
+ readonly interp?: Interp;
26
+ readonly deltaBits?: number;
27
+ }
28
+ /** Angle in radians packed into 16 bits (0..2π). Interpolates the short way. */
29
+ | {
30
+ readonly name: string;
31
+ readonly kind: 'angle16';
32
+ readonly deltaBits?: number;
33
+ }
34
+ /** One bit. Never interpolated. */
35
+ | {
36
+ readonly name: string;
37
+ readonly kind: 'bool';
38
+ };
39
+ export type Interp = 'lerp' | 'step';
40
+ export interface EntitySchema {
41
+ readonly fields: readonly FieldSpec[];
42
+ /** Index of the field holding x, used by area-of-interest and rewind. */
43
+ readonly xField: number;
44
+ /** Index of the field holding y. */
45
+ readonly yField: number;
46
+ /** Units of x/y per world unit (e.g. 32 for 1/32 fixed point). */
47
+ readonly unitsPerWorld: number;
48
+ /**
49
+ * Optional per-kind field subsets, ascending field indices. An entity of a
50
+ * listed kind carries only those fields on the wire (its change mask is
51
+ * that many bits) and decodes with the rest as 0. Crowds of simple
52
+ * entities (2,000 enemies) cost a few bits each this way. At most 32 per kind.
53
+ */
54
+ readonly kindFields?: Readonly<Record<number, readonly number[]>>;
55
+ }
56
+ /** One entity as the sim reports it: values are real (dequantized) numbers. */
57
+ export interface EntityState {
58
+ readonly id: number;
59
+ readonly kind: number;
60
+ /** Player id that owns it, 0 for none. */
61
+ readonly owner: number;
62
+ readonly values: readonly number[];
63
+ }
64
+ export declare function fieldBits(f: FieldSpec): number;
65
+ /** Default bit width for a "small change" delta of a field, or 0 for none. */
66
+ export declare function fieldDeltaBits(f: FieldSpec): number;
67
+ /** Real value to the integer the wire carries. */
68
+ export declare function quantize(f: FieldSpec, v: number): number;
69
+ /** Wire integer back to the real value. */
70
+ export declare function dequantize(f: FieldSpec, q: number): number;
71
+ /** Interpolate one field between a and b (real values). */
72
+ export declare function interpolateField(f: FieldSpec, a: number, b: number, t: number): number;
73
+ /**
74
+ * Quantize an entity's values. With `only` (the kind's wire fields), the
75
+ * other fields are left 0: they never go on the wire for that kind.
76
+ */
77
+ export declare function quantizeEntity(schema: EntitySchema, e: EntityState, only?: readonly number[]): number[];
78
+ export declare function dequantizeValues(schema: EntitySchema, q: readonly number[]): number[];
@@ -0,0 +1,121 @@
1
+ // Per-entity field schema: how each networked value is quantized, packed and
2
+ // interpolated. A game (via its SimAdapter) declares one schema; the snapshot
3
+ // codec, the interpolation buffer and area-of-interest all read it.
4
+ const TWO_PI = Math.PI * 2;
5
+ export function fieldBits(f) {
6
+ switch (f.kind) {
7
+ case 'uint':
8
+ case 'int':
9
+ case 'quant':
10
+ return f.bits;
11
+ case 'angle16':
12
+ return 16;
13
+ case 'bool':
14
+ return 1;
15
+ }
16
+ }
17
+ /** Default bit width for a "small change" delta of a field, or 0 for none. */
18
+ export function fieldDeltaBits(f) {
19
+ if (f.kind === 'bool')
20
+ return 0;
21
+ if (f.deltaBits !== undefined)
22
+ return f.deltaBits;
23
+ const b = fieldBits(f);
24
+ return b >= 8 ? Math.max(4, Math.ceil(b / 2)) : 0;
25
+ }
26
+ /** Real value to the integer the wire carries. */
27
+ export function quantize(f, v) {
28
+ switch (f.kind) {
29
+ case 'uint': {
30
+ const max = 2 ** f.bits - 1;
31
+ return clamp(Math.round(v), 0, max);
32
+ }
33
+ case 'int': {
34
+ const lim = 2 ** (f.bits - 1);
35
+ return clamp(Math.round(v), -lim, lim - 1);
36
+ }
37
+ case 'quant': {
38
+ const steps = 2 ** f.bits - 1;
39
+ const t = (clamp(v, f.min, f.max) - f.min) / (f.max - f.min);
40
+ return Math.round(t * steps);
41
+ }
42
+ case 'angle16': {
43
+ const a = ((v % TWO_PI) + TWO_PI) % TWO_PI;
44
+ return Math.round((a / TWO_PI) * 65536) & 0xffff;
45
+ }
46
+ case 'bool':
47
+ return v ? 1 : 0;
48
+ }
49
+ }
50
+ /** Wire integer back to the real value. */
51
+ export function dequantize(f, q) {
52
+ switch (f.kind) {
53
+ case 'uint':
54
+ case 'int':
55
+ case 'bool':
56
+ return q;
57
+ case 'quant': {
58
+ const steps = 2 ** f.bits - 1;
59
+ return f.min + (q / steps) * (f.max - f.min);
60
+ }
61
+ case 'angle16':
62
+ return (q / 65536) * TWO_PI;
63
+ }
64
+ }
65
+ /** Interpolate one field between a and b (real values). */
66
+ export function interpolateField(f, a, b, t) {
67
+ switch (f.kind) {
68
+ case 'bool':
69
+ return t < 1 ? a : b;
70
+ case 'angle16': {
71
+ let d = (b - a) % TWO_PI;
72
+ if (d > Math.PI)
73
+ d -= TWO_PI;
74
+ if (d < -Math.PI)
75
+ d += TWO_PI;
76
+ return a + d * t;
77
+ }
78
+ default:
79
+ if (f.interp === 'step')
80
+ return t < 1 ? a : b;
81
+ return a + (b - a) * t;
82
+ }
83
+ }
84
+ /**
85
+ * Quantize an entity's values. With `only` (the kind's wire fields), the
86
+ * other fields are left 0: they never go on the wire for that kind.
87
+ */
88
+ export function quantizeEntity(schema, e, only) {
89
+ const n = schema.fields.length;
90
+ if (only && only.length < n) {
91
+ const out = new Array(n).fill(0);
92
+ for (const i of only) {
93
+ const f = schema.fields[i];
94
+ if (f === undefined)
95
+ throw new Error('schema field missing');
96
+ out[i] = quantize(f, e.values[i] ?? 0);
97
+ }
98
+ return out;
99
+ }
100
+ const out = new Array(n);
101
+ for (let i = 0; i < n; i++) {
102
+ const f = schema.fields[i];
103
+ if (f === undefined)
104
+ throw new Error('schema field missing');
105
+ out[i] = quantize(f, e.values[i] ?? 0);
106
+ }
107
+ return out;
108
+ }
109
+ export function dequantizeValues(schema, q) {
110
+ const out = new Array(schema.fields.length);
111
+ for (let i = 0; i < schema.fields.length; i++) {
112
+ const f = schema.fields[i];
113
+ if (f === undefined)
114
+ throw new Error('schema field missing');
115
+ out[i] = dequantize(f, q[i] ?? 0);
116
+ }
117
+ return out;
118
+ }
119
+ function clamp(v, lo, hi) {
120
+ return v < lo ? lo : v > hi ? hi : v;
121
+ }
@@ -0,0 +1,134 @@
1
+ import type { BitReader, BitWriter } from './bits.js';
2
+ import type { EntitySchema, EntityState } from './schema.js';
3
+ /** Small integer assigned by the room to each connected player (1..65535). */
4
+ export type PlayerId = number;
5
+ export interface InputCodec<I> {
6
+ write(w: BitWriter, input: I): void;
7
+ read(r: BitReader): I;
8
+ /** The input a player is assumed to send when idle. */
9
+ readonly idle: I;
10
+ }
11
+ /** Something the authoritative sim says a player earned. */
12
+ export interface EarnEvent {
13
+ readonly type: 'earn';
14
+ readonly player: PlayerId;
15
+ /** Earn rule name, as declared in the game's earn rules. */
16
+ readonly rule: string;
17
+ /** Units earned (the referee converts units to token amount per rule). */
18
+ readonly units: number;
19
+ }
20
+ /** The sim started a fresh run in place (game over or victory screen ended). Referee runs restart. */
21
+ export interface RunResetEvent {
22
+ readonly type: 'run.reset';
23
+ }
24
+ /**
25
+ * The sim says a player reached something their profile keeps: an achievement, one of today's goals,
26
+ * or an end-screen title. The room writes it to the player's profile on
27
+ * the server; the browser never grants these.
28
+ */
29
+ export interface ProgressEvent {
30
+ readonly type: 'progress';
31
+ readonly player: PlayerId;
32
+ readonly kind: 'achievement' | 'goal' | 'title';
33
+ /** The declared achievement, mission or title id. */
34
+ readonly id: string;
35
+ }
36
+ export type SimEvent = EarnEvent | RunResetEvent | ProgressEvent;
37
+ /** What a player's profile lets them choose (the room checks it on every input). */
38
+ export interface PlayerRights {
39
+ /** Keeper indices this player may pick; null when the game has no keepers. */
40
+ readonly keepers: readonly number[] | null;
41
+ }
42
+ /** A room-level command from a player (store hooks): a paid revive, a cosmetic. */
43
+ export interface SimCommand {
44
+ readonly name: string;
45
+ readonly value?: number;
46
+ }
47
+ /** A hitscan the sim wants resolved against lag-compensated hitboxes. */
48
+ export interface HitscanRequest {
49
+ readonly shooter: PlayerId;
50
+ readonly ox: number;
51
+ readonly oy: number;
52
+ readonly dx: number;
53
+ readonly dy: number;
54
+ readonly range: number;
55
+ }
56
+ export interface Hitbox {
57
+ readonly entity: number;
58
+ readonly owner: PlayerId;
59
+ readonly x: number;
60
+ readonly y: number;
61
+ readonly r: number;
62
+ }
63
+ export interface SimAdapter<W, I> {
64
+ readonly name: string;
65
+ /** Content version of the logic module (hot swap compares these). */
66
+ readonly version: string;
67
+ readonly tickHz: number;
68
+ readonly schema: EntitySchema;
69
+ readonly input: InputCodec<I>;
70
+ /**
71
+ * Most inputs one player may apply in one tick. Omit for "drain" sims
72
+ * where each input is one tick of motion and catching up after a stall is
73
+ * exact (the movers test sim). Set 1 for physics sims whose state moves
74
+ * without input (engine-sim: `step(world, Map<PlayerId, InputCommand>)`
75
+ * takes exactly one command per player per tick).
76
+ */
77
+ readonly maxInputsPerTick?: number;
78
+ /**
79
+ * A fresh world. The server's is authoritative ('server'); the client's
80
+ * ('client') holds only what it predicts, so it must not spawn world
81
+ * entities of its own.
82
+ */
83
+ createWorld(seed: number, role: 'server' | 'client'): W;
84
+ /** Spawns the player's entity, returns its entity id. */
85
+ addPlayer(world: W, player: PlayerId): number;
86
+ removePlayer(world: W, player: PlayerId): void;
87
+ /** Advances one tick. Inputs per player are applied in order. */
88
+ step(world: W, inputsByPlayer: ReadonlyMap<PlayerId, readonly I[]>): readonly SimEvent[];
89
+ /** Every networked entity, real values in schema field order. */
90
+ snapshot(world: W): readonly EntityState[];
91
+ /**
92
+ * Overwrites world entities with these states (creating any missing).
93
+ * The client uses it to reset its predicted entity to the server's state.
94
+ */
95
+ applySnapshot(world: W, entities: readonly EntityState[]): void;
96
+ /** Lag compensation hook: hitscans requested by this tick's inputs. */
97
+ hitscans?(world: W, player: PlayerId, input: I): HitscanRequest | null;
98
+ /** Current hitboxes, recorded each tick into the rewind buffer. */
99
+ hitboxes?(world: W): readonly Hitbox[];
100
+ /** Resolution of a lag-compensated hitscan; may emit earn events. */
101
+ applyHit?(world: W, shooter: PlayerId, target: Hitbox): readonly SimEvent[];
102
+ /**
103
+ * Run state for the room to broadcast a few times a second (JSON-safe):
104
+ * phase, timer, wave, boss health. Kept out of snapshots on purpose.
105
+ */
106
+ info?(world: W): unknown;
107
+ /**
108
+ * Run state for one player (JSON-safe), sent to that player only, in
109
+ * place of `info`: what only they may see (a hand of cards, their own
110
+ * gold). A sim with hidden information implements this and keeps every
111
+ * secret out of `snapshot` and `info`. The room sends it when it changed.
112
+ */
113
+ infoFor?(world: W, player: PlayerId): unknown;
114
+ /** Ticks between run-state sends (default: the room's, 2 Hz at 30 Hz). Board games send more often. */
115
+ readonly infoEveryTicks?: number;
116
+ /**
117
+ * Rewrites an input so it chooses only what the player's profile allows
118
+ * (a locked keeper becomes the first open one). The room applies it as
119
+ * inputs arrive, before they are recorded, so a replay sees the same.
120
+ */
121
+ restrictInput?(input: I, rights: PlayerRights): I;
122
+ /** Apply a room-level command (already verified by the room). Returns false if it did nothing. */
123
+ command?(world: W, player: PlayerId, cmd: SimCommand): boolean;
124
+ /**
125
+ * A referee run starts for this player now (sign-in, or a new round): the
126
+ * player's earn progress starts over, so a run is paid only for what it
127
+ * did itself (never for counts made as a guest or in an earlier run). The
128
+ * room records it in the run log, so a replay applies it at the same
129
+ * point. Optional: a sim without earn progress leaves it out.
130
+ */
131
+ startRun?(world: W, player: PlayerId): void;
132
+ /** Free native memory (Rapier) when the room disposes. */
133
+ disposeWorld?(world: W): void;
134
+ }
@@ -0,0 +1,22 @@
1
+ // The narrow seam between the netcode and a game's simulation.
2
+ //
3
+ // The room server and the client both drive a SimAdapter: the server steps
4
+ // the authoritative world, the client steps a local copy that holds only its
5
+ // own predicted entity. Until `@netforge/engine-sim` lands on main, the
6
+ // room runs the built-in test sim (`src/sim/movers.ts`) behind this
7
+ // interface; swapping in the shared sim is a change to `src/sim/index.ts`.
8
+ //
9
+ // Contract the netcode relies on (and the engine-sim adapter must keep):
10
+ //
11
+ // 1. Each input command is ONE tick of the owning player's motion. `step`
12
+ // applies a player's queued inputs in order, then runs world systems
13
+ // once. A player with no input this tick does not move by input.
14
+ // 2. A player's own predicted fields depend only on that player's inputs
15
+ // and on static world state, so replaying unacknowledged inputs on top
16
+ // of the server's state reproduces the server exactly (reconciliation
17
+ // converges to zero error). Interactions that break this (being pushed
18
+ // by others) are corrected by the next snapshot and smoothed.
19
+ // 3. `snapshot` values are real numbers; the schema quantizes them. A sim
20
+ // that keeps its state on the quantization grid (fixed-point) gets
21
+ // bit-exact prediction.
22
+ export {};
@@ -0,0 +1,61 @@
1
+ import { type EntitySchema } from './schema.js';
2
+ /** One entity on the wire, values already quantized. */
3
+ export interface QEntity {
4
+ readonly id: number;
5
+ readonly kind: number;
6
+ readonly owner: number;
7
+ readonly q: readonly number[];
8
+ }
9
+ export interface SnapshotHeader {
10
+ /** Server tick this snapshot describes. */
11
+ readonly tick: number;
12
+ /** Tick of the baseline it was encoded against; 0 means full. */
13
+ readonly baselineTick: number;
14
+ /** Highest input sequence number the server has applied for this client. */
15
+ readonly ackSeq: number;
16
+ /** The receiving player's own entity id (0 if none). */
17
+ readonly self: number;
18
+ /**
19
+ * Inputs waiting in the server's queue for this client after this tick
20
+ * (one-input-per-tick sims). The client speeds up or slows its input clock
21
+ * to hold it near a small target. Absent from older encoders.
22
+ */
23
+ readonly inputBuffer?: number;
24
+ }
25
+ export interface DecodedSnapshot {
26
+ readonly header: SnapshotHeader;
27
+ /** The FULL reconstructed entity set for this tick (baseline + delta). */
28
+ readonly entities: ReadonlyMap<number, QEntity>;
29
+ /** Ids that were in the baseline and are gone now. */
30
+ readonly removed: readonly number[];
31
+ }
32
+ export declare class BaselineMissingError extends Error {
33
+ readonly baselineTick: number;
34
+ constructor(baselineTick: number);
35
+ }
36
+ /**
37
+ * Encodes `current` (any order) against `baseline` (null for a full snapshot).
38
+ */
39
+ export declare function encodeSnapshot(schema: EntitySchema, header: SnapshotHeader, current: readonly QEntity[], baseline: ReadonlyMap<number, QEntity> | null): Uint8Array;
40
+ /**
41
+ * Decodes a snapshot. `baselineFor` returns the client's stored entity set
42
+ * for a tick it previously decoded.
43
+ */
44
+ export declare function decodeSnapshot(schema: EntitySchema, bytes: Uint8Array, baselineFor: (tick: number) => ReadonlyMap<number, QEntity> | undefined): DecodedSnapshot;
45
+ /**
46
+ * Per-client history of what was sent, keyed by tick, so the next snapshot
47
+ * can be encoded against whatever the client last acked.
48
+ */
49
+ export declare class SnapshotHistory {
50
+ private readonly capacity;
51
+ private readonly byTick;
52
+ private readonly order;
53
+ constructor(capacity?: number);
54
+ put(tick: number, entities: readonly QEntity[] | ReadonlyMap<number, QEntity>): void;
55
+ get(tick: number): ReadonlyMap<number, QEntity> | undefined;
56
+ /** Drop entries older than `tick` (they can never be a baseline again). */
57
+ pruneBefore(tick: number): void;
58
+ get size(): number;
59
+ }
60
+ /** Field indices carried on the wire for an entity kind (all fields unless the schema narrows that kind). */
61
+ export declare function kindFieldIndices(schema: EntitySchema, kind: number): readonly number[];