@netforge/engine 0.0.0-stage → 0.2.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 (245) hide show
  1. package/GAME-DESIGN.md +195 -0
  2. package/LICENSE +21 -0
  3. package/NOTICE +30 -0
  4. package/README.md +100 -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 +215 -0
  76. package/dist/net/protocol.js +10 -0
  77. package/dist/net/room-client.d.ts +65 -0
  78. package/dist/net/room-client.js +156 -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-full.txt +460 -0
  244. package/llms.txt +183 -0
  245. package/package.json +104 -3
@@ -0,0 +1,386 @@
1
+ import { BOT_PLAYER_BASE, Button } from "@netforge/engine-sim";
2
+ import { SystemScheduler } from "../api/scheduler.js";
3
+ import { EarnTracker } from "./earn.js";
4
+ const isBot = (id) => id >= BOT_PLAYER_BASE;
5
+ const MIN_END_SECONDS = 2;
6
+ /** Seat bots run looser than the playtest bot; the newest person in a round faces looser bots still. */
7
+ const SEAT_BOT_SKILL = 0.72;
8
+ export class RaceRunner {
9
+ manifest;
10
+ core;
11
+ opts;
12
+ scheduler;
13
+ runN = 0;
14
+ runTick = 0;
15
+ clock = 0;
16
+ phase = "countdown";
17
+ phaseTicks = 0;
18
+ course = 0;
19
+ pending = [];
20
+ dispatchingUnpaid = false;
21
+ lateStart = new Set();
22
+ earn;
23
+ achievements;
24
+ missions;
25
+ tracked;
26
+ reached = [];
27
+ places = new Map();
28
+ times = new Map();
29
+ coins = new Map();
30
+ ready = new Set();
31
+ announcement = null;
32
+ announceN = 0;
33
+ upcomingAt = null;
34
+ allDoneTick = -1;
35
+ /** Run tick the round closes at once someone has finished (-1: nobody yet). */
36
+ closeAt = -1;
37
+ windowTicks;
38
+ rng;
39
+ countdownTicks;
40
+ endTicks;
41
+ graceTicks;
42
+ limitTicks;
43
+ constructor(manifest, core, opts) {
44
+ this.manifest = manifest;
45
+ this.core = core;
46
+ this.opts = opts;
47
+ const rate = core.tickRate;
48
+ this.earn = new EarnTracker(manifest.earnRules);
49
+ const asRules = (m) => new Map([...m].map(([id, d]) => [id, { when: d.when, n: d.n, pay: 0, oncePer: "run" }]));
50
+ this.achievements = new EarnTracker(asRules(manifest.achievements));
51
+ this.missions = new EarnTracker(asRules(manifest.missions));
52
+ this.tracked = [...new Set([...manifest.achievements.values(), ...manifest.missions.values()].map((d) => d.when))];
53
+ this.countdownTicks = Math.round((opts.countdownSeconds ?? 3) * rate);
54
+ this.endTicks = Math.round((opts.endSeconds ?? 8) * rate);
55
+ this.graceTicks = Math.round((opts.graceSeconds ?? 4) * rate);
56
+ this.limitTicks = opts.roundSeconds ? Math.round(opts.roundSeconds * rate) : 0;
57
+ this.windowTicks = Math.round((opts.finishWindowSeconds ?? 25) * rate);
58
+ this.rng = (opts.seed >>> 0) || 0x9e3779b9;
59
+ // The first course comes from the world's seed: a room rebuilds its world (a fresh seed) between rounds,
60
+ // so every round draws its course; a world that runs many rounds (the harness) takes them in order.
61
+ this.course = (opts.seed >>> 0) % Math.max(1, opts.courses);
62
+ this.newRun(true);
63
+ }
64
+ random() {
65
+ let s = this.rng;
66
+ s ^= s << 13;
67
+ s ^= s >>> 17;
68
+ s ^= s << 5;
69
+ this.rng = s >>> 0;
70
+ return this.rng / 4294967296;
71
+ }
72
+ newRun(first = false) {
73
+ this.runN++;
74
+ this.runTick = 0;
75
+ this.phase = "countdown";
76
+ this.phaseTicks = 0;
77
+ if (!first)
78
+ this.course = (this.course + 1) % Math.max(1, this.opts.courses);
79
+ this.places.clear();
80
+ this.times.clear();
81
+ this.coins.clear();
82
+ this.ready.clear();
83
+ this.announcement = null;
84
+ this.upcomingAt = null;
85
+ this.allDoneTick = -1;
86
+ this.closeAt = -1;
87
+ this.earn.resetRun();
88
+ this.achievements.resetRun();
89
+ this.missions.resetRun();
90
+ this.lateStart.clear();
91
+ if (this.reached.length > 1024)
92
+ this.reached = [];
93
+ this.core.reset(this.course, true);
94
+ this.scheduler = new SystemScheduler(this.manifest, {
95
+ tickRate: this.core.tickRate,
96
+ tick: () => this.runTick,
97
+ players: () => this.playerViews(),
98
+ spawn: () => {
99
+ throw new Error("spawn is not part of the race templates: build the course in game() instead");
100
+ },
101
+ random: () => this.random(),
102
+ emit: (event, players) => {
103
+ const ev = { name: event, players: players ? [...players] : this.core.ids() };
104
+ this.pending.push(this.dispatchingUnpaid ? { ...ev, unpaid: true } : ev);
105
+ },
106
+ end: () => this.closeRound(),
107
+ announce: (text, tone) => {
108
+ this.announcement = { id: ++this.announceN, text, tone };
109
+ },
110
+ upcoming: (label, seconds) => {
111
+ this.upcomingAt = { label, tick: this.runTick + Math.round(seconds * this.core.tickRate) };
112
+ },
113
+ });
114
+ this.scheduler.start();
115
+ }
116
+ playerViews() {
117
+ return this.core.ids().map((id) => ({
118
+ id,
119
+ position: this.core.position(id),
120
+ health: 1,
121
+ alive: !this.core.finished(id),
122
+ kills: 0,
123
+ deaths: 0,
124
+ bot: isBot(id),
125
+ }));
126
+ }
127
+ get phaseNow() {
128
+ return this.phase;
129
+ }
130
+ get runNumber() {
131
+ return this.runN;
132
+ }
133
+ get courseNow() {
134
+ return this.course;
135
+ }
136
+ placeOf(id) {
137
+ return this.places.get(id) ?? null;
138
+ }
139
+ /** Seats bots fill up to, with at least one person present; one change a tick. */
140
+ fillSeats() {
141
+ const ids = this.core.ids();
142
+ const humans = ids.filter((id) => !isBot(id)).length;
143
+ const bots = ids.filter(isBot);
144
+ const want = humans > 0 ? Math.max(0, this.opts.bots - humans) : 0;
145
+ if (bots.length < want) {
146
+ let id = BOT_PLAYER_BASE;
147
+ while (bots.includes(id))
148
+ id++;
149
+ this.core.add(id, true, this.phase !== "racing");
150
+ }
151
+ else if (bots.length > want) {
152
+ this.core.remove(Math.max(...bots));
153
+ }
154
+ }
155
+ /** A player joined (room addPlayer): they race from the start, held if the countdown runs. */
156
+ joined(id) {
157
+ this.core.add(id, false, this.phase !== "racing");
158
+ }
159
+ left(id) {
160
+ this.core.remove(id);
161
+ this.earn.forget(id);
162
+ this.places.delete(id);
163
+ }
164
+ /** Call before each sim step: seats, systems, the clock, and every bot's input. */
165
+ beforeStep(inputs) {
166
+ this.clock++;
167
+ this.fillSeats();
168
+ if (this.phase === "racing")
169
+ this.scheduler.tick();
170
+ if (this.phase === "results") {
171
+ for (const [pid, cmd] of inputs)
172
+ if ((cmd.buttons & Button.Ready) !== 0)
173
+ this.ready.add(pid);
174
+ }
175
+ const n = this.core.ids().filter((id) => !isBot(id)).length;
176
+ for (const id of this.core.ids()) {
177
+ if (!isBot(id))
178
+ continue;
179
+ // Seat bots spread out: each one a little looser than the last, and looser with more people present.
180
+ const k = (id - BOT_PLAYER_BASE) % 8;
181
+ const skill = Math.max(0.35, SEAT_BOT_SKILL - 0.05 * k - 0.04 * Math.max(0, n - 1));
182
+ inputs.set(id, this.core.botInput(id, this.clock, skill));
183
+ }
184
+ }
185
+ humansPresent() {
186
+ return this.core.ids().filter((id) => !isBot(id));
187
+ }
188
+ record(ev) {
189
+ if (ev.unpaid)
190
+ return [];
191
+ const late = ev.own === true || this.lateStart.size === 0 ? null : this.lateStart;
192
+ const paid = ev.players.filter((p) => !isBot(p) && !(late?.has(p) ?? false));
193
+ return paid.length === ev.players.length ? this.earn.record(ev) : this.earn.record({ name: ev.name, players: paid });
194
+ }
195
+ /** The round is over: "round.finished" for everyone present, then results. */
196
+ closeRound() {
197
+ if (this.phase !== "racing")
198
+ return;
199
+ this.pending.push({ name: "round.finished", players: this.core.ids() });
200
+ this.phase = "results";
201
+ this.phaseTicks = 0;
202
+ this.core.setFrozen(true);
203
+ }
204
+ /** Call after each sim step with its events. */
205
+ afterStep(sim) {
206
+ const events = this.pending;
207
+ this.pending = [];
208
+ if (this.phase === "racing") {
209
+ for (const e of sim) {
210
+ if (e.name === "player.finished") {
211
+ if (this.places.has(e.player))
212
+ continue;
213
+ const place = this.places.size + 1;
214
+ this.places.set(e.player, place);
215
+ if (this.closeAt < 0 && !isBot(e.player)) {
216
+ this.closeAt = this.runTick + this.windowTicks;
217
+ this.announcement = { id: ++this.announceN, text: `${Math.round(this.windowTicks / this.core.tickRate)} seconds to the finish`, tone: "info" };
218
+ }
219
+ this.times.set(e.player, this.runTick / this.core.tickRate);
220
+ const bot = isBot(e.player);
221
+ // Finishing is the player's own act. Winning or a podium needs another person in the round:
222
+ // beating a field of seat bots pays nothing extra (the arena's round.top rule).
223
+ events.push(bot ? { name: "player.finished", players: [e.player], unpaid: true } : { name: "player.finished", players: [e.player], own: true });
224
+ const contested = this.humansPresent().length >= 2 && !bot;
225
+ if (place === 1)
226
+ events.push(contested ? { name: "race.won", players: [e.player] } : { name: "race.won", players: [e.player], unpaid: true });
227
+ if (place <= 3)
228
+ events.push(contested ? { name: "race.podium", players: [e.player] } : { name: "race.podium", players: [e.player], unpaid: true });
229
+ }
230
+ else {
231
+ events.push(isBot(e.player) ? { name: e.name, players: [e.player], unpaid: true } : { name: e.name, players: [e.player], own: true });
232
+ }
233
+ }
234
+ }
235
+ const awards = [];
236
+ for (const ev of events) {
237
+ this.dispatchingUnpaid = ev.unpaid === true;
238
+ try {
239
+ this.scheduler.dispatch(ev.name, ev.players);
240
+ }
241
+ finally {
242
+ this.dispatchingUnpaid = false;
243
+ }
244
+ awards.push(...this.record(ev));
245
+ }
246
+ for (const ev of this.pending) {
247
+ awards.push(...this.record(ev));
248
+ events.push(ev);
249
+ }
250
+ this.pending = [];
251
+ for (const ev of events)
252
+ this.progress(ev);
253
+ for (const a of awards) {
254
+ const row = this.coins.get(a.player) ?? new Map();
255
+ row.set(a.rule, (row.get(a.rule) ?? 0) + a.units);
256
+ this.coins.set(a.player, row);
257
+ }
258
+ let reset = false;
259
+ const ids = this.core.ids();
260
+ if (this.phase === "countdown") {
261
+ if (++this.phaseTicks >= this.countdownTicks) {
262
+ this.phase = "racing";
263
+ this.phaseTicks = 0;
264
+ this.core.setFrozen(false);
265
+ }
266
+ }
267
+ else if (this.phase === "racing") {
268
+ this.runTick++;
269
+ const humans = ids.filter((id) => !isBot(id));
270
+ if (humans.length > 0 && humans.every((id) => this.places.has(id))) {
271
+ if (this.allDoneTick < 0)
272
+ this.allDoneTick = this.runTick;
273
+ if (this.runTick - this.allDoneTick >= this.graceTicks || ids.every((id) => this.places.has(id)))
274
+ this.closeRound();
275
+ }
276
+ if (this.limitTicks > 0 && this.runTick >= this.limitTicks)
277
+ this.closeRound();
278
+ if (this.closeAt >= 0 && this.runTick >= this.closeAt)
279
+ this.closeRound();
280
+ if (this.upcomingAt && this.runTick >= this.upcomingAt.tick)
281
+ this.upcomingAt = null;
282
+ if (this.phase === "results") {
283
+ for (const ev of this.pending.splice(0)) {
284
+ awards.push(...this.record(ev));
285
+ events.push(ev);
286
+ this.progress(ev);
287
+ }
288
+ }
289
+ if (ids.length === 0 && this.runTick > 1)
290
+ reset = true;
291
+ }
292
+ else if (++this.phaseTicks >= this.endTicks || this.allReady()) {
293
+ reset = true;
294
+ }
295
+ if (reset)
296
+ this.newRun();
297
+ return { events, awards, reset };
298
+ }
299
+ allReady() {
300
+ if (this.phaseTicks < MIN_END_SECONDS * this.core.tickRate)
301
+ return false;
302
+ const humans = this.humansPresent();
303
+ return humans.length > 0 && humans.every((id) => this.ready.has(id));
304
+ }
305
+ progress(ev) {
306
+ if (this.tracked.length === 0 || !this.tracked.includes(ev.name))
307
+ return;
308
+ const e = { name: ev.name, players: ev.players.filter((p) => !isBot(p)) };
309
+ for (const a of this.achievements.record(e))
310
+ this.reached.push({ player: a.player, kind: "achievement", id: a.rule });
311
+ for (const g of this.missions.record(e))
312
+ this.reached.push({ player: g.player, kind: "goal", id: g.rule });
313
+ }
314
+ takeProgress() {
315
+ if (this.reached.length === 0)
316
+ return [];
317
+ const out = this.reached;
318
+ this.reached = [];
319
+ return out;
320
+ }
321
+ /** A referee run starts for this player: counts start over; a run begun mid-race is not paid world progress. */
322
+ startRun(player) {
323
+ this.earn.forget(player);
324
+ if (this.phase === "racing" && this.runTick > 0)
325
+ this.lateStart.add(player);
326
+ this.pending.push({ name: "run.started", players: [player], unpaid: true });
327
+ }
328
+ count(player, event) {
329
+ return this.earn.count(player, event);
330
+ }
331
+ /** Live order: finished by place, then everyone else by progress (ties by id). */
332
+ standings() {
333
+ const ids = this.core.ids();
334
+ return ids.sort((a, b) => {
335
+ const pa = this.places.get(a);
336
+ const pb = this.places.get(b);
337
+ if (pa !== undefined || pb !== undefined)
338
+ return (pa ?? 1e9) - (pb ?? 1e9);
339
+ return this.core.progress(b) - this.core.progress(a) || a - b;
340
+ });
341
+ }
342
+ /** Ticks until the round closes (the time limit, or the finish window once someone has finished), or null. */
343
+ leftTicks() {
344
+ const ends = [this.limitTicks > 0 ? this.limitTicks : Infinity, this.closeAt >= 0 ? this.closeAt : Infinity];
345
+ const end = Math.min(...ends);
346
+ return Number.isFinite(end) ? end - this.runTick : null;
347
+ }
348
+ info() {
349
+ const rate = this.core.tickRate;
350
+ const order = this.standings();
351
+ const counts = (id) => {
352
+ const out = {};
353
+ for (const name of this.tracked) {
354
+ const n = Math.max(this.achievements.count(id, name), this.missions.count(id, name));
355
+ if (n > 0)
356
+ out[name] = n;
357
+ }
358
+ return out;
359
+ };
360
+ return {
361
+ run: this.runN,
362
+ phase: this.phase,
363
+ course: this.course,
364
+ countdown: this.phase === "countdown" ? Math.max(0, Math.ceil((this.countdownTicks - this.phaseTicks) / rate)) : 0,
365
+ seconds: Math.floor(this.runTick / rate),
366
+ left: this.leftTicks() === null ? null : Math.max(0, Math.ceil((this.leftTicks() ?? 0) / rate)),
367
+ players: order.map((id, i) => {
368
+ const t = this.times.get(id);
369
+ return {
370
+ id,
371
+ bot: isBot(id),
372
+ place: this.places.get(id) ?? null,
373
+ time: t === undefined ? null : Math.round(t * 100) / 100,
374
+ progress: Math.round(this.core.progress(id) * 1000) / 1000,
375
+ rank: i + 1,
376
+ counts: counts(id),
377
+ ready: this.ready.has(id),
378
+ };
379
+ }),
380
+ announce: this.announcement,
381
+ next: this.upcomingAt ? { label: this.upcomingAt.label, in: Math.max(0, Math.ceil((this.upcomingAt.tick - this.runTick) / rate)) } : null,
382
+ nextRunIn: this.phase === "results" ? Math.max(0, Math.ceil((this.endTicks - this.phaseTicks) / rate)) : null,
383
+ coins: [...this.coins].filter(([id]) => !isBot(id)).map(([id, m]) => ({ id, rules: [...m].map(([rule, units]) => ({ rule, units })) })),
384
+ };
385
+ }
386
+ }
@@ -0,0 +1,98 @@
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 { captureKart, lateralOffset, nearestSample } from "@netforge/engine-sim";
8
+ import type { InputCommand, Kart, KartStat, KartTrackDef, KartTuning, KartWorld } from "@netforge/engine-sim";
9
+ import type { GameManifest } from "../api/registry.js";
10
+ import type { EntitySchema, EntityState } from "../net/schema.js";
11
+ import type { SimAdapter } from "../net/sim-adapter.js";
12
+ import { RaceRunner } from "./race.js";
13
+ import type { RaceCore, RaceRunnerOptions, RaceSimEvent } from "./race.js";
14
+ /** Racer defaults: a 4 minute limit, 60 Hz, five seat bots. */
15
+ export declare const RACER_DEFAULTS: {
16
+ readonly roundSeconds: 240;
17
+ readonly tickRate: 60;
18
+ readonly bots: 5;
19
+ };
20
+ export interface CompiledRacer {
21
+ readonly tracks: readonly KartTrackDef[];
22
+ readonly names: readonly string[];
23
+ readonly tuning: Partial<KartTuning>;
24
+ readonly statSteps: readonly number[];
25
+ readonly upgradeStat: readonly KartStat[];
26
+ }
27
+ /** Compile the racer parts of a manifest. Issues are agent-readable. */
28
+ export declare function compileRacer(m: GameManifest, issues: string[]): CompiledRacer;
29
+ /**
30
+ * What a kart bot does: steer at a point on the racing line ahead (further
31
+ * at speed), lift off for tight bends, and drift through long ones at
32
+ * higher skill. `skill` 0..1 shades its top speed and its line.
33
+ */
34
+ export declare function kartBotInput(w: KartWorld, k: Kart, clock: number, skill: number, lane?: number, mem?: {
35
+ driftUntil: number;
36
+ }): InputCommand;
37
+ export declare class KartCore implements RaceCore {
38
+ readonly compiled: CompiledRacer;
39
+ readonly tickRate: number;
40
+ readonly world: KartWorld;
41
+ private nextSlot;
42
+ private readonly memory;
43
+ constructor(compiled: CompiledRacer, tickRate: number, authority?: boolean);
44
+ ids(): number[];
45
+ /** The lowest grid slot nobody holds. */
46
+ private freeSlot;
47
+ add(id: number, bot: boolean, frozen: boolean): void;
48
+ remove(id: number): void;
49
+ reset(course: number, frozen: boolean): void;
50
+ setFrozen(frozen: boolean): void;
51
+ finished(id: number): boolean;
52
+ progress(id: number): number;
53
+ position(id: number): readonly [number, number, number];
54
+ step(inputs: ReadonlyMap<number, InputCommand>): RaceSimEvent[];
55
+ botInput(id: number, clock: number, skill: number): InputCommand;
56
+ grant(id: number, upgrade: number): boolean;
57
+ setCosmetic(id: number, cosmetic: number): void;
58
+ }
59
+ /** Field indices of the racer wire schema. */
60
+ export declare const KF: {
61
+ readonly x: 0;
62
+ readonly z: 1;
63
+ readonly yaw: 2;
64
+ readonly vx: 3;
65
+ readonly vz: 4;
66
+ readonly flags: 5;
67
+ readonly timers: 6;
68
+ readonly track: 7;
69
+ readonly coinsLo: 8;
70
+ readonly coinsHi: 9;
71
+ readonly finishTick: 10;
72
+ readonly stats: 11;
73
+ readonly cosmetic: 12;
74
+ readonly clock: 13;
75
+ };
76
+ export declare const kartSchema: EntitySchema;
77
+ export interface KartRoomWorld {
78
+ readonly role: "server" | "client";
79
+ readonly core: KartCore;
80
+ readonly runner: RaceRunner | null;
81
+ }
82
+ export interface KartAdapterOptions {
83
+ readonly name: string;
84
+ readonly version: string;
85
+ readonly manifest: GameManifest;
86
+ readonly compiled: CompiledRacer;
87
+ readonly tickRate: number;
88
+ readonly bots: number;
89
+ readonly roundSeconds: number | null;
90
+ readonly runner?: RaceRunnerOptions;
91
+ }
92
+ type Wire = ReturnType<typeof captureKart>;
93
+ export declare function kartFromValues(e: EntityState): Wire;
94
+ /** The racer's SimAdapter: the room runs the race; a client predicts only its own kart (no bumps). */
95
+ export declare function kartAdapter(o: KartAdapterOptions): SimAdapter<KartRoomWorld, InputCommand>;
96
+ /** A client autopilot: the seat bot's driving on our newest predicted kart. */
97
+ export declare function kartAutopilot(compiled: CompiledRacer, tickRate: number): (self: EntityState | undefined, tick: number, skill?: number, others?: readonly EntityState[]) => InputCommand;
98
+ export { nearestSample, lateralOffset };