@netforge/template-platformer 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 +321 -0
- package/llms.txt +1 -1
- package/package.json +37 -11
package/AGENTS.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Extending SKYLEDGE
|
|
2
|
+
|
|
3
|
+
SKYLEDGE is the platformer 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 (upgrades, earn rules, store items). 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: "platformer", style, levels: [...] })` with 1 to 8 levels.
|
|
25
|
+
- No `weapon()`, `director()`, `keeper()` or enemy prefabs: the challenge is the course.
|
|
26
|
+
- Every course must be finishable: the route planner runs from the start to the flag on every level, and a level with no route fails the build.
|
|
27
|
+
- Upgrades raise one of: speed (run speed), amount (extra air jumps), magnet (coin reach, m), maxHealth (shield hits per life), haste (dash cooldown).
|
|
28
|
+
- 4 players plus seat bots must all reach the flag, 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: feather, sprint, shield, skin_sunset and skin_frost (with the outfits' grants: sunset 1, frost 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 (rounds every playtest climber finished), scorers, deaths (falls), earned by rule, softlock, ms per tick and fun.routes (the planner's route per course).
|
|
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-platformer
|
|
47
|
+
cp node_modules/@netforge/template-platformer/src/game.ts src/
|
|
48
|
+
echo '{"template":"platformer"}' > 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; build challenge into the course instead.
|
|
62
|
+
|
|
63
|
+
## Docs
|
|
64
|
+
|
|
65
|
+
https://forge.netnet.capital/docs/templates/platformer
|
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-platformer
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
(hold for height, tap for a hop), dash across gaps, ride moving platforms,
|
|
5
|
-
bounce off springs and stay off the thorns. Each climber collects their own
|
|
6
|
-
coins. A round is one course; the next round runs the next course. Seat bots
|
|
7
|
-
fill empty places, so one person alone still has a race.
|
|
3
|
+
A multiplayer platformer game template for Net Forge: a side-view race to the flag for up to 8 players over cliffside courses (SKYLEDGE).
|
|
8
4
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
## How it plays on the server
|
|
20
|
-
|
|
21
|
-
- The sim is `@netforge/engine-sim`'s platform mode (`src/platform.ts`):
|
|
22
|
-
pure TypeScript swept AABB movement on a 1/1024 m grid at 60 Hz, so a
|
|
23
|
-
predicting client replays its own climber bit for bit. Climbers are
|
|
24
|
-
ghosts to each other and coins are per climber, so nothing another player
|
|
25
|
-
does can make your prediction wrong.
|
|
26
|
-
- Jump feel: coyote time (0.1 s), a jump buffer (0.13 s), variable jump
|
|
27
|
-
height (releasing Jump while rising cuts the rise), a lighter apex while
|
|
28
|
-
Jump is held, a heavier fall, ceiling corner nudges, an air dash.
|
|
29
|
-
- The race runner (`@netforge/engine/game` `RaceRunner`) owns rounds:
|
|
30
|
-
countdown, race, results, the next course. It pays `player.finished` (your
|
|
31
|
-
own act), and `race.won` and `race.podium` only with another person in the
|
|
32
|
-
round. Seat bots never earn.
|
|
33
|
-
- Seat bots run routes found by the route planner (`planCourse`, cached per
|
|
34
|
-
course and start tick), each starting a little after the signal by its
|
|
35
|
-
skill. A bot stuck for 5 s skips to its next checkpoint.
|
|
36
|
-
- The gate's softlock check: the planner must find a route to the flag on
|
|
37
|
-
every course.
|
|
38
|
-
|
|
39
|
-
## Run it
|
|
40
|
-
|
|
41
|
-
This template runs on your own computer with play money through
|
|
42
|
-
`kit play` (`@netforge/kit-play`). From the repository root:
|
|
5
|
+
Climbers run, jump (hold for height, tap for a hop), dash across gaps, ride moving platforms, bounce off springs and stay off the thorns. Each climber collects their own coins. A round is one course, and the next round runs the next course. Seat bots fill empty places, so one person alone still has a race.
|
|
6
|
+
|
|
7
|
+
The simulation is the engine's deterministic platform mode: swept box movement on a 1/1024 m grid at 60 Hz, so a predicting client replays its own climber bit for bit. Climbers are ghosts to each other and coins are per climber, so nothing another player does can make your prediction wrong.
|
|
8
|
+
|
|
9
|
+
Jump feel: coyote time (0.1 s), a jump buffer (0.13 s), variable jump height, a lighter apex while Jump is held, a heavier fall, ceiling corner nudges and an air dash.
|
|
10
|
+
|
|
11
|
+
The race runner owns rounds (countdown, race, results, next course). It pays `player.finished` for your own finish, and `race.won` and `race.podium` only with another person in the round. Seat bots run routes found by the route planner and never earn. Every course must have a planner route to the flag, or the build fails.
|
|
12
|
+
|
|
13
|
+
## Install
|
|
43
14
|
|
|
44
15
|
```sh
|
|
45
|
-
|
|
46
|
-
mkdir -p play-platformer/src && cp packages/template-platformer/src/game.ts play-platformer/src/
|
|
47
|
-
echo '{"template":"platformer"}' > play-platformer/netnet.json
|
|
48
|
-
node packages/kit-play/dist/cli.js play-platformer # opens http://127.0.0.1:4663
|
|
16
|
+
npm install @netforge/template-platformer
|
|
49
17
|
```
|
|
50
18
|
|
|
51
|
-
|
|
52
|
-
|
|
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-platformer
|
|
26
|
+
cp node_modules/@netforge/template-platformer/src/game.ts src/
|
|
27
|
+
echo '{"template":"platformer"}' > 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
|
+
Courses run three minutes and three seat bots fill the room. Field more bots and give each course a little longer:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
players: { prefab: "style.lowpoly.rogue", max: 8, bots: 5 },
|
|
39
|
+
round: { seconds: 210 },
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Save `src/game.ts` and run `npx kit-play` again.
|
|
43
|
+
|
|
44
|
+
## Files
|
|
45
|
+
|
|
46
|
+
| Path | What |
|
|
47
|
+
|---|---|
|
|
48
|
+
| `src/game.ts` | The game, declarations only: three courses, three round upgrades, earn rules, stake, store. |
|
|
49
|
+
| `src/manifest.ts` | Loads and compiles the game; `platformerAdapter()` and `platformerGame()` for the room host and the client. |
|
|
50
|
+
| `src/store.ts` | Store hooks on the Kit's names: three round upgrades (40, 40, 60 coins), two outfits (300, 500) and the shield charm gear (120). |
|
|
51
|
+
| `client/` | The browser client: a side-view three.js scene in every art style, HUD, input (keys, gamepad, touch), game feel. |
|
|
52
|
+
| `scripts/plan.ts` | The route planner run on every course (how long a good run takes). |
|
|
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: A D or arrows run, Space W or Up jump, Shift dash, Down plus Jump drops through a ledge, R or Enter readies the next course. 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/platformer
|
|
71
|
+
- `llms.txt` and `llms-full.txt` in this package (also exported as `@netforge/template-platformer/llms.txt` and `@netforge/template-platformer/llms-full.txt`)
|
|
72
|
+
|
|
73
|
+
## License
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
through a ledge, R or Enter readies the next course. ` shows stats.
|
|
75
|
+
MIT. See `LICENSE` and `NOTICE`.
|
package/client/main.ts
CHANGED
|
@@ -134,7 +134,7 @@ async function main(): Promise<void> {
|
|
|
134
134
|
if (friendRoomId && conn.roomId !== friendRoomId) setTimeout(() => hud.toast("Room full, new room"), 500);
|
|
135
135
|
if (conn.welcome.version !== GAME_VERSION) setTimeout(() => hud.toast("Update ready. Reload."), 800);
|
|
136
136
|
const linkFor = (base: string) =>
|
|
137
|
-
`${base}?room=${conn.roomId}${place !== PLACE ? `&place=${encodeURIComponent(place)}` : ""}${params.has("server") ? `&server=${encodeURIComponent(endpoint)}` : ""}`;
|
|
137
|
+
`${base}?room=${conn.roomId}${place !== PLACE ? `&place=${encodeURIComponent(place)}` : ""}${params.has("server") ? `&server=${encodeURIComponent(endpoint)}` : ""}${params.has("game") ? `&game=${encodeURIComponent(params.get("game") ?? "")}` : ""}`;
|
|
138
138
|
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" : ""}`);
|
|
139
139
|
|
|
140
140
|
const libraryBase = params.get("library") ?? env.VITE_LIBRARY_URL ?? "./library";
|
package/llms-full.txt
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Net Forge platformer template (SKYLEDGE)
|
|
2
|
+
|
|
3
|
+
> A side-view race to the flag for up to 8 players over cliffside courses,
|
|
4
|
+
> with seat bots filling empty places. A round is one course; the next round
|
|
5
|
+
> runs the next course. You change the game by editing src/game.ts only. The
|
|
6
|
+
> template already has the run and jump motor (coyote time, jump buffering,
|
|
7
|
+
> variable jump height, air dash, springs, one-way ledges, moving platforms),
|
|
8
|
+
> checkpoints and falls, per-player coins, the race clock and places, bots
|
|
9
|
+
> that run planned routes, netcode with client prediction, rendering in every
|
|
10
|
+
> art style, keyboard, gamepad and touch controls, earn and the store. Do not
|
|
11
|
+
> write renderers, netcode, physics or bot AI.
|
|
12
|
+
|
|
13
|
+
## Files
|
|
14
|
+
|
|
15
|
+
- src/game.ts: THE GAME. Declarations against @netforge/engine:
|
|
16
|
+
game(), upgrade(), system(), earn.rule(), earn.stake(), store.sku().
|
|
17
|
+
- src/names.json: the title and every label players read (upgrades, earn
|
|
18
|
+
rules, store items).
|
|
19
|
+
- Everything else is template machinery.
|
|
20
|
+
|
|
21
|
+
## Rules the build enforces
|
|
22
|
+
|
|
23
|
+
- game({ template: "platformer", style: "lowpoly", levels: [...] }) with 1 to 8 levels.
|
|
24
|
+
- No weapon(), director(), keeper() or enemy prefabs: the challenge is the course.
|
|
25
|
+
- Every course must be finishable: the gate runs a route planner from the start
|
|
26
|
+
to the flag on every level, and a level with no route fails the build.
|
|
27
|
+
- Asset ids look like "style.lowpoly.rogue", never URLs or paths, and must be in the asset library.
|
|
28
|
+
- game({ style }) is the art style the creator picked: lowpoly, toon, ps1, pixel, voxel or handdrawn. Keep it.
|
|
29
|
+
- Ids are lower_snake_case. Every game declares at least one earn.rule and one earn.stake (the buy-in run).
|
|
30
|
+
- Upgrades raise one of: speed (run speed), amount (extra air jumps),
|
|
31
|
+
magnet (coin reach, m), maxHealth (shield hits per life), haste (dash cooldown).
|
|
32
|
+
- 4 players plus seat bots must all reach the flag, and an earn rule must
|
|
33
|
+
fire, in a 4-minute playtest.
|
|
34
|
+
|
|
35
|
+
## Units
|
|
36
|
+
|
|
37
|
+
Seconds, metres, metres per second. Side view: x runs right from the start
|
|
38
|
+
to the flag, y runs up. Tick rate 60 Hz. A climber's body is 0.68 m wide and
|
|
39
|
+
1.4 m tall; feet are at its y.
|
|
40
|
+
|
|
41
|
+
## The loop
|
|
42
|
+
|
|
43
|
+
Everyone starts at the level's start. A 3 s countdown, then the race: run
|
|
44
|
+
(A/D, arrows, stick), jump (Space/W/Up, A on a pad; hold for a full jump, tap
|
|
45
|
+
for a hop), dash (Shift, X on a pad; once in the air, refilled on landing),
|
|
46
|
+
Down + Jump drops through a ledge. Thorns and the drop send you back to the
|
|
47
|
+
last checkpoint you touched. Touch the flag to finish; your place is the
|
|
48
|
+
order of finishing. The round closes a few seconds after every person has
|
|
49
|
+
finished, or at round.seconds. The results show, then the next course.
|
|
50
|
+
|
|
51
|
+
Numbers that make the default feel: run 7.5 m/s, a full jump 3.3 m high in
|
|
52
|
+
0.36 s, a tap jump about 1.4 m, coyote 0.1 s, jump buffer 0.13 s, fall
|
|
53
|
+
gravity x1.75, dash 16 m/s for 0.16 s. A full running jump clears a gap of
|
|
54
|
+
about 6 m; with the dash about 8.5 m.
|
|
55
|
+
|
|
56
|
+
## Building a course
|
|
57
|
+
|
|
58
|
+
Coordinates are metres. Helpers you can write in game.ts:
|
|
59
|
+
const ground = (x0, x1, y = 0) => [x0, y - 4, x1 - x0, 4]; // ground from x0 to x1, top at y
|
|
60
|
+
levels: [{
|
|
61
|
+
name: "Mossy Steps",
|
|
62
|
+
start: [0, 0], // feet of every climber at the start
|
|
63
|
+
flag: [128, 4.5], // the flag's foot: stand it on a block top
|
|
64
|
+
blocks: [ground(-8, 24), [14, 0, 3, 1.2], ...], // [x, y, width, height] from the bottom-left corner
|
|
65
|
+
ledges: [[40, 2.6, 4]], // [x, top y, width]: one-way, jump up through, Down + Jump drops
|
|
66
|
+
hazards: [[35, 0, 2, 0.5]], // [x, y, width, height]: thorns
|
|
67
|
+
springs: [[59, 0, 21]], // [x, y, launch speed m/s]: 21 is about 4.5 m up
|
|
68
|
+
movers: [{ at: [81, 0, 3], to: [5, 0], seconds: 4 }], // a 3 m platform at x 81, swinging 5 m right and back every 4 s
|
|
69
|
+
coins: [[4, 1], [5.2, 1]], // centres; each climber collects their own (at most 62)
|
|
70
|
+
checkpoints: [[31, 0], [92, 0]], // feet, in course order
|
|
71
|
+
}]
|
|
72
|
+
Rules of thumb: steps up of 1 to 2.5 m, gaps of 2 to 5 m (6 with a running
|
|
73
|
+
jump, 8 with the dash), a checkpoint every 30 to 50 m, coins along the line a
|
|
74
|
+
good run takes and a few on a harder high road. A course of 120 to 180 m runs
|
|
75
|
+
in 16 to 25 s for a good climber. killY (optional) is the fall line; default
|
|
76
|
+
8 m under the lowest block.
|
|
77
|
+
|
|
78
|
+
## Common changes
|
|
79
|
+
|
|
80
|
+
### A new course
|
|
81
|
+
Add a level object to levels. Courses rotate one per round in order.
|
|
82
|
+
|
|
83
|
+
### Tune the feel
|
|
84
|
+
motor: { runSpeed: 8, jumpHeight: 3.6, airJumps: 1 } (runSpeed 3-14, jumpHeight 1-8,
|
|
85
|
+
timeToApex 0.2-0.7, fallGravity 1-3, jumpCut 0.2-1, coyoteSeconds and
|
|
86
|
+
bufferSeconds 0-0.25, airJumps 0-2, dashSpeed 6-30). Change a few at a
|
|
87
|
+
time; the planner re-checks every course with the new numbers.
|
|
88
|
+
|
|
89
|
+
### Round length and bots
|
|
90
|
+
round: { seconds: 180 } (60 to 600). players.bots: 3 fills a room up to 3
|
|
91
|
+
players with seat bots (0 turns them off). Bots run planned routes and start
|
|
92
|
+
a little after the signal (looser bots later), never earn, and a win over a
|
|
93
|
+
field of bots pays nothing extra.
|
|
94
|
+
|
|
95
|
+
### Add an earn rule
|
|
96
|
+
earn.rule("speedy", { when: "player.finished", n: 1, pay: 6, oncePer: "run" });
|
|
97
|
+
Events: player.finished (you reached the flag), race.won (first to the flag,
|
|
98
|
+
paid only with another person in the round), race.podium (top three, same),
|
|
99
|
+
coin.collected (credits the collector), checkpoint.reached, player.fell,
|
|
100
|
+
spring.bounced, round.finished (everyone present), run.started (unpaid: a
|
|
101
|
+
player's earning run started over), and anything you emit(). A run is a
|
|
102
|
+
round; each rule pays once per round per player, in coins; the room server
|
|
103
|
+
is the referee; bots never earn.
|
|
104
|
+
|
|
105
|
+
### Track something with a system
|
|
106
|
+
system("clean_climb", ({ on, emit }) => {
|
|
107
|
+
const falls = new Map<number, number>();
|
|
108
|
+
on("player.fell", (ids) => { for (const id of ids) falls.set(id, (falls.get(id) ?? 0) + 1); });
|
|
109
|
+
on("player.finished", (ids) => { const clean = ids.filter((id) => !falls.get(id)); if (clean.length) emit("climb.clean", clean); });
|
|
110
|
+
on("run.started", (ids) => { for (const id of ids) falls.delete(id); });
|
|
111
|
+
});
|
|
112
|
+
Systems also have every(seconds, fn), after, onTick, players(), random(),
|
|
113
|
+
announce(text), upcoming(label, seconds) and end() (closes the round now).
|
|
114
|
+
spawn and enemies are not part of this template.
|
|
115
|
+
|
|
116
|
+
### The buy-in run (required)
|
|
117
|
+
earn.stake({ min: 10, max: 100, multiple: 2 });
|
|
118
|
+
Every game declares one, on one top-level line. A player stakes coins at the
|
|
119
|
+
start of a run; a staked run pays its normal earn plus a share of
|
|
120
|
+
stake x multiple (the share is the part of the run's most earn the player
|
|
121
|
+
makes). What it does not win back stays in the reward pool. multiple is 1
|
|
122
|
+
to 2. "stake" is reserved: never a SKU id.
|
|
123
|
+
|
|
124
|
+
### Losable gear (optional)
|
|
125
|
+
store.sku("shield_charm", { coins: 120, grants: { gear: "shield", loseBelow: 0.25 } });
|
|
126
|
+
Owned in the player's profile, coins only. It brings one level of its upgrade
|
|
127
|
+
into every staked run and is lost when that run makes less than loseBelow of
|
|
128
|
+
its most earn.
|
|
129
|
+
|
|
130
|
+
### Coins: earn and the store
|
|
131
|
+
Every game has coins: 1 coin = 1 of the game's own token, shown under the
|
|
132
|
+
token's own name, symbol and logo. The default coin model sells round
|
|
133
|
+
consumables and looks, never permanent stat power:
|
|
134
|
+
store.sku("feather", { coins: 40 }); // one level of the "feather" upgrade for the round
|
|
135
|
+
store.sku("skin_sunset", { coins: 300, grants: { cosmetic: 1 } }); // an outfit, owned for good
|
|
136
|
+
Keep the five SKUs the client is wired to (feather, sprint, shield,
|
|
137
|
+
skin_sunset, skin_frost) at their prices, with the outfits' grants (sunset 1,
|
|
138
|
+
frost 2). "Cosmetics only": economy({ store: "cosmetics" }). PvP pots are not
|
|
139
|
+
part of this template.
|
|
140
|
+
|
|
141
|
+
### Change who the climbers are (the asset library)
|
|
142
|
+
players: { prefab: "style.lowpoly.astronaut", ... } (the template's own is style.lowpoly.rogue) (any character; it plays
|
|
143
|
+
run, idle and jump). Coins draw style.lowpoly.coin, seat bots
|
|
144
|
+
style.lowpoly.mini_female_c, and the cliffs carry trees, rocks and bushes.
|
|
145
|
+
|
|
146
|
+
## Test
|
|
147
|
+
|
|
148
|
+
engine.test.run({ ticks: 60 * 240, bots: 4, assert: [...] }) runs the race
|
|
149
|
+
on the server sim with 4 playtest climbers plus seat bots and reports rounds
|
|
150
|
+
(wave), runs.won (rounds every playtest climber finished), scorers (climbers
|
|
151
|
+
that finished at least once), deaths (falls), earned by rule, softlock, ms per
|
|
152
|
+
tick, and fun.routes (the planner's route per course).
|
|
153
|
+
|
|
154
|
+
## Worked example: src/game.ts
|
|
155
|
+
|
|
156
|
+
The whole game as the template ships it. Every change starts from this file.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
/**
|
|
160
|
+
* SKYLEDGE: the platformer template (play style
|
|
161
|
+
* "platformer"). Up to eight climbers race along cliffside courses to the
|
|
162
|
+
* flag: run, jump (hold for height, tap for a hop), dash across gaps, ride
|
|
163
|
+
* moving platforms, bounce off springs and stay off the thorns. Coins hang
|
|
164
|
+
* along every route; each climber collects their own. A round is one
|
|
165
|
+
* course; the next round runs the next course. Seat bots fill empty places
|
|
166
|
+
* so a round always has a race.
|
|
167
|
+
*
|
|
168
|
+
* This file is the whole game as the model writes it: declarations only,
|
|
169
|
+
* against the typed API in @netforge/engine. Everything else (the
|
|
170
|
+
* motor, netcode, renderer, bots, route planner, referee) is the template's
|
|
171
|
+
* machinery.
|
|
172
|
+
*
|
|
173
|
+
* Course coordinates are metres, side view: x runs right from the start to
|
|
174
|
+
* the flag, y runs up. blocks are [x, y, width, height] from the bottom-left
|
|
175
|
+
* corner; ledges are [x, y, width] one-way platforms; hazards are
|
|
176
|
+
* [x, y, width, height]; springs are [x, y, power]; coins and checkpoints
|
|
177
|
+
* are [x, y].
|
|
178
|
+
*/
|
|
179
|
+
import { earn, game, store, system, upgrade } from "@netforge/engine";
|
|
180
|
+
|
|
181
|
+
/** Ground from x0 to x1, its top at y (4 m deep). */
|
|
182
|
+
const ground = (x0: number, x1: number, y = 0): [number, number, number, number] => [x0, y - 4, x1 - x0, 4];
|
|
183
|
+
/** A row of n coins from x, `gap` metres apart, at height y. */
|
|
184
|
+
const coins = (x: number, y: number, n: number, gap = 1.2): [number, number][] => Array.from({ length: n }, (_, i) => [x + i * gap, y]);
|
|
185
|
+
|
|
186
|
+
export default game({
|
|
187
|
+
template: "platformer",
|
|
188
|
+
style: "lowpoly",
|
|
189
|
+
tickRate: 60,
|
|
190
|
+
places: { cliffs: { chunks: "places/cliffs", spawn: [0, 0, 0] } },
|
|
191
|
+
players: { prefab: "style.lowpoly.rogue", max: 8, bots: 3 },
|
|
192
|
+
// A course has three minutes; the round closes a few seconds after every person has finished.
|
|
193
|
+
round: { seconds: 180 },
|
|
194
|
+
levels: [
|
|
195
|
+
{
|
|
196
|
+
name: "Mossy Steps",
|
|
197
|
+
start: [0, 0],
|
|
198
|
+
flag: [128, 4.5],
|
|
199
|
+
blocks: [
|
|
200
|
+
ground(-8, 24),
|
|
201
|
+
[14, 0, 3, 1.2],
|
|
202
|
+
[17, 0, 3, 2.4],
|
|
203
|
+
ground(28, 50),
|
|
204
|
+
ground(55, 80),
|
|
205
|
+
[62, 5, 7, 0.8],
|
|
206
|
+
ground(90, 100),
|
|
207
|
+
[100, -4, 4, 5.5],
|
|
208
|
+
[104, -4, 4, 7],
|
|
209
|
+
[108, -4, 30, 8.5],
|
|
210
|
+
],
|
|
211
|
+
ledges: [[40, 2.6, 4]],
|
|
212
|
+
hazards: [[35, 0, 2, 0.5], [72, 0, 2, 0.5]],
|
|
213
|
+
springs: [[59, 0, 21]],
|
|
214
|
+
movers: [{ at: [81, 0, 3], to: [5, 0], seconds: 4 }],
|
|
215
|
+
coins: [...coins(4, 1, 5), ...coins(14.8, 2.4, 1), ...coins(18, 3.6, 2), ...coins(41, 3.6, 3), ...coins(63, 6.4, 5), ...coins(84, 1.4, 3), ...coins(112, 5.4, 4)],
|
|
216
|
+
checkpoints: [[31, 0], [92, 0]],
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
name: "Windmill Gap",
|
|
220
|
+
start: [0, 0],
|
|
221
|
+
flag: [150, 6],
|
|
222
|
+
blocks: [
|
|
223
|
+
ground(-8, 18),
|
|
224
|
+
[12, 0, 6, 2],
|
|
225
|
+
ground(24, 34, 2),
|
|
226
|
+
[38, -2, 6, 4],
|
|
227
|
+
ground(52, 70, 1),
|
|
228
|
+
[58, 1, 2, 2.2],
|
|
229
|
+
ground(84, 96, 3),
|
|
230
|
+
[96, -1, 8, 4],
|
|
231
|
+
ground(118, 160, 6),
|
|
232
|
+
[126, 6, 3, 2],
|
|
233
|
+
],
|
|
234
|
+
ledges: [[46, 3.6, 3], [74, 3.4, 3], [104, 4.8, 3], [110, 6.2, 3]],
|
|
235
|
+
hazards: [[64, 1, 3, 0.5], [88, 3, 2, 0.5], [136, 6, 2, 0.5]],
|
|
236
|
+
springs: [],
|
|
237
|
+
movers: [
|
|
238
|
+
{ at: [74, 1, 3], to: [0, 4], seconds: 3.5 },
|
|
239
|
+
{ at: [112, 2, 3], to: [0, 3], seconds: 3, phase: 0.5 },
|
|
240
|
+
],
|
|
241
|
+
coins: [...coins(13, 3, 4), ...coins(26, 3.2, 5), ...coins(47, 4.8, 2), ...coins(58.3, 4, 1), ...coins(85, 4.4, 3), ...coins(105, 6, 2), ...coins(130, 7.4, 6)],
|
|
242
|
+
checkpoints: [[54, 1], [120, 6]],
|
|
243
|
+
},
|
|
244
|
+
{
|
|
245
|
+
name: "Summit Run",
|
|
246
|
+
start: [0, 0],
|
|
247
|
+
flag: [168, 14],
|
|
248
|
+
blocks: [
|
|
249
|
+
ground(-8, 20),
|
|
250
|
+
[20, -4, 6, 5],
|
|
251
|
+
[30, -4, 6, 7],
|
|
252
|
+
ground(42, 60, 3),
|
|
253
|
+
[48, 3, 3, 1],
|
|
254
|
+
ground(66, 82, 5),
|
|
255
|
+
[74, 5, 2, 3],
|
|
256
|
+
[88, 4, 6, 1],
|
|
257
|
+
ground(98, 120, 8),
|
|
258
|
+
[124, 9, 6, 1],
|
|
259
|
+
[134, 10, 6, 1],
|
|
260
|
+
ground(146, 176, 14),
|
|
261
|
+
],
|
|
262
|
+
ledges: [[60, 6.5, 4], [113, 11, 4]],
|
|
263
|
+
hazards: [[54, 3, 3, 0.5], [102, 8, 2, 0.5], [110, 8, 2, 0.5], [156, 14, 2, 0.5]],
|
|
264
|
+
springs: [[16, 0, 22], [80, 5, 22], [118, 8, 21]],
|
|
265
|
+
movers: [{ at: [138, 11, 3], to: [5, 2], seconds: 3.2 }],
|
|
266
|
+
coins: [...coins(8, 1.2, 4), ...coins(16, 6, 3), ...coins(43, 4.4, 4), ...coins(61, 8, 3), ...coins(89, 6.2, 4), ...coins(125, 11, 3), ...coins(150, 15.4, 5)],
|
|
267
|
+
checkpoints: [[44, 3], [100, 8], [148, 14]],
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Run upgrades, sold in the store for the rest of the round (one level each).
|
|
273
|
+
// Platformer upgrades raise speed (run speed), amount (extra air jumps), magnet (coin reach),
|
|
274
|
+
// maxHealth (shield hits per life) or haste (dash cooldown).
|
|
275
|
+
upgrade("feather", { stat: "amount", step: 1, maxLevel: 1 });
|
|
276
|
+
upgrade("sprint", { stat: "speed", step: 0.1, maxLevel: 1 });
|
|
277
|
+
upgrade("shield", { stat: "maxHealth", step: 1, maxLevel: 1 });
|
|
278
|
+
|
|
279
|
+
// A clean climb: reach the flag without a fall.
|
|
280
|
+
system("clean_climb", ({ on, emit }) => {
|
|
281
|
+
const falls = new Map<number, number>();
|
|
282
|
+
on("player.fell", (ids) => {
|
|
283
|
+
for (const id of ids) falls.set(id, (falls.get(id) ?? 0) + 1);
|
|
284
|
+
});
|
|
285
|
+
on("player.finished", (ids) => {
|
|
286
|
+
const clean = ids.filter((id) => (falls.get(id) ?? 0) === 0);
|
|
287
|
+
if (clean.length > 0) emit("climb.clean", clean);
|
|
288
|
+
});
|
|
289
|
+
// A player's earning run started over (signed in, or staked): their count does too.
|
|
290
|
+
on("run.started", (ids) => {
|
|
291
|
+
for (const id of ids) falls.delete(id);
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// Earn (mandatory). Pays the game's coins (1 coin = 1 token), once per round each.
|
|
296
|
+
// Seat bots never earn, and winning a field of bots pays nothing extra. Values are PLACEHOLDER.
|
|
297
|
+
earn.rule("pocketful", { when: "coin.collected", n: 12, pay: 4, oncePer: "run" });
|
|
298
|
+
earn.rule("summit", { when: "player.finished", n: 1, pay: 10, oncePer: "run" });
|
|
299
|
+
earn.rule("clean_climb", { when: "climb.clean", n: 1, pay: 12, oncePer: "run" });
|
|
300
|
+
earn.rule("first_flag", { when: "race.won", n: 1, pay: 25, oncePer: "run" });
|
|
301
|
+
// Buy-in run (the mandatory sink loop): stake coins at run start to earn above free play.
|
|
302
|
+
// A staked run also pays up to stake x multiple, by the share of this run's earn the player makes. PLACEHOLDER.
|
|
303
|
+
earn.stake({ min: 10, max: 100, multiple: 2 });
|
|
304
|
+
|
|
305
|
+
// Store (the Kit), priced in coins: three round upgrades and two cosmetic outfits owned for good.
|
|
306
|
+
store.sku("feather", { coins: 40 });
|
|
307
|
+
store.sku("sprint", { coins: 40 });
|
|
308
|
+
store.sku("shield", { coins: 60 });
|
|
309
|
+
store.sku("skin_sunset", { coins: 300, grants: { cosmetic: 1 } });
|
|
310
|
+
store.sku("skin_frost", { coins: 500, grants: { cosmetic: 2 } });
|
|
311
|
+
// Losable gear: one shield into every staked run, lost when that run makes less than loseBelow of its most earn. PLACEHOLDER.
|
|
312
|
+
store.sku("shield_charm", { coins: 120, grants: { gear: "shield", loseBelow: 0.25 } });
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
## Validate and playtest
|
|
316
|
+
|
|
317
|
+
- 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.
|
|
318
|
+
- 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 (rounds every playtest climber finished), scorers, deaths (falls), earned by rule, softlock, ms per tick and fun.routes (the planner's route per course).
|
|
319
|
+
- 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.
|
|
320
|
+
- Play locally: put `src/game.ts` and a `netnet.json` of `{"template":"platformer"}` 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.
|
|
321
|
+
- Docs: https://forge.netnet.capital/docs/templates/platformer
|
package/llms.txt
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Net Forge platformer template (SKYLEDGE)
|
|
2
2
|
|
|
3
3
|
> A side-view race to the flag for up to 8 players over cliffside courses,
|
|
4
4
|
> with seat bots filling empty places. A round is one course; the next round
|
package/package.json
CHANGED
|
@@ -1,12 +1,30 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netforge/template-platformer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Multiplayer platformer game template for Net Forge: races to the flag with coyote time, jump buffering, dashes, springs and moving platforms (SKYLEDGE).",
|
|
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
|
+
"platformer",
|
|
18
|
+
"physics"
|
|
19
|
+
],
|
|
5
20
|
"license": "MIT",
|
|
6
21
|
"type": "module",
|
|
7
22
|
"exports": {
|
|
8
23
|
".": "./src/manifest.ts",
|
|
9
|
-
"./store": "./src/store.ts"
|
|
24
|
+
"./store": "./src/store.ts",
|
|
25
|
+
"./package.json": "./package.json",
|
|
26
|
+
"./llms.txt": "./llms.txt",
|
|
27
|
+
"./llms-full.txt": "./llms-full.txt"
|
|
10
28
|
},
|
|
11
29
|
"files": [
|
|
12
30
|
"src",
|
|
@@ -15,25 +33,36 @@
|
|
|
15
33
|
"llms.txt",
|
|
16
34
|
"README.md",
|
|
17
35
|
"NOTICE",
|
|
18
|
-
"LICENSE"
|
|
36
|
+
"LICENSE",
|
|
37
|
+
"llms-full.txt",
|
|
38
|
+
"AGENTS.md",
|
|
39
|
+
"CLAUDE.md"
|
|
40
|
+
],
|
|
41
|
+
"sideEffects": [
|
|
42
|
+
"./src/game.ts",
|
|
43
|
+
"./src/manifest.ts",
|
|
44
|
+
"./client/**"
|
|
19
45
|
],
|
|
20
46
|
"publishConfig": {
|
|
21
47
|
"access": "public"
|
|
22
48
|
},
|
|
49
|
+
"homepage": "https://forge.netnet.capital/docs/templates/platformer",
|
|
50
|
+
"bugs": {
|
|
51
|
+
"url": "https://github.com/netnetdev/net-forge/issues"
|
|
52
|
+
},
|
|
23
53
|
"repository": {
|
|
24
54
|
"type": "git",
|
|
25
55
|
"url": "git+https://github.com/netnetdev/net-forge.git",
|
|
26
56
|
"directory": "packages/template-platformer"
|
|
27
57
|
},
|
|
28
|
-
"homepage": "https://github.com/netnetdev/net-forge/tree/main/packages/template-platformer#readme",
|
|
29
58
|
"engines": {
|
|
30
59
|
"node": ">=20"
|
|
31
60
|
},
|
|
32
61
|
"dependencies": {
|
|
33
62
|
"three": "0.186.1",
|
|
34
|
-
"@netforge/asset-library": "0.
|
|
35
|
-
"@netforge/engine": "0.
|
|
36
|
-
"@netforge/engine-sim": "0.
|
|
63
|
+
"@netforge/asset-library": "0.2.0",
|
|
64
|
+
"@netforge/engine": "0.2.0",
|
|
65
|
+
"@netforge/engine-sim": "0.2.0"
|
|
37
66
|
},
|
|
38
67
|
"devDependencies": {
|
|
39
68
|
"@types/node": "22.20.1",
|
|
@@ -43,9 +72,6 @@
|
|
|
43
72
|
"vite": "6.4.3",
|
|
44
73
|
"vitest": "3.2.7"
|
|
45
74
|
},
|
|
46
|
-
"bugs": {
|
|
47
|
-
"url": "https://github.com/netnetdev/net-forge/issues"
|
|
48
|
-
},
|
|
49
75
|
"scripts": {
|
|
50
76
|
"build": "node ../engine/scripts/fresh-dist.mjs && vite build --config client/vite.config.ts",
|
|
51
77
|
"typecheck": "tsc -p tsconfig.json",
|