@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,379 @@
1
+ /**
2
+ * The racer template's machinery: compile a manifest's tracks into the
3
+ * engine-sim kart sim, drive it with the race runner (race.ts), seat bots
4
+ * that drive the racing line, a client autopilot, and the netcode
5
+ * SimAdapter.
6
+ */
7
+ import { Button, BOT_PLAYER_BASE, KART_STATS, KartFlag, addKart, applyKart, captureKart, createKartWorld, grantKartStat, kartProgress, kartStep, lateralOffset, nearestSample, removeKart, resetKart, setKartTrack, setKartsFrozen, } from "@netforge/engine-sim";
8
+ import { raceInputCodec } from "./platformer.js";
9
+ import { RaceRunner } from "./race.js";
10
+ /** Racer defaults: a 4 minute limit, 60 Hz, five seat bots. */
11
+ export const RACER_DEFAULTS = { roundSeconds: 240, tickRate: 60, bots: 5 };
12
+ /** Compile the racer parts of a manifest. Issues are agent-readable. */
13
+ export function compileRacer(m, issues) {
14
+ const g = m.game;
15
+ if (!g.tracks || g.tracks.length === 0)
16
+ issues.push("a racer game sets tracks: [{ points, width, laps, ... }] (the circuits)");
17
+ if (m.weapons.size > 0)
18
+ issues.push("weapon(...) is not part of the racer template: boost pads, coins and drifts make the race");
19
+ if (m.director)
20
+ issues.push("director(...) is not part of the racer template");
21
+ if (g.arena || g.field || g.levels)
22
+ issues.push("arena, field and levels belong to other templates; a racer declares tracks");
23
+ if (g.players.loadout)
24
+ issues.push("players.loadout is for the arena template");
25
+ if (m.keepers.size > 0)
26
+ issues.push("keeper(...) is for the survivor template");
27
+ for (const [id, p] of m.prefabs)
28
+ if ((p.kind ?? "enemy") === "enemy" && !p.prop)
29
+ issues.push(`prefab "${id}" is an enemy; the racer template has no enemies`);
30
+ const upgradeStat = [];
31
+ const statSteps = KART_STATS.map(() => 0);
32
+ for (const [id, u] of m.upgrades) {
33
+ const i = KART_STATS.indexOf(u.stat);
34
+ if (i < 0) {
35
+ issues.push(`upgrade "${id}" raises ${u.stat}; racer upgrades raise one of: ${KART_STATS.join(", ")}`);
36
+ upgradeStat.push("speed");
37
+ continue;
38
+ }
39
+ if (u.stat === "speed" && u.step > 0.2)
40
+ issues.push(`upgrade "${id}": a speed step above 0.2 (+20% a level) breaks the races`);
41
+ statSteps[i] = u.step;
42
+ upgradeStat.push(u.stat);
43
+ }
44
+ const names = [];
45
+ const tracks = (g.tracks ?? []).map((t, ti) => {
46
+ names.push(t.name ?? `Track ${ti + 1}`);
47
+ const where = `tracks[${ti}]`;
48
+ // Neighbouring control points closer than the width fold the road over itself.
49
+ t.points.forEach((p, i) => {
50
+ const q = t.points[(i + 1) % t.points.length];
51
+ if (Math.hypot(q[0] - p[0], q[1] - p[1]) < 6)
52
+ issues.push(`${where}.points[${i}] is within 6 m of the next point: spread the control points out`);
53
+ });
54
+ let len = 0;
55
+ for (let i = 0; i < t.points.length; i++) {
56
+ const p = t.points[i];
57
+ const q = t.points[(i + 1) % t.points.length];
58
+ len += Math.hypot(q[0] - p[0], q[1] - p[1]);
59
+ }
60
+ if (len < 200)
61
+ issues.push(`${where} is ${Math.round(len)} m round; a lap needs at least 200 m`);
62
+ return { points: t.points, width: t.width, laps: t.laps, boosts: t.boosts, coins: t.coins, verge: t.verge };
63
+ });
64
+ const kt = g.kart ?? {};
65
+ const tuning = Object.fromEntries(Object.entries(kt).filter(([, v]) => v !== undefined));
66
+ return { tracks, names, tuning, statSteps, upgradeStat };
67
+ }
68
+ // ---------------------------------------------------------------- bots
69
+ /**
70
+ * What a kart bot does: steer at a point on the racing line ahead (further
71
+ * at speed), lift off for tight bends, and drift through long ones at
72
+ * higher skill. `skill` 0..1 shades its top speed and its line.
73
+ */
74
+ export function kartBotInput(w, k, clock, skill, lane = 0, mem) {
75
+ const idle = { seq: 0, tick: clock, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
76
+ if ((k.flags & (KartFlag.Frozen | KartFlag.Finished)) !== 0)
77
+ return idle;
78
+ const t = w.track;
79
+ const speed = Math.hypot(k.vx, k.vz);
80
+ const ahead = Math.round((8 + speed * 0.55) / 2);
81
+ const i = (k.seg + ahead) % t.count;
82
+ const far = (k.seg + ahead * 2) % t.count;
83
+ const nx = -(t.tz[i] ?? 0);
84
+ const nz = t.tx[i] ?? 0;
85
+ // Pass a kart close ahead on our line: move over to the side it is not on.
86
+ const fx = Math.cos(k.yaw);
87
+ const fz = Math.sin(k.yaw);
88
+ let pass = 0;
89
+ for (const o of w.karts.values()) {
90
+ if (o === k)
91
+ continue;
92
+ const dx = o.x - k.x;
93
+ const dz = o.z - k.z;
94
+ const ahead = dx * fx + dz * fz;
95
+ const side = -dx * fz + dz * fx;
96
+ if (ahead > 0 && ahead < 10 && Math.abs(side) < 2.2)
97
+ pass = side >= 0 ? -1 : 1;
98
+ }
99
+ const off = Math.max(-0.8, Math.min(0.8, lane * 0.45 + pass * 0.55)) * t.half;
100
+ const tx = (t.x[i] ?? 0) + nx * off;
101
+ const tz = (t.z[i] ?? 0) + nz * off;
102
+ const want = Math.atan2(tz - k.z, tx - k.x);
103
+ let d = want - k.yaw;
104
+ while (d > Math.PI)
105
+ d -= Math.PI * 2;
106
+ while (d < -Math.PI)
107
+ d += Math.PI * 2;
108
+ const steer = Math.max(-1, Math.min(1, d * 2.2));
109
+ // How sharp the road bends ahead: the angle between here and further on.
110
+ const bend = Math.abs(Math.atan2((t.tx[i] ?? 0) * (t.tz[far] ?? 0) - (t.tz[i] ?? 0) * (t.tx[far] ?? 0), (t.tx[i] ?? 0) * (t.tx[far] ?? 0) + (t.tz[i] ?? 0) * (t.tz[far] ?? 0)));
111
+ const cap = w.tuning.topSpeed * (0.78 + 0.2 * skill);
112
+ let throttle = speed < cap ? 1 : 0;
113
+ if (bend > 0.9 && speed > cap * 0.7)
114
+ throttle = -0.3;
115
+ let buttons = 0;
116
+ const drifting = (k.flags & KartFlag.Drifting) !== 0;
117
+ // Drift into a long bend and hold it through (a held drift charges the mini-turbo); let go as the road straightens.
118
+ if (mem && skill > 0.55) {
119
+ if (!drifting && bend > 0.5 && bend < 1.1 && speed > 14 && Math.abs(steer) > 0.35)
120
+ mem.driftUntil = clock + Math.round((0.6 + skill * 0.3) * w.tickRate);
121
+ // Let go early when the drift swings the nose past the line (the heading error flips against the drift).
122
+ const dir = (k.flags & KartFlag.DriftRight) !== 0 ? 1 : -1;
123
+ const overturn = drifting && d * dir < -0.25;
124
+ if (!overturn && clock < mem.driftUntil)
125
+ buttons |= Button.Sprint;
126
+ }
127
+ // A stuck kart (a wall nose-in) backs off and turns.
128
+ if (speed < 2 && clock % 120 > 60)
129
+ return { ...idle, moveX: -steer, moveZ: -1 };
130
+ return { seq: 0, tick: clock, moveX: steer, moveZ: throttle, yaw: 0, buttons };
131
+ }
132
+ // ---------------------------------------------------------------- core
133
+ export class KartCore {
134
+ compiled;
135
+ tickRate;
136
+ world;
137
+ nextSlot = 0;
138
+ memory = new Map();
139
+ constructor(compiled, tickRate, authority = true) {
140
+ this.compiled = compiled;
141
+ this.tickRate = tickRate;
142
+ this.world = createKartWorld({ tickRate, tracks: compiled.tracks, tuning: compiled.tuning, statSteps: compiled.statSteps, authority });
143
+ }
144
+ ids() {
145
+ return [...this.world.karts.keys()].sort((a, b) => a - b);
146
+ }
147
+ /** The lowest grid slot nobody holds. */
148
+ freeSlot() {
149
+ const used = new Set([...this.world.karts.values()].map((k) => k.slot));
150
+ let s = 0;
151
+ while (used.has(s))
152
+ s++;
153
+ return s;
154
+ }
155
+ add(id, bot, frozen) {
156
+ if (this.world.karts.has(id))
157
+ return;
158
+ addKart(this.world, id, { bot, frozen, slot: this.freeSlot() });
159
+ this.nextSlot++;
160
+ }
161
+ remove(id) {
162
+ removeKart(this.world, id);
163
+ }
164
+ reset(course, frozen) {
165
+ setKartTrack(this.world, course);
166
+ this.world.tick = 0;
167
+ // People take the front of the grid, seat bots fill in behind them.
168
+ const order = [...this.world.karts.values()].sort((a, b) => (a.id >= BOT_PLAYER_BASE ? 1 : 0) - (b.id >= BOT_PLAYER_BASE ? 1 : 0) || a.id - b.id);
169
+ order.forEach((k, slot) => {
170
+ k.slot = slot;
171
+ k.stats = 0;
172
+ resetKart(this.world, k, frozen);
173
+ });
174
+ }
175
+ setFrozen(frozen) {
176
+ setKartsFrozen(this.world, frozen);
177
+ }
178
+ finished(id) {
179
+ return ((this.world.karts.get(id)?.flags ?? 0) & KartFlag.Finished) !== 0;
180
+ }
181
+ progress(id) {
182
+ const k = this.world.karts.get(id);
183
+ return k ? Math.min(1, kartProgress(this.world, k) / this.world.track.def.laps) : 0;
184
+ }
185
+ position(id) {
186
+ const k = this.world.karts.get(id);
187
+ return k ? [k.x, 0, k.z] : [0, 0, 0];
188
+ }
189
+ step(inputs) {
190
+ const out = [];
191
+ for (const e of kartStep(this.world, inputs)) {
192
+ if (e.type === "coin")
193
+ out.push({ name: "coin.collected", player: e.kart });
194
+ else if (e.type === "lap")
195
+ out.push({ name: "lap.completed", player: e.kart });
196
+ else if (e.type === "finish")
197
+ out.push({ name: "player.finished", player: e.kart });
198
+ else if (e.type === "turbo")
199
+ out.push({ name: e.tier >= 3 ? "drift.ultra" : "drift.boosted", player: e.kart });
200
+ else if (e.type === "wall")
201
+ out.push({ name: "kart.bumped", player: e.kart });
202
+ }
203
+ return out;
204
+ }
205
+ botInput(id, clock, skill) {
206
+ const k = this.world.karts.get(id);
207
+ if (!k)
208
+ return { seq: 0, tick: clock, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
209
+ const lane = ((id * 7) % 5) / 2 - 1;
210
+ let mem = this.memory.get(id);
211
+ if (!mem) {
212
+ mem = { driftUntil: 0 };
213
+ this.memory.set(id, mem);
214
+ }
215
+ return kartBotInput(this.world, k, clock, skill, lane * 0.6, mem);
216
+ }
217
+ grant(id, upgrade) {
218
+ const stat = this.compiled.upgradeStat[upgrade];
219
+ return stat ? grantKartStat(this.world, id, stat) : false;
220
+ }
221
+ setCosmetic(id, cosmetic) {
222
+ const k = this.world.karts.get(id);
223
+ if (k)
224
+ k.cosmetic = cosmetic & 0xff;
225
+ }
226
+ }
227
+ // ---------------------------------------------------------------- netcode
228
+ /** Field indices of the racer wire schema. */
229
+ export const KF = { x: 0, z: 1, yaw: 2, vx: 3, vz: 4, flags: 5, timers: 6, track: 7, coinsLo: 8, coinsHi: 9, finishTick: 10, stats: 11, cosmetic: 12, clock: 13 };
230
+ export const kartSchema = {
231
+ fields: [
232
+ { name: "x", kind: "int", bits: 22, deltaBits: 12 },
233
+ { name: "z", kind: "int", bits: 22, deltaBits: 12 },
234
+ { name: "yaw", kind: "uint", bits: 16, interp: "step" },
235
+ { name: "vx", kind: "int", bits: 18, deltaBits: 12 },
236
+ { name: "vz", kind: "int", bits: 18, deltaBits: 12 },
237
+ { name: "flags", kind: "uint", bits: 8, interp: "step" },
238
+ { name: "timers", kind: "uint", bits: 25, interp: "step" },
239
+ { name: "track", kind: "uint", bits: 28, interp: "step" },
240
+ { name: "coinsLo", kind: "uint", bits: 31, interp: "step" },
241
+ { name: "coinsHi", kind: "uint", bits: 31, interp: "step" },
242
+ { name: "finishTick", kind: "uint", bits: 20, interp: "step" },
243
+ { name: "stats", kind: "uint", bits: 20, interp: "step" },
244
+ { name: "cosmetic", kind: "uint", bits: 16, interp: "step" },
245
+ { name: "clock", kind: "uint", bits: 20, interp: "step", deltaBits: 6 },
246
+ ],
247
+ xField: KF.x,
248
+ yField: KF.z,
249
+ unitsPerWorld: 1024,
250
+ };
251
+ function toValues(s) {
252
+ return [s.x, s.z, s.yaw, s.vx, s.vz, s.flags, s.timers, s.track, s.coinsLo, s.coinsHi, s.finishTick, s.stats, s.cosmetic, s.clock];
253
+ }
254
+ export function kartFromValues(e) {
255
+ const v = (i) => Math.round(e.values[i] ?? 0);
256
+ return {
257
+ id: e.id,
258
+ x: v(KF.x),
259
+ z: v(KF.z),
260
+ yaw: v(KF.yaw) & 0xffff,
261
+ vx: v(KF.vx),
262
+ vz: v(KF.vz),
263
+ flags: v(KF.flags),
264
+ timers: v(KF.timers) >>> 0,
265
+ track: v(KF.track) >>> 0,
266
+ coinsLo: v(KF.coinsLo) >>> 0,
267
+ coinsHi: v(KF.coinsHi) >>> 0,
268
+ finishTick: v(KF.finishTick),
269
+ stats: v(KF.stats) >>> 0,
270
+ cosmetic: v(KF.cosmetic),
271
+ clock: v(KF.clock),
272
+ };
273
+ }
274
+ /** The racer's SimAdapter: the room runs the race; a client predicts only its own kart (no bumps). */
275
+ export function kartAdapter(o) {
276
+ const upgradeIds = [...o.manifest.upgrades.keys()];
277
+ const gear = new Set([...o.manifest.skus.values()].flatMap((s) => (s.grants && "gear" in s.grants ? [s.grants.gear] : [])));
278
+ return {
279
+ name: o.name,
280
+ version: o.version,
281
+ tickHz: o.tickRate,
282
+ schema: kartSchema,
283
+ input: raceInputCodec,
284
+ maxInputsPerTick: 1,
285
+ createWorld(seed, role) {
286
+ const core = new KartCore(o.compiled, o.tickRate, role === "server");
287
+ const runner = role === "server" ? new RaceRunner(o.manifest, core, { bots: o.bots, roundSeconds: o.roundSeconds, courses: o.compiled.tracks.length, seed, ...o.runner }) : null;
288
+ return { role, core, runner };
289
+ },
290
+ addPlayer(w, player) {
291
+ if (w.runner)
292
+ w.runner.joined(player);
293
+ else
294
+ w.core.add(player, false, false);
295
+ return player;
296
+ },
297
+ removePlayer(w, player) {
298
+ if (w.runner)
299
+ w.runner.left(player);
300
+ else
301
+ w.core.remove(player);
302
+ },
303
+ step(w, inputsByPlayer) {
304
+ const inputs = new Map();
305
+ for (const [p, list] of inputsByPlayer) {
306
+ const c = list[0];
307
+ if (c)
308
+ inputs.set(p, c);
309
+ }
310
+ if (!w.runner) {
311
+ w.core.step(inputs);
312
+ return [];
313
+ }
314
+ w.runner.beforeStep(inputs);
315
+ const r = w.runner.afterStep(w.core.step(inputs));
316
+ const out = [];
317
+ for (const a of r.awards)
318
+ out.push({ type: "earn", player: a.player, rule: a.rule, units: a.units });
319
+ for (const p of w.runner.takeProgress())
320
+ out.push({ type: "progress", player: p.player, kind: p.kind, id: p.id });
321
+ if (r.reset)
322
+ out.push({ type: "run.reset" });
323
+ return out;
324
+ },
325
+ snapshot(w) {
326
+ return w.core.ids().map((id) => ({ id, kind: 1, owner: id, values: toValues(captureKart(w.core.world, w.core.world.karts.get(id))) }));
327
+ },
328
+ applySnapshot(w, entities) {
329
+ for (const e of entities)
330
+ applyKart(w.core.world, kartFromValues(e));
331
+ },
332
+ info(w) {
333
+ return w.runner ? { ...w.runner.info(), level: o.compiled.names[w.runner.courseNow] ?? null, laps: w.core.world.track.def.laps } : null;
334
+ },
335
+ startRun(w, player) {
336
+ w.runner?.startRun(player);
337
+ },
338
+ command(w, player, cmd) {
339
+ if (cmd.name === "cosmetic") {
340
+ w.core.setCosmetic(player, cmd.value ?? 0);
341
+ return true;
342
+ }
343
+ const up = upgradeIds.indexOf(cmd.name);
344
+ if (up < 0)
345
+ return false;
346
+ if (!o.manifest.skus.has(cmd.name) && !gear.has(cmd.name))
347
+ return false;
348
+ const stat = o.compiled.upgradeStat[up];
349
+ const def = o.manifest.upgrades.get(cmd.name);
350
+ const k = w.core.world.karts.get(player);
351
+ if (!stat || !def || !k)
352
+ return false;
353
+ const lvl = (k.stats >>> (KART_STATS.indexOf(stat) * 4)) & 0xf;
354
+ if (lvl >= def.maxLevel)
355
+ return false;
356
+ return w.core.grant(player, up);
357
+ },
358
+ disposeWorld() { },
359
+ };
360
+ }
361
+ /** A client autopilot: the seat bot's driving on our newest predicted kart. */
362
+ export function kartAutopilot(compiled, tickRate) {
363
+ const core = new KartCore(compiled, tickRate, false);
364
+ const mem = { driftUntil: 0 };
365
+ return (self, tick, skill = 1, others = []) => {
366
+ if (!self)
367
+ return { seq: 0, tick, moveX: 0, moveZ: 0, yaw: 0, buttons: 0 };
368
+ // The karts we can see, so we pass them instead of queueing behind.
369
+ for (const id of [...core.world.karts.keys()])
370
+ if (id !== self.id && !others.some((o) => o.id === id))
371
+ core.world.karts.delete(id);
372
+ for (const o of others)
373
+ if (o.id !== self.id)
374
+ applyKart(core.world, kartFromValues(o));
375
+ const k = applyKart(core.world, kartFromValues(self));
376
+ return kartBotInput(core.world, k, tick, skill, 0, mem);
377
+ };
378
+ }
379
+ export { nearestSample, lateralOffset };
@@ -0,0 +1,286 @@
1
+ import type { HitSource, InputCommand, SimEvent, SimWorld } from "@netforge/engine-sim";
2
+ import type { CompiledGame } from "./compile.js";
3
+ import type { EarnAward, GameEvent } from "./earn.js";
4
+ import type { RunProgress } from "./profile.js";
5
+ export type RunPhase = "playing" | "won" | "lost";
6
+ export interface RunnerOptions {
7
+ /**
8
+ * Measurement only: keep this many enemies alive (topped up on rings,
9
+ * up to 64 a tick) on top of the game's own waves. Default 0 (off).
10
+ */
11
+ readonly holdEnemies?: number;
12
+ /** Measurement only: downed players stand straight back up. */
13
+ readonly godMode?: boolean;
14
+ /** Measurement only: the held crowd cycles through every non-boss enemy type (all behaviours on), not type 0. */
15
+ readonly holdMix?: boolean;
16
+ /** Seconds every player may be down before the run is lost. Default 5. */
17
+ readonly loseGraceSeconds?: number;
18
+ /** Seconds the end screen shows before a fresh run. Default 10 (arena 8). */
19
+ readonly endSeconds?: number;
20
+ }
21
+ export interface RunPlayerInfo {
22
+ readonly id: number;
23
+ readonly level: number;
24
+ readonly kills: number;
25
+ readonly downed: boolean;
26
+ readonly dead: boolean;
27
+ /** Times this player died this run. */
28
+ readonly deaths: number;
29
+ /** A server bot filling a seat. */
30
+ readonly bot: boolean;
31
+ /** Keeper id (null when the game declares none). */
32
+ readonly keeper: string | null;
33
+ /** A revive buff is running. */
34
+ readonly buffed: boolean;
35
+ /** This run's counts of the events the game's missions and achievements key on. */
36
+ readonly counts: Readonly<Record<string, number>>;
37
+ /** Achievement ids reached this run. */
38
+ readonly achievements: readonly string[];
39
+ /** Voted to play again (end screen). */
40
+ readonly ready: boolean;
41
+ }
42
+ /** One player's line on the end screen. Everything is data: names are declared ids, titles are declared text. */
43
+ export interface RunSummaryPlayer {
44
+ readonly id: number;
45
+ readonly keeper: string | null;
46
+ /** Weapons with their level and the damage each dealt this run. */
47
+ readonly weapons: readonly {
48
+ readonly id: string;
49
+ readonly level: number;
50
+ readonly damage: number;
51
+ }[];
52
+ readonly passives: readonly {
53
+ readonly id: string;
54
+ readonly level: number;
55
+ }[];
56
+ readonly level: number;
57
+ readonly kills: number;
58
+ readonly damage: number;
59
+ readonly revives: number;
60
+ readonly downs: number;
61
+ readonly pickups: number;
62
+ /** What put this player down last (enemy prefab id and how), or null. */
63
+ readonly downedBy: {
64
+ readonly prefab: string;
65
+ readonly source: HitSource;
66
+ } | null;
67
+ /** Coins earned this run by earn rule (units of each rule's pay). */
68
+ readonly coins: readonly {
69
+ readonly rule: string;
70
+ readonly units: number;
71
+ }[];
72
+ /** Declared titles this player took. */
73
+ readonly titles: readonly string[];
74
+ readonly achievements: readonly string[];
75
+ }
76
+ export interface RunSummary {
77
+ /** Most enemies alive at once this run. */
78
+ readonly peak: number;
79
+ readonly seconds: number;
80
+ readonly players: readonly RunSummaryPlayer[];
81
+ }
82
+ /** Arena: one kill, newest last. `ago` is seconds since it happened. */
83
+ export interface KillFeedEntry {
84
+ readonly by: number;
85
+ readonly victim: number;
86
+ readonly ago: number;
87
+ }
88
+ /** What the room broadcasts about the run (a few times a second, a few hundred bytes). */
89
+ export interface RunInfo {
90
+ readonly run: number;
91
+ readonly phase: RunPhase;
92
+ /** Seconds since the run started. */
93
+ readonly seconds: number;
94
+ /** 1 + waves cleared (a dungeon's rooms cleared count as waves). */
95
+ readonly wave: number;
96
+ readonly kills: number;
97
+ readonly enemies: number;
98
+ /** The first living boss (kept for older clients; see `bosses`). */
99
+ readonly boss: {
100
+ readonly prefab: string;
101
+ readonly health: number;
102
+ readonly max: number;
103
+ } | null;
104
+ /** Every living boss, oldest first: each gets a HUD bar, and an edge arrow when off screen (x, z in m). */
105
+ readonly bosses: readonly {
106
+ readonly prefab: string;
107
+ readonly health: number;
108
+ readonly max: number;
109
+ readonly x: number;
110
+ readonly z: number;
111
+ }[];
112
+ /** True while the sim is paused for a solo player's upgrade cards. */
113
+ readonly paused: boolean;
114
+ /** The latest announcement (id increases with each new one), for the HUD's banner. */
115
+ readonly announce: {
116
+ readonly id: number;
117
+ readonly text: string;
118
+ readonly tone: "info" | "danger" | "reward";
119
+ } | null;
120
+ readonly players: readonly RunPlayerInfo[];
121
+ /** Seconds until a fresh run while the end screen shows. */
122
+ readonly nextRunIn: number | null;
123
+ /** Arena: the round clock and frag limit; null for survivor runs. */
124
+ readonly round: {
125
+ readonly seconds: number;
126
+ readonly left: number;
127
+ readonly frags: number | null;
128
+ } | null;
129
+ /** Arena: the last few kills. */
130
+ readonly feed: readonly KillFeedEntry[];
131
+ /** The HUD countdown a system set with `upcoming` ("THE BELL WARDEN" in 42 s), or null. */
132
+ readonly next: {
133
+ readonly label: string;
134
+ readonly in: number;
135
+ } | null;
136
+ /** The end screen's data while the run is over (null while playing). */
137
+ readonly summary: RunSummary | null;
138
+ }
139
+ export interface RunnerStep {
140
+ readonly events: readonly GameEvent[];
141
+ readonly awards: readonly EarnAward[];
142
+ /** True when this tick started a fresh run (players reset, counts cleared). */
143
+ readonly reset: boolean;
144
+ }
145
+ /**
146
+ * How sharp seat bots play against a person, from that person's record this
147
+ * round: a newcomer (no kills yet) faces loose bots, and they sharpen as
148
+ * the person wins (GAME-DESIGN.md "Bots that ramp").
149
+ */
150
+ export declare function seatBotSkillVs(kills: number, deaths: number): number;
151
+ export declare class GameRunner {
152
+ readonly compiled: CompiledGame;
153
+ readonly world: SimWorld;
154
+ private readonly opts;
155
+ private scheduler;
156
+ private runTick;
157
+ private runN;
158
+ private phase;
159
+ private phaseTicks;
160
+ private allDownTicks;
161
+ private pending;
162
+ /** Players whose referee run started after this world (round) began: world and fight progress never pays them (record). */
163
+ private readonly lateStart;
164
+ /** Systems are handling an unpaid event: what they emit now is unpaid too. */
165
+ private dispatchingUnpaid;
166
+ private readonly earn;
167
+ private readonly kills;
168
+ private readonly deaths;
169
+ private readonly emitted;
170
+ private teamKills;
171
+ private bosses;
172
+ private feed;
173
+ private readonly endTicks;
174
+ private readonly graceTicks;
175
+ private readonly arena;
176
+ private readonly roundTicks;
177
+ private readonly botMemory;
178
+ private readonly loadout;
179
+ private clock;
180
+ private director;
181
+ private holdNext;
182
+ private announcement;
183
+ private announceN;
184
+ /** Run tick when an XP boost ends (0: none). */
185
+ private boostEnds;
186
+ /** The sim was paused for a solo pick at the start of this step: the run clock stands still. */
187
+ private pausedStep;
188
+ private readonly ready;
189
+ private upcomingAt;
190
+ private readonly achievements;
191
+ private readonly missions;
192
+ /** Event names the missions and achievements count (their per-player counts go to the HUD). */
193
+ private readonly tracked;
194
+ /** Per enemy type: the "<prefab>.killed" event to raise, when something counts it. */
195
+ private readonly killNames;
196
+ private readonly unlocked;
197
+ /** Achievements, goals and titles reached since the room last took them (it writes them to profiles). */
198
+ private reached;
199
+ private readonly revives;
200
+ private readonly downs;
201
+ private readonly pickups;
202
+ private readonly lastHit;
203
+ private readonly downedBy;
204
+ private readonly coins;
205
+ private peak;
206
+ private viewTick;
207
+ private view;
208
+ constructor(compiled: CompiledGame, world: SimWorld, opts?: RunnerOptions);
209
+ private newRun;
210
+ private radiusOf;
211
+ /** Bosses are tougher with more players: health x (1 - k + k x players), k = players.bossHealthPerPlayer (0.55). */
212
+ private bossScale;
213
+ private spawnEdge;
214
+ private playerViews;
215
+ private presentPlayers;
216
+ private spawn;
217
+ private finish;
218
+ /**
219
+ * Profile progress since the last call: achievements and mission goals
220
+ * reached (humans only) and titles taken at the end of a run. The room
221
+ * server writes these to the players' profiles; the browser never does.
222
+ */
223
+ takeProgress(): RunProgress[];
224
+ /**
225
+ * Arena: the round is over. "round.top" credits the top fragger(s) (with at
226
+ * least one kill), "round.finished" everyone present. Topping a board of
227
+ * seat bots pays nothing: "round.top" is unpaid unless another person was
228
+ * in the round at the end.
229
+ */
230
+ private finishRound;
231
+ get phaseNow(): RunPhase;
232
+ get runNumber(): number;
233
+ /**
234
+ * Call once before each sim step: fills empty arena seats with bots,
235
+ * runs the systems' timers (waves) and the round clock while playing, and,
236
+ * given the step's input map, adds every bot's input to it.
237
+ */
238
+ beforeStep(inputs?: Map<number, InputCommand>): void;
239
+ /** Every human present voted to play again (bots always agree), after the end screen had a moment. */
240
+ private allReady;
241
+ /** A player joining a run in progress starts players.lateJoinBehind levels below the team's average, cards waiting. */
242
+ private catchUp;
243
+ private prefabOfEnemy;
244
+ /** Seats bots fill up to, with at least one human present; bots leave as humans join. One change a tick. */
245
+ private fillSeats;
246
+ /**
247
+ * Arena: what a bot (or a playtest player) does this tick, from the
248
+ * present state. Deterministic, so a room's run replays exactly.
249
+ */
250
+ autopilot(id: number): InputCommand;
251
+ private hold;
252
+ /** Earn credits humans only: bots fill seats, they never earn, and an unpaid event (a bot's death, a board of bots topped) pays no one. */
253
+ private record;
254
+ /** Call once after each sim step with its events. */
255
+ afterStep(simEvents: readonly SimEvent[]): RunnerStep;
256
+ /** Achievements and missions count the run's events (humans only, like earn). */
257
+ private progress;
258
+ private countsOf;
259
+ private keeperId;
260
+ /** The end screen: each player's build, damage per weapon, what downed them, coins and titles. */
261
+ summary(): RunSummary;
262
+ /** Each human's end-screen row, without titles. */
263
+ private summaryRows;
264
+ /** Who took each declared title (by id): the most (or, with 2+ players, the least) of its stat. */
265
+ private titleIds;
266
+ info(): RunInfo;
267
+ private bossHealth;
268
+ /** Boss NetIds alive now. */
269
+ bossIds(): number[];
270
+ /**
271
+ * A referee run starts for this player (the room's sign-in or round start):
272
+ * their earn counts and once-per-run payouts start over, and systems hear
273
+ * "run.started" (unpaid) so their own counters can start over too (a kill
274
+ * streak). A run started after this world began is never paid world or
275
+ * fight progress (boss.killed, wave.cleared, round.top, run.won, system
276
+ * emits: every event that is not the player's own act) until the next
277
+ * world, since that progress predates the run.
278
+ */
279
+ startRun(player: number): void;
280
+ /** Earn counts for one player this run (HUD and tests). */
281
+ count(player: number, event: string): number;
282
+ /** Total number of times an event has fired this run. */
283
+ emittedCount(event: string): number;
284
+ /** Kills by one player this run (arena: opponents). */
285
+ killsOf(player: number): number;
286
+ }