@netforge/template-racer 0.1.0 → 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.
package/AGENTS.md ADDED
@@ -0,0 +1,65 @@
1
+ # Extending TORQUEBAY
2
+
3
+ TORQUEBAY is the kart racer template of the Net Forge game engine. This file tells a coding agent how to change the game safely. `llms.txt` in this package has the full recipe book for this genre, and `llms-full.txt` adds the whole `src/game.ts` as a worked example.
4
+
5
+ ## The one file to edit
6
+
7
+ Change the game by editing `src/game.ts` only. It holds declarations, not engine code. src/names.json holds the title and every label players read. Everything else (the client, the manifest, the store hooks, the simulation, the netcode) is template machinery.
8
+
9
+ ## Import only @netforge/engine
10
+
11
+ `src/game.ts` imports from `@netforge/engine` and nothing else. The template's game imports:
12
+
13
+ ```ts
14
+ import { earn, game, store, system, upgrade } from "@netforge/engine";
15
+ ```
16
+
17
+ Also available for this genre: `economy`. `earn` has `earn.rule(...)` and `earn.stake(...)`; `store` has `store.sku(...)`. `system(id, ctx => ...)` gets the systems API described in `llms.txt`. The renderer, input, netcode and physics live elsewhere and are not reachable from game code.
18
+
19
+ ## Rules the build enforces
20
+
21
+ - Ids are lower_snake_case.
22
+ - Every game declares at least one `earn.rule(...)` (mandatory earn) and one `earn.stake(...)` (the buy-in run), on one top-level line. A game without either does not build.
23
+ - Asset ids are library ids such as `style.lowpoly.turret`, never URLs or file paths, and must exist in the asset library.
24
+ - `game({ template: "racer", style, tracks: [...] })` with 1 to 8 tracks.
25
+ - No `weapon()`, `director()`, `keeper()` or enemy prefabs.
26
+ - A track has 4 to 64 control points at least 6 m apart and is at least 200 m round.
27
+ - Upgrades raise one of: speed (top speed), haste (drift charge rate), might (boost length), armor (grass and wall losses), magnet (coin reach, m).
28
+ - 4 players plus seat bots must all finish a race, and an earn rule must fire, in a 4-minute playtest.
29
+ - `game({ style })` is the art style the creator picked (lowpoly, toon, ps1, pixel, voxel or handdrawn). Keep it.
30
+ - Keep the store SKUs the client is wired to, at their prices: turbo, tuned_engine, soft_tyres, skin_flame and skin_chrome (with the paints' grants: flame 1, chrome 2). "stake" is reserved and is never a SKU id.
31
+
32
+ ## Validate
33
+
34
+ - With the MCP server (`npx -y @netforge/mcp`, bin `netforge-mcp`): call `netforge_validate_game` on the game. It returns one line per problem.
35
+ - In this package: `npx vitest run` runs `test/harness.test.ts`, `test/host.test.ts`, `test/race.test.ts`, `test/store.test.ts`. A bad declaration throws `GameDefinitionError` with a line you can act on.
36
+
37
+ ## Playtest
38
+
39
+ - With the MCP server: call `netforge_run_playtest`.
40
+ - In code: `import { engine } from "@netforge/engine"` and run `engine.test.run({ ticks: 60 * 240, bots: 4, assert: [] })`. It reports rounds (wave), runs.won (races every playtest driver finished), scorers, deaths (hard wall hits), earned by rule, softlock and ms per tick.
41
+
42
+ ## Play locally
43
+
44
+ ```sh
45
+ mkdir -p my-game/src && cd my-game
46
+ npm init -y && npm install @netforge/kit-play @netforge/engine @netforge/template-racer
47
+ cp node_modules/@netforge/template-racer/src/game.ts src/
48
+ echo '{"template":"racer"}' > netnet.json
49
+ npx kit-play # opens http://127.0.0.1:4663
50
+ ```
51
+
52
+ The MCP tool `netforge_play_locally` does the same. Coins are play money; nothing pays out and no chain or wallet is involved. Edit `src/game.ts` and run it again to try a change.
53
+
54
+ ## What not to do
55
+
56
+ - Do not write renderers, netcode, physics or bot AI. The template already has them.
57
+ - Do not use asset URLs or file paths. Use library ids.
58
+ - Do not import anything other than `@netforge/engine` in `src/game.ts`.
59
+ - Do not edit `client/`, `src/manifest.ts` or `src/store.ts`.
60
+ - Do not rename or reprice the store SKUs the client is wired to.
61
+ - Do not add weapons, enemies or a director; tune the tracks and handling instead.
62
+
63
+ ## Docs
64
+
65
+ https://forge.netnet.capital/docs/templates/racer
package/CLAUDE.md ADDED
@@ -0,0 +1,11 @@
1
+ # CLAUDE.md
2
+
3
+ Read AGENTS.md before changing this game.
4
+
5
+ The five hard rules:
6
+
7
+ 1. Edit only `src/game.ts`.
8
+ 2. Import only `@netforge/engine`.
9
+ 3. Ids are lower_snake_case.
10
+ 4. Declare at least one `earn.rule(...)` and exactly one `earn.stake(...)`.
11
+ 5. No renderers, netcode, physics or asset URLs.
package/README.md CHANGED
@@ -1,55 +1,75 @@
1
- # TORQUEBAY (racer template)
1
+ # @netforge/template-racer
2
2
 
3
- Up to eight karts race laps round harbour circuits: hold the throttle, drift
4
- through the bends to charge a mini-turbo (release it for the boost), hit the
5
- boost pads, collect coins (each one held adds 1% top speed, up to ten) and
6
- keep off the grass. A round is one race; the next round runs another
7
- circuit. Seat bots fill the grid, so one person alone still has a field.
3
+ A kart racer game template for Net Forge: up to 8 players on harbour circuits, with seat bots filling the grid (TORQUEBAY).
8
4
 
9
- | Path | What |
10
- |---|---|
11
- | `src/game.ts` | The game, as the model writes it (declarations only): three circuits, round upgrades, earn rules, stake, store. |
12
- | `src/manifest.ts` | Loads and compiles it; `racerAdapter()` for the room server and the client. Room template `racer`. |
13
- | `src/store.ts` | Store hooks on the Kit's API names: three round upgrades (40, 40, 60 coins), two paint jobs (300, 500), bumper kit gear (120); mock until the token is live. |
14
- | `client/` | The browser client: chase camera, a streamed track, HUD with minimap, keyboard/gamepad/touch, game feel. |
15
- | `test/` | Playtest bots through `engine.test.run`, the race rules, prediction, the store. |
16
- | `llms.txt` | How to change the game (for agents). |
17
-
18
- ## How it plays on the server
19
-
20
- - The sim is `@netforge/engine-sim`'s kart mode (`src/kart.ts`): arcade
21
- vehicle physics on a 1/1024 grid at 60 Hz. Velocity splits into forward
22
- and sideways parts each tick: the engine pushes the forward part toward
23
- top speed, grip bleeds the sideways part (much less while drifting), and
24
- steering turns with speed. A held drift charges a mini-turbo in three
25
- tiers. Laps count through four quarter checkpoints in order. A client
26
- predicts its own kart alone; kart bumps run only on the room server and
27
- the next snapshot corrects them.
28
- - The track streams: the client builds the road,
29
- kerbs, barriers and scenery in chunks of about 40 m within 170 m of the
30
- camera, two chunks a frame, nearest first, and drops chunks behind.
31
- - The race runner (`@netforge/engine/game` `RaceRunner`, shared with
32
- SKYLEDGE) owns rounds: countdown, race, results. `player.finished` pays
33
- your own finish; `race.won` and `race.podium` pay only with another
34
- person in the round. Seat bots never earn.
35
- - Seat bots drive the racing line, pass the kart ahead, hold drifts through
36
- long bends, and are shaded by skill (top speed and line).
37
-
38
- ## Run it
39
-
40
- This template runs on your own computer with play money through
41
- `kit play` (`@netforge/kit-play`). From the repository root:
5
+ Hold the throttle, drift through the bends to charge a mini-turbo (release it for the boost), hit the boost pads, collect coins (each one held adds 1% top speed, up to ten) and keep off the grass. A round is one race, and the next round runs another circuit.
6
+
7
+ The simulation is the engine's kart mode: arcade vehicle physics on a 1/1024 grid at 60 Hz. A held drift charges a mini-turbo in three tiers. Laps count through four quarter checkpoints in order. A client predicts its own kart, and kart bumps run only on the room host, corrected by the next snapshot.
8
+
9
+ The client streams the track: it builds road, kerbs, barriers and scenery in chunks of about 40 m within 170 m of the camera and drops chunks behind.
10
+
11
+ The race runner owns rounds (countdown, race, results). `player.finished` pays your own finish; `race.won` and `race.podium` pay only with another person in the round. Seat bots drive the racing line, pass the kart ahead, hold drifts through long bends and never earn.
12
+
13
+ ## Install
42
14
 
43
15
  ```sh
44
- pnpm install --frozen-lockfile && pnpm build
45
- mkdir -p play-racer/src && cp packages/template-racer/src/game.ts play-racer/src/
46
- echo '{"template":"racer"}' > play-racer/netnet.json
47
- node packages/kit-play/dist/cli.js play-racer # opens http://127.0.0.1:4663
16
+ npm install @netforge/template-racer
48
17
  ```
49
18
 
50
- Edit `play-racer/src/game.ts` and run it again to try a change. Real-token
51
- earning, stakes and claims run only on the hosted Net Forge server.
19
+ You normally do not import this package yourself. `kit play` runs it on your own computer, and the Net Forge MCP server and studio copy its `src/game.ts` as the starting point for a new game.
20
+
21
+ ## Play it locally
22
+
23
+ ```sh
24
+ mkdir -p my-game/src && cd my-game
25
+ npm init -y && npm install @netforge/kit-play @netforge/engine @netforge/template-racer
26
+ cp node_modules/@netforge/template-racer/src/game.ts src/
27
+ echo '{"template":"racer"}' > netnet.json
28
+ npx kit-play # opens http://127.0.0.1:4663
29
+ ```
30
+
31
+ Coins are play money and nothing pays out. Real-token earning, stakes and claims run only on the hosted Net Forge server.
32
+
33
+ ## Change the game
34
+
35
+ Races are three laps and six seat bots fill the grid. Run a shorter race with a smaller field on the Harbour Loop:
36
+
37
+ ```ts
38
+ players: { prefab: "style.lowpoly.kart_a", max: 8, bots: 4 },
39
+ // in tracks[0], the Harbour Loop:
40
+ laps: 2,
41
+ ```
42
+
43
+ Save `src/game.ts` and run `npx kit-play` again.
44
+
45
+ ## Files
46
+
47
+ | Path | What |
48
+ |---|---|
49
+ | `src/game.ts` | The game, declarations only: three circuits, round upgrades, earn rules, stake, store. |
50
+ | `src/manifest.ts` | Loads and compiles the game; `racerAdapter()` and `racerGame()` for the room host and the client. |
51
+ | `src/store.ts` | Store hooks on the Kit's names: three round upgrades (40, 40, 60 coins), two paint jobs (300, 500) and the bumper kit gear (120). |
52
+ | `client/` | The browser client: chase camera, a streamed track, HUD with minimap, keyboard, gamepad and touch, game feel. |
53
+ | `test/` | Playtest bots through `engine.test.run`, the race rules, prediction, the local host and the store. |
54
+ | `llms.txt` | How to change the game, for agents. |
55
+
56
+ ## Controls
57
+
58
+ Keys: W or Up accelerate, S or Down brake, A D or arrows steer, hold Space or Shift through a bend to drift (release for the boost), R or Enter readies the next race. The backquote key shows stats.
59
+
60
+ ## For AI agents
61
+
62
+ Read `AGENTS.md` and `llms.txt` in this package before changing the game. `llms-full.txt` adds the whole `src/game.ts` as a worked example.
63
+
64
+ Agents can also drive this template through the Net Forge MCP server: run `npx -y @netforge/mcp` (package `@netforge/mcp`, bin `netforge-mcp`). Its tools are `netforge_list_templates`, `netforge_create_game`, `netforge_explain_api`, `netforge_search_docs`, `netforge_validate_game`, `netforge_run_playtest` and `netforge_play_locally`.
65
+
66
+ To set up a coding agent in a game project, run `npx netforge setup agent`. It writes AGENTS.md, CLAUDE.md, Cursor rules and `.mcp.json` into the project. In Claude Code you can also add the server directly with `claude mcp add netforge -- npx -y @netforge/mcp`.
67
+
68
+ ## Docs
69
+
70
+ - https://forge.netnet.capital/docs/templates/racer
71
+ - `llms.txt` and `llms-full.txt` in this package (also exported as `@netforge/template-racer/llms.txt` and `@netforge/template-racer/llms-full.txt`)
72
+
73
+ ## License
52
74
 
53
- Keys: W or Up accelerate, S or Down brake, A D or arrows steer, hold Space or
54
- Shift through a bend to drift (release for the boost), R or Enter readies
55
- the next race. ` shows stats.
75
+ MIT. See `LICENSE` and `NOTICE`.
package/client/main.ts CHANGED
@@ -137,7 +137,7 @@ async function main(): Promise<void> {
137
137
  if (friendRoomId && conn.roomId !== friendRoomId) setTimeout(() => hud.toast("Room full, new room"), 500);
138
138
  if (conn.welcome.version !== GAME_VERSION) setTimeout(() => hud.toast("Update ready. Reload."), 800);
139
139
  const linkFor = (base: string) =>
140
- `${base}?room=${conn.roomId}${place !== PLACE ? `&place=${encodeURIComponent(place)}` : ""}${params.has("server") ? `&server=${encodeURIComponent(endpoint)}` : ""}`;
140
+ `${base}?room=${conn.roomId}${place !== PLACE ? `&place=${encodeURIComponent(place)}` : ""}${params.has("server") ? `&server=${encodeURIComponent(endpoint)}` : ""}${params.has("game") ? `&game=${encodeURIComponent(params.get("game") ?? "")}` : ""}`;
141
141
  history.replaceState(null, "", `${linkFor(location.pathname)}${autopilot ? `&autopilot=${autopilotMode}` : ""}${params.has("webgl") ? "&webgl=1" : ""}${params.has("stats") ? "&stats=1" : ""}${params.has("style") ? `&style=${encodeURIComponent(params.get("style") ?? "")}` : ""}${params.has("preview") ? "&preview=1" : ""}`);
142
142
 
143
143
  const libraryBase = params.get("library") ?? env.VITE_LIBRARY_URL ?? "./library";
package/llms-full.txt ADDED
@@ -0,0 +1,236 @@
1
+ # Net Forge racer template (TORQUEBAY)
2
+
3
+ > Kart races for up to 8 players on harbour circuits, with seat bots filling
4
+ > the grid. A round is one race of a few laps; the next round runs another
5
+ > circuit. You change the game by editing src/game.ts only. The template
6
+ > already has the kart physics (throttle, brakes, steering, grip, drift
7
+ > mini-turbos in three tiers, boost pads, coins that raise top speed, grass,
8
+ > barriers, kart bumps), laps through ordered checkpoints, the race clock and
9
+ > places, bots that drive the racing line, netcode with client prediction at
10
+ > 60 Hz, a streamed track (only the road near you is built), rendering in
11
+ > every art style, keyboard, gamepad and touch controls, earn and the store.
12
+ > Do not write renderers, netcode, physics or bot AI.
13
+
14
+ ## Files
15
+
16
+ - src/game.ts: THE GAME. Declarations against @netforge/engine:
17
+ game(), upgrade(), system(), earn.rule(), earn.stake(), store.sku().
18
+ - src/names.json: the title and every label players read.
19
+ - Everything else is template machinery.
20
+
21
+ ## Rules the build enforces
22
+
23
+ - game({ template: "racer", style: "lowpoly", tracks: [...] }) with 1 to 8 tracks.
24
+ - No weapon(), director(), keeper() or enemy prefabs.
25
+ - A track has 4 to 64 control points at least 6 m apart and is at least 200 m round.
26
+ - Asset ids look like "style.lowpoly.kart_a", never URLs or paths, and must be in the asset library.
27
+ - game({ style }) is the art style the creator picked: lowpoly, toon, ps1, pixel, voxel or handdrawn. Keep it.
28
+ - Ids are lower_snake_case. Every game declares at least one earn.rule and one earn.stake (the buy-in run).
29
+ - Upgrades raise one of: speed (top speed), haste (drift charge rate), might
30
+ (boost length), armor (grass and wall losses), magnet (coin reach, m).
31
+ - 4 players plus seat bots must all finish a race, and an earn rule must
32
+ fire, in a 4-minute playtest.
33
+
34
+ ## Units
35
+
36
+ Seconds, metres, metres per second. The ground plane: x and z in metres.
37
+ Tick rate 60 Hz. Default top speed 26 m/s; a kart is about 1.8 m long.
38
+
39
+ ## The loop
40
+
41
+ Everyone lines up on a staggered grid behind the line. A 3 s countdown, then
42
+ the race: W/Up accelerate, S/Down brake, A/D steer, hold Space or Shift
43
+ while steering through a bend to drift (sparks turn blue, orange, then
44
+ purple as the mini-turbo charges; release for a 0.5, 0.9 or 1.4 s boost).
45
+ Boost pads boost; each coin held adds 1% top speed, up to 10. Grass slows,
46
+ barriers bounce. Laps count through four quarter checkpoints in order. The
47
+ round closes 25 s after the first person finishes (or at round.seconds);
48
+ the results show, then the next circuit.
49
+
50
+ ## Building a track
51
+
52
+ tracks: [{
53
+ name: "Harbour Loop",
54
+ laps: 3,
55
+ width: 14, // asphalt, m (8 to 30)
56
+ points: [[0, 0], [90, 0], [150, 15], ...], // the closed centreline in driving order; a smooth curve runs through them
57
+ boosts: [[0.12, 0], [0.47, -0.4]], // [lap fraction 0..1, lateral -1 (left) .. 1 (right)]
58
+ coins: [[0.04, 0.5], [0.046, 0.5]], // same coordinates; each kart collects its own (at most 62)
59
+ verge: 4, // optional grass before the barrier, m
60
+ }]
61
+ Rules of thumb: 500 to 900 m a lap at 14 m wide runs 25 to 40 s a lap; keep
62
+ neighbouring points 20 m or more apart for smooth bends; a hairpin needs
63
+ about 25 m of radius; put boosts on the exits of bends and coin rows on the
64
+ racing line or slightly off it (a choice). Two to four laps a race.
65
+
66
+ ## Common changes
67
+
68
+ ### A new circuit
69
+ Add a track to tracks. Circuits come one a round (the room draws one per round).
70
+
71
+ ### Tune the handling
72
+ kart: { topSpeed: 30, turn: 2.6, grip: 10, driftGrip: 1.5 } (topSpeed 10-45,
73
+ accel 8-80, turn 1-4, grip 3-20, driftGrip 0.5-6, grass 0.2-1, boostSpeed 1-1.8).
74
+
75
+ ### Round length and bots
76
+ round: { seconds: 240 } (60 to 600). players.bots: 6 fills the grid up to 6
77
+ with seat bots (0 turns them off). Bots drive the racing line, shaded by
78
+ skill, never earn, and a win over a field of bots pays nothing extra.
79
+
80
+ ### Add an earn rule
81
+ earn.rule("lap_leader", { when: "lap.completed", n: 2, pay: 3, oncePer: "run" });
82
+ Events: player.finished (you finished the race), race.won (first, paid only
83
+ with another person in the round), race.podium (top three, same),
84
+ lap.completed, coin.collected, drift.boosted (a mini-turbo fired),
85
+ drift.ultra (a full-charge one), kart.bumped (a hard wall hit),
86
+ round.finished, run.started (unpaid), and anything you emit(). Each rule
87
+ pays once per race per player, in coins; the room server is the referee.
88
+
89
+ ### Track something with a system
90
+ system("clean_line", ({ on, emit }) => {
91
+ const scrapes = new Map<number, number>();
92
+ on("kart.bumped", (ids) => { for (const id of ids) scrapes.set(id, (scrapes.get(id) ?? 0) + 1); });
93
+ on("player.finished", (ids) => { const clean = ids.filter((id) => !scrapes.get(id)); if (clean.length) emit("race.clean", clean); });
94
+ on("run.started", (ids) => { for (const id of ids) scrapes.delete(id); });
95
+ });
96
+ Systems also have every, after, onTick, players(), random(), announce(text),
97
+ upcoming(label, seconds) and end(). spawn and enemies are not part of this template.
98
+
99
+ ### The buy-in run (required)
100
+ earn.stake({ min: 10, max: 100, multiple: 2 }); one top-level line. "stake" is reserved.
101
+
102
+ ### Losable gear (optional)
103
+ store.sku("bumper_kit", { coins: 120, grants: { gear: "bumpers", loseBelow: 0.25 } });
104
+ Owned in the profile, coins only; one level of its upgrade in every staked
105
+ race; lost when that race makes less than loseBelow of its most earn.
106
+
107
+ ### Coins: earn and the store
108
+ store.sku("turbo", { coins: 40 }); // one level of the "turbo" upgrade for the race
109
+ store.sku("skin_flame", { coins: 300, grants: { cosmetic: 1 } }); // a paint job, owned for good
110
+ Keep the five SKUs the client is wired to (turbo, tuned_engine, soft_tyres,
111
+ skin_flame, skin_chrome) at their prices, with the paints' grants (flame 1,
112
+ chrome 2). PvP pots are not part of this template.
113
+
114
+ ### Change the karts (the asset library)
115
+ players: { prefab: "style.lowpoly.race_car", ... } (any vehicle: kart_a..e,
116
+ race_car, race_car_future, buggy-like speeder_a..c). Seat bots drive
117
+ kart_b..e; the pixel style draws its own kart sprites.
118
+
119
+ ## Test
120
+
121
+ engine.test.run({ ticks: 60 * 240, bots: 4, assert: [...] }) runs races on
122
+ the server sim with 4 playtest drivers plus seat bots and reports rounds
123
+ (wave), runs.won (races every playtest driver finished), scorers (drivers
124
+ that finished at least once), deaths (hard wall hits), earned by rule,
125
+ softlock and ms per tick.
126
+
127
+ ## Worked example: src/game.ts
128
+
129
+ The whole game as the template ships it. Every change starts from this file.
130
+
131
+ ```ts
132
+ /**
133
+ * TORQUEBAY: the racer template (play style "racer").
134
+ * Up to eight karts race laps around harbour circuits: hold the throttle,
135
+ * drift through the bends to charge a mini-turbo (release for the boost),
136
+ * hit the boost pads, collect coins (every coin you hold adds 1% top speed,
137
+ * up to ten) and keep off the grass. A round is one race; the next round runs
138
+ * another circuit. Seat bots fill the grid so a race always has a field.
139
+ *
140
+ * This file is the whole game as the model writes it: declarations only,
141
+ * against the typed API in @netforge/engine. Everything else (the kart
142
+ * physics, netcode, renderer, track streaming, bots, referee) is the
143
+ * template's machinery.
144
+ *
145
+ * Track coordinates are metres on the ground (x right, z down the screen's
146
+ * map). points is the closed centreline in driving order; boosts and coins
147
+ * are [lap fraction 0..1, lateral -1..1 of the half width].
148
+ */
149
+ import { earn, game, store, system, upgrade } from "@netforge/engine";
150
+
151
+ /** Coins in a row: n coins from lap fraction f, `gap` of a lap apart, at one lateral. */
152
+ const coinRow = (f: number, lateral: number, n: number, gap = 0.006): [number, number][] => Array.from({ length: n }, (_, i) => [f + i * gap, lateral]);
153
+
154
+ export default game({
155
+ template: "racer",
156
+ style: "lowpoly",
157
+ tickRate: 60,
158
+ places: { harbour: { chunks: "places/harbour", spawn: [0, 0, 0] } },
159
+ players: { prefab: "style.lowpoly.kart_a", max: 8, bots: 6 },
160
+ round: { seconds: 240 },
161
+ tracks: [
162
+ {
163
+ name: "Harbour Loop",
164
+ laps: 3,
165
+ width: 14,
166
+ points: [[0, 0], [90, 0], [150, 15], [175, 65], [150, 120], [90, 135], [50, 175], [-20, 190], [-85, 160], [-110, 100], [-90, 35]],
167
+ boosts: [[0.12, 0], [0.47, -0.4], [0.78, 0.4]],
168
+ coins: [...coinRow(0.04, 0.5, 5), ...coinRow(0.3, -0.5, 5), ...coinRow(0.6, 0, 5), ...coinRow(0.88, -0.3, 5)],
169
+ },
170
+ {
171
+ name: "Crane Yard",
172
+ laps: 3,
173
+ width: 13,
174
+ points: [[0, 0], [70, -10], [120, 10], [130, 60], [95, 85], [60, 70], [30, 95], [45, 140], [5, 175], [-55, 160], [-75, 105], [-50, 60], [-75, 20]],
175
+ boosts: [[0.08, 0], [0.38, 0.3], [0.7, -0.3]],
176
+ coins: [...coinRow(0.15, 0, 5), ...coinRow(0.45, 0.4, 5), ...coinRow(0.62, -0.4, 5), ...coinRow(0.85, 0, 5)],
177
+ },
178
+ {
179
+ name: "Lighthouse Run",
180
+ laps: 2,
181
+ width: 15,
182
+ points: [[0, 0], [120, -15], [200, 10], [235, 70], [210, 140], [140, 170], [80, 150], [40, 190], [-30, 210], [-95, 175], [-115, 105], [-80, 40]],
183
+ boosts: [[0.05, 0], [0.25, 0.35], [0.5, -0.35], [0.8, 0]],
184
+ coins: [...coinRow(0.1, -0.4, 6), ...coinRow(0.35, 0.4, 6), ...coinRow(0.66, 0, 6), ...coinRow(0.9, 0.3, 6)],
185
+ },
186
+ ],
187
+ });
188
+
189
+ // Round upgrades sold in the store (one level each, for the race). Racer upgrades raise speed
190
+ // (top speed), haste (drift charge), might (boost length), armor (grass and wall losses) or magnet (coin reach).
191
+ upgrade("turbo", { stat: "might", step: 0.25, maxLevel: 1 });
192
+ upgrade("tuned_engine", { stat: "speed", step: 0.05, maxLevel: 1 });
193
+ upgrade("soft_tyres", { stat: "haste", step: 0.3, maxLevel: 1 });
194
+ upgrade("bumpers", { stat: "armor", step: 0.3, maxLevel: 1 });
195
+
196
+ // A clean race: finish without scraping a wall hard.
197
+ system("clean_line", ({ on, emit }) => {
198
+ const scrapes = new Map<number, number>();
199
+ on("kart.bumped", (ids) => {
200
+ for (const id of ids) scrapes.set(id, (scrapes.get(id) ?? 0) + 1);
201
+ });
202
+ on("player.finished", (ids) => {
203
+ const clean = ids.filter((id) => (scrapes.get(id) ?? 0) === 0);
204
+ if (clean.length > 0) emit("race.clean", clean);
205
+ });
206
+ on("run.started", (ids) => {
207
+ for (const id of ids) scrapes.delete(id);
208
+ });
209
+ });
210
+
211
+ // Earn (mandatory). Pays the game's coins (1 coin = 1 token), once per race each.
212
+ // Seat bots never earn, and winning a field of bots pays nothing extra. Values are PLACEHOLDER.
213
+ earn.rule("hot_lap", { when: "drift.boosted", n: 3, pay: 4, oncePer: "run" });
214
+ earn.rule("chequered", { when: "player.finished", n: 1, pay: 10, oncePer: "run" });
215
+ earn.rule("clean_line", { when: "race.clean", n: 1, pay: 12, oncePer: "run" });
216
+ earn.rule("pole_to_flag", { when: "race.won", n: 1, pay: 25, oncePer: "run" });
217
+ // Buy-in run (the mandatory sink loop). PLACEHOLDER.
218
+ earn.stake({ min: 10, max: 100, multiple: 2 });
219
+
220
+ // Store (the Kit), priced in coins: round upgrades and two paint jobs owned for good.
221
+ store.sku("turbo", { coins: 40 });
222
+ store.sku("tuned_engine", { coins: 40 });
223
+ store.sku("soft_tyres", { coins: 60 });
224
+ store.sku("skin_flame", { coins: 300, grants: { cosmetic: 1 } });
225
+ store.sku("skin_chrome", { coins: 500, grants: { cosmetic: 2 } });
226
+ // Losable gear: bumpers in every staked race, lost when that race makes less than loseBelow of its most earn. PLACEHOLDER.
227
+ store.sku("bumper_kit", { coins: 120, grants: { gear: "bumpers", loseBelow: 0.25 } });
228
+ ```
229
+
230
+ ## Validate and playtest
231
+
232
+ - Validate: the MCP tool `netforge_validate_game` builds `src/game.ts` and returns one actionable line per problem (an unknown asset id, a missing earn.stake, a bad id). The same checks throw `GameDefinitionError` when the module is imported.
233
+ - Playtest: the MCP tool `netforge_run_playtest`, or in code `engine.test.run({ ticks: 60 * 240, bots: 4, assert: [] })`. It reports rounds (wave), runs.won (races every playtest driver finished), scorers, deaths (hard wall hits), earned by rule, softlock and ms per tick.
234
+ - Template tests: test/harness.test.ts, test/host.test.ts, test/race.test.ts, test/store.test.ts, run with `npx vitest run` in this package.
235
+ - Play locally: put `src/game.ts` and a `netnet.json` of `{"template":"racer"}` in a folder, install `@netforge/kit-play` and `@netforge/engine`, and run `npx kit-play` (or the MCP tool `netforge_play_locally`). It opens http://127.0.0.1:4663 with play money.
236
+ - Docs: https://forge.netnet.capital/docs/templates/racer
package/llms.txt CHANGED
@@ -1,4 +1,4 @@
1
- # TORQUEBAY: the racer template (NetNet game runtime, play style "racer")
1
+ # Net Forge racer template (TORQUEBAY)
2
2
 
3
3
  > Kart races for up to 8 players on harbour circuits, with seat bots filling
4
4
  > the grid. A round is one race of a few laps; the next round runs another
package/package.json CHANGED
@@ -1,12 +1,31 @@
1
1
  {
2
2
  "name": "@netforge/template-racer",
3
- "version": "0.1.0",
4
- "description": "TORQUEBAY: a racer template of the NetNet game runtime, kart races on harbour circuits with drift mini-turbos, boost pads, coins and laps at 60 Hz, a streamed track and seat-filling bots. The game module, its browser client and its playtest bots.",
3
+ "version": "0.2.0",
4
+ "description": "Kart racer game template for Net Forge: harbour circuits with drift mini-turbos, boost pads, coins and laps at 60 Hz, with seat-filling bots (TORQUEBAY).",
5
+ "keywords": [
6
+ "game-template",
7
+ "multiplayer",
8
+ "browser-game",
9
+ "html5-game",
10
+ "threejs",
11
+ "game-engine",
12
+ "typescript",
13
+ "gamedev",
14
+ "ai-agents",
15
+ "mcp",
16
+ "net-forge",
17
+ "kart-racer",
18
+ "racing",
19
+ "physics"
20
+ ],
5
21
  "license": "MIT",
6
22
  "type": "module",
7
23
  "exports": {
8
24
  ".": "./src/manifest.ts",
9
- "./store": "./src/store.ts"
25
+ "./store": "./src/store.ts",
26
+ "./package.json": "./package.json",
27
+ "./llms.txt": "./llms.txt",
28
+ "./llms-full.txt": "./llms-full.txt"
10
29
  },
11
30
  "files": [
12
31
  "src",
@@ -15,25 +34,36 @@
15
34
  "llms.txt",
16
35
  "README.md",
17
36
  "NOTICE",
18
- "LICENSE"
37
+ "LICENSE",
38
+ "llms-full.txt",
39
+ "AGENTS.md",
40
+ "CLAUDE.md"
41
+ ],
42
+ "sideEffects": [
43
+ "./src/game.ts",
44
+ "./src/manifest.ts",
45
+ "./client/**"
19
46
  ],
20
47
  "publishConfig": {
21
48
  "access": "public"
22
49
  },
50
+ "homepage": "https://forge.netnet.capital/docs/templates/racer",
51
+ "bugs": {
52
+ "url": "https://github.com/netnetdev/net-forge/issues"
53
+ },
23
54
  "repository": {
24
55
  "type": "git",
25
56
  "url": "git+https://github.com/netnetdev/net-forge.git",
26
57
  "directory": "packages/template-racer"
27
58
  },
28
- "homepage": "https://github.com/netnetdev/net-forge/tree/main/packages/template-racer#readme",
29
59
  "engines": {
30
60
  "node": ">=20"
31
61
  },
32
62
  "dependencies": {
33
63
  "three": "0.186.1",
34
- "@netforge/asset-library": "0.1.0",
35
- "@netforge/engine": "0.1.0",
36
- "@netforge/engine-sim": "0.1.0"
64
+ "@netforge/asset-library": "0.2.0",
65
+ "@netforge/engine-sim": "0.2.0",
66
+ "@netforge/engine": "0.2.0"
37
67
  },
38
68
  "devDependencies": {
39
69
  "@types/node": "22.20.1",
@@ -43,9 +73,6 @@
43
73
  "vite": "6.4.3",
44
74
  "vitest": "3.2.7"
45
75
  },
46
- "bugs": {
47
- "url": "https://github.com/netnetdev/net-forge/issues"
48
- },
49
76
  "scripts": {
50
77
  "build": "node ../engine/scripts/fresh-dist.mjs && vite build --config client/vite.config.ts",
51
78
  "typecheck": "tsc -p tsconfig.json",