@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 +65 -0
- package/CLAUDE.md +11 -0
- package/README.md +68 -48
- package/client/main.ts +1 -1
- package/llms-full.txt +236 -0
- package/llms.txt +1 -1
- package/package.json +38 -11
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
|
-
#
|
|
1
|
+
# @netforge/template-racer
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
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
|
-
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
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.
|
|
4
|
-
"description": "
|
|
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.
|
|
35
|
-
"@netforge/engine": "0.
|
|
36
|
-
"@netforge/engine
|
|
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",
|