@netforge/template-arena 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +75 -0
- package/CLAUDE.md +11 -0
- package/LICENSE +21 -0
- package/NOTICE +34 -0
- package/README.md +82 -2
- package/client/audio.ts +43 -0
- package/client/decor.ts +22 -0
- package/client/host.ts +96 -0
- package/client/hud.ts +330 -0
- package/client/index.html +230 -0
- package/client/input.ts +233 -0
- package/client/library.ts +146 -0
- package/client/main.ts +544 -0
- package/client/names.ts +43 -0
- package/client/palette.ts +35 -0
- package/client/scene.ts +1075 -0
- package/client/stake.ts +143 -0
- package/llms-full.txt +346 -0
- package/llms.txt +164 -0
- package/package.json +80 -3
- package/src/game.ts +117 -0
- package/src/manifest.ts +38 -0
- package/src/store.ts +109 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Extending VOLTYARD
|
|
2
|
+
|
|
3
|
+
VOLTYARD is the arena shooter template of the Net Forge game engine. The template already has loadouts, aiming, projectiles, lag-compensated beams, death and respawn, heal drops, rounds, a scoreboard, seat bots, netcode, rendering, touch and gamepad controls, earn and the store. You change the game by changing declarations. Read `llms.txt` for every field, and `llms-full.txt` for the complete worked example.
|
|
4
|
+
|
|
5
|
+
## The one file to edit
|
|
6
|
+
|
|
7
|
+
- `src/game.ts` is the whole game. Change declarations in place.
|
|
8
|
+
- `src/names.json`, next to it, holds the title and every label players read (weapons, upgrades, earn rules, store items). Add a label there for every new id.
|
|
9
|
+
- Everything else (`src/manifest.ts`, `src/store.ts`, `client/`, `test/`, `scripts/`) is template machinery. Do not edit it.
|
|
10
|
+
|
|
11
|
+
## Imports
|
|
12
|
+
|
|
13
|
+
`src/game.ts` imports only from `@netforge/engine`. The shipped game uses:
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { earn, game, prefab, store, system, upgrade, weapon } from "@netforge/engine";
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Also available for this genre: `economy` and `metaUpgrade` (opt-in permanent upgrades, capped by the engine), and `earn.pot` (an optional PvP pot, arena only).
|
|
20
|
+
|
|
21
|
+
## Declarations
|
|
22
|
+
|
|
23
|
+
- `game({ template: "arena", style, places, players: { prefab, max, bots, loadout, respawn, drop }, arena: { halfSize, pillars, spawns }, round: { seconds, frags } })`.
|
|
24
|
+
- `weapon(id, { kind, damage, cooldown, range, ... })` with kind `"blaster"`, `"beam"` or `"nova"` only.
|
|
25
|
+
- `prefab(id, { model, kind: "pickup", value })` for the heal cell and other pickups. A declared enemy prefab spawned by a system chases the nearest player.
|
|
26
|
+
- `upgrade(id, { stat, step, maxLevel })` for round upgrades the store sells.
|
|
27
|
+
- `system(id, (ctx) => { ... })` with `every`, `after`, `onTick`, `spawn`, `spawnRing`, `alive`, `players`, `emit`, `on`, `end`, `random`, `tick`, `time`.
|
|
28
|
+
- `earn.rule(id, { when, n, pay, oncePer: "run" })`. Events: `opponent.killed`, `player.died`, `round.top`, `round.finished`, `run.won`, and anything a system emits.
|
|
29
|
+
- `earn.stake({ min, max, multiple })`, exactly one, on one top-level line.
|
|
30
|
+
- `store.sku(id, { coins, grants? })`.
|
|
31
|
+
|
|
32
|
+
## Rules the build enforces
|
|
33
|
+
|
|
34
|
+
- Ids are lower_snake_case.
|
|
35
|
+
- Every game declares at least one `earn.rule` and exactly one `earn.stake`. A game without them does not build. `"stake"` and `"pot"` are reserved and are never a SKU or upgrade id.
|
|
36
|
+
- `players.loadout` names 1 to 4 declared weapons. Arena weapons are `blaster`, `beam` or `nova`, never `bolt` or `orbit`. Keep beam cooldowns at 0.8 s or more.
|
|
37
|
+
- Pillars fit on the floor. Spawns sit 1 m inside the floor and 1 m clear of pillars.
|
|
38
|
+
- Asset ids are library ids such as `"style.lowpoly.mini_male_f"`, never URLs or paths, and must exist in the asset library.
|
|
39
|
+
- Keep `game({ style })` as the creator picked it: lowpoly, toon, ps1, pixel, voxel or handdrawn.
|
|
40
|
+
- Bots never earn, and killing a bot pays no earn rule.
|
|
41
|
+
- In a 4-minute playtest, 4 players plus seat bots must all score kills and an earn rule must fire. A round should end within about 4 minutes.
|
|
42
|
+
- Keep the five SKUs the client is wired to at their prices: `plating`, `servo`, `capacitor`, `skin_neon` (cosmetic 1) and `skin_chrome` (cosmetic 2).
|
|
43
|
+
|
|
44
|
+
## Validate
|
|
45
|
+
|
|
46
|
+
- With the MCP server (`npx -y @netforge/mcp`), call `netforge_validate_game` on `src/game.ts`. It returns one actionable line per problem.
|
|
47
|
+
- In this package, run the tests with vitest: `test/harness.test.ts`, `test/host.test.ts`, `test/newcomer.test.ts` and `test/store.test.ts`.
|
|
48
|
+
- A bad declaration throws `GameDefinitionError` with a line that says what to change.
|
|
49
|
+
|
|
50
|
+
## Playtest
|
|
51
|
+
|
|
52
|
+
- With the MCP server, call `netforge_run_playtest`.
|
|
53
|
+
- In code, import the game module and run the headless harness:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { engine } from "@netforge/engine";
|
|
57
|
+
import "./src/game.js";
|
|
58
|
+
|
|
59
|
+
const report = await engine.test.run({ ticks: 60 * 240, bots: 4, seed: 1 });
|
|
60
|
+
console.log(report.kills, report.deaths, report.earned, report.softlock);
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The report has kills, deaths, rounds (`runs.won`), `topScore`, `scorers`, earn by rule, softlock and ms per tick.
|
|
64
|
+
|
|
65
|
+
## Play locally
|
|
66
|
+
|
|
67
|
+
In a folder with `netnet.json` (`{"template":"arena"}`) and `src/game.ts`, with `@netforge/kit-play` and `@netforge/engine` installed, run `npx kit-play`. It opens http://127.0.0.1:4663 with play money. The MCP tool `netforge_play_locally` gives the same steps.
|
|
68
|
+
|
|
69
|
+
## Do not
|
|
70
|
+
|
|
71
|
+
- Do not write renderers, netcode, physics or bot AI. The template has them.
|
|
72
|
+
- Do not reference assets by URL or file path.
|
|
73
|
+
- Do not import anything other than `@netforge/engine` in `src/game.ts`.
|
|
74
|
+
- Do not edit `client/`, `src/manifest.ts` or `src/store.ts`.
|
|
75
|
+
- Do not rename or reprice the SKUs the client is wired to.
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This package is VOLTYARD, a Net Forge game template. Read AGENTS.md before changing this game. It lists the declarations this genre has, the rules the build enforces, and how to validate, playtest and play the game locally.
|
|
4
|
+
|
|
5
|
+
The five hard rules:
|
|
6
|
+
|
|
7
|
+
1. Edit only src/game.ts (and src/names.json for the labels players read).
|
|
8
|
+
2. Import only from @netforge/engine.
|
|
9
|
+
3. Ids are lower_snake_case.
|
|
10
|
+
4. Declare at least one earn.rule and exactly one earn.stake.
|
|
11
|
+
5. Never write renderers, netcode, physics or bot AI, and never reference assets by URL or file path (use library ids such as "style.lowpoly.frog").
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NetNet Capital Management
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/NOTICE
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@netforge/template-arena (VOLTYARD)
|
|
2
|
+
Copyright (c) 2026 NetNet Capital Management
|
|
3
|
+
|
|
4
|
+
An original game. No names, characters, maps, models, sounds or code from
|
|
5
|
+
any other game or franchise.
|
|
6
|
+
|
|
7
|
+
Art and sound
|
|
8
|
+
- Models come from the per-style asset library
|
|
9
|
+
(@netforge/asset-library): CC0 1.0 models by Kenney (Mini
|
|
10
|
+
Characters, Blaster Kit, Space Kit, Car Kit), each pack listed with its
|
|
11
|
+
source URL and licence verification in that package's NOTICE and
|
|
12
|
+
manifest/packs.json. The files are fetched and built, never committed
|
|
13
|
+
here; the build copies only the models this game uses.
|
|
14
|
+
- Fallback shapes (client/library.ts) are generated from three.js
|
|
15
|
+
primitives; all textures are drawn on a canvas at load; all sounds are
|
|
16
|
+
synthesized with WebAudio oscillators (client/audio.ts).
|
|
17
|
+
|
|
18
|
+
Dependencies (unmodified, via npm)
|
|
19
|
+
- three.js 0.186.1 (https://github.com/mrdoob/three.js), MIT.
|
|
20
|
+
- nipplejs 1.0.4 (https://github.com/yoannmoinet/nipplejs), MIT.
|
|
21
|
+
- @netforge/engine and @netforge/engine-sim (this repository);
|
|
22
|
+
see their NOTICE files (bitECS MPL-2.0, Rapier Apache-2.0, Colyseus MIT,
|
|
23
|
+
zod MIT).
|
|
24
|
+
|
|
25
|
+
Patterns (ideas only)
|
|
26
|
+
- The arena shooter genre's general mechanics (twin-stick aim, weapon
|
|
27
|
+
loadouts, respawns, frag limits, health drops, a scoreboard). Mechanics,
|
|
28
|
+
not expression; no level, weapon, character or text was copied from any
|
|
29
|
+
game.
|
|
30
|
+
- Lag compensation for hitscan as described in Valve's public Source
|
|
31
|
+
multiplayer networking write-up (ideas only; implemented in the Net Forge
|
|
32
|
+
room server and engine-sim's arena seam).
|
|
33
|
+
- Hit-stop, screen shake and damage numbers as described in common
|
|
34
|
+
game-feel talks and articles (e.g. "Juice it or lose it", GDC 2012).
|
package/README.md
CHANGED
|
@@ -1,3 +1,83 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @netforge/template-arena
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
An arena shooter game template for Net Forge: a twin-stick shooter for up to 16 players, with bots that fill empty seats (VOLTYARD).
|
|
4
|
+
|
|
5
|
+
Wirejacks duel at night in a yard of humming transformers. Players move with one hand, aim with the other and hold fire, swapping between an arc rifle, a scatter coil and a rail lance. Every kill drops a heal cell. A round lasts three minutes or ends at 25 kills, and the top of the board takes the round. Bots fill empty seats, so one player alone still gets a fight.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
npm install @netforge/template-arena
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
You rarely import this package directly. kit play (`@netforge/kit-play`) and the Net Forge MCP server use it to build and run a game whose `src/game.ts` you write.
|
|
14
|
+
|
|
15
|
+
## Play it locally
|
|
16
|
+
|
|
17
|
+
```sh
|
|
18
|
+
mkdir -p my-game/src && cd my-game
|
|
19
|
+
npm init -y && npm install @netforge/kit-play @netforge/engine @netforge/template-arena
|
|
20
|
+
cp node_modules/@netforge/template-arena/src/game.ts src/
|
|
21
|
+
echo '{"template":"arena"}' > netnet.json
|
|
22
|
+
npx kit-play # opens http://127.0.0.1:4663
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Open the Invite link (`?room=<id>`) in another tab to play together, or start with `--host 0.0.0.0` and open it on a phone on the same network. Coins are play money. Real-token earning, stakes and claims run only on the hosted Net Forge server.
|
|
26
|
+
|
|
27
|
+
## Change the game
|
|
28
|
+
|
|
29
|
+
Edit `src/game.ts` and run `npx kit-play` again. For example, make the arc rifle hit harder and fire a little slower:
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
weapon("arc_rifle", { kind: "blaster", damage: 16, cooldown: 0.2, range: 26, speed: 44, knockback: 0 });
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or shorten the round and lower the kill target in `game({ ... })`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
round: { seconds: 120, frags: 15 },
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## How it plays on the server
|
|
42
|
+
|
|
43
|
+
- Server-authoritative at 60 Hz. Your own wirejack is predicted (move, aim, weapon, cooldowns), and everyone else is drawn about 100 ms in the past.
|
|
44
|
+
- The rail lance is a hitscan resolved with lag compensation: the server rewinds the other players to what you saw, capped at 200 ms. Rifle and scatter shots are server projectiles.
|
|
45
|
+
- Seat bots (`players.bots`) join while at least one person is in the room and leave as people join. They adapt to each person's record in the round, ease off when they lead, never earn and never reach the frag limit. Killing a bot scores and shows in the feed but pays no earn rule.
|
|
46
|
+
- Spawns are out of every opponent's sight and 12 m clear, with 2 s of protection on joining, respawning and each new round.
|
|
47
|
+
- Earn: kills of people (3 and 12 in a round), a five-kill streak, and topping the board with another person in the round. Bot kills and a board of bots pay nothing. The room server is the referee.
|
|
48
|
+
|
|
49
|
+
## Files
|
|
50
|
+
|
|
51
|
+
| Path | What |
|
|
52
|
+
|---|---|
|
|
53
|
+
| `src/game.ts` | The game, as declarations only. This is the file you edit. |
|
|
54
|
+
| `src/manifest.ts` | Loads and compiles the game; `arenaAdapter()` for the room host and the client. |
|
|
55
|
+
| `src/store.ts` | Store hooks on the Kit's API names (`kit.coins.*`, `kit.store.*`): three round upgrades and two finishes. |
|
|
56
|
+
| `client/` | The browser client: three.js scene, HUD, twin-stick input (keys and mouse, gamepad, touch), game feel. |
|
|
57
|
+
| `test/` | Playtest bots through `engine.test.run`, the newcomer test and the store test. |
|
|
58
|
+
| `llms.txt`, `llms-full.txt` | How to change the game, for agents. |
|
|
59
|
+
|
|
60
|
+
## Controls
|
|
61
|
+
|
|
62
|
+
WASD to move, mouse to aim, click to fire, 1 2 3 or Q to change weapon, Space to dash, Tab for the board. On touch, the left stick moves, the right stick aims and fires, and SWAP and DASH are buttons. The backtick key shows stats.
|
|
63
|
+
|
|
64
|
+
## For AI agents
|
|
65
|
+
|
|
66
|
+
Read `AGENTS.md` and `llms.txt` before editing. The Net Forge MCP server gives a coding agent the templates, the API and a playtest loop:
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
npx -y @netforge/mcp
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The package is `@netforge/mcp` and its bin is `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`.
|
|
73
|
+
|
|
74
|
+
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`.
|
|
75
|
+
|
|
76
|
+
## Docs
|
|
77
|
+
|
|
78
|
+
- https://forge.netnet.capital/docs/templates/arena
|
|
79
|
+
- `llms.txt` and `llms-full.txt` in this package. The full file includes the complete `src/game.ts` and the validate and playtest steps.
|
|
80
|
+
|
|
81
|
+
## License
|
|
82
|
+
|
|
83
|
+
MIT. See LICENSE and NOTICE.
|
package/client/audio.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VOLTYARD's sound cues on the engine's FeelAudio (FeelKit). Shots, downs
|
|
3
|
+
* and heals play real CC0 samples from the asset library's audio section
|
|
4
|
+
* (ARENA_SAMPLES, loaded by the engine's SoundPlayer with a music loop);
|
|
5
|
+
* the FeelKit's own cues (impact, bigKill, hurt) take the library's samples
|
|
6
|
+
* by name. Two cues stay synthesized on purpose so you can tell them apart
|
|
7
|
+
* without looking: the hit tick and the two-note kill confirm. Every cue
|
|
8
|
+
* keeps a synth recipe as the fallback when a sample cannot load. Others'
|
|
9
|
+
* cues ("-o") are played quieter. Starts on the first user gesture.
|
|
10
|
+
*/
|
|
11
|
+
import type { FeelAudio } from "@netforge/engine/runtime";
|
|
12
|
+
|
|
13
|
+
/** Arena cue name -> library sound id (soundBank's `cues` overrides). */
|
|
14
|
+
export const ARENA_SAMPLES: Readonly<Record<string, string>> = {
|
|
15
|
+
zap: "audio.sfx.shoot",
|
|
16
|
+
"zap-o": "audio.sfx.shoot",
|
|
17
|
+
wide: "audio.sfx.shoot",
|
|
18
|
+
"wide-o": "audio.sfx.shoot",
|
|
19
|
+
down: "audio.sfx.downed",
|
|
20
|
+
"down-o": "audio.sfx.kill",
|
|
21
|
+
heal: "audio.sfx.revive",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function defineArenaCues(a: FeelAudio): void {
|
|
25
|
+
a.define("zap", { wave: "square", f0: 1300, f1: 500, ms: 50, gain: 0.35 }, { minGapMs: 40, voices: 3 });
|
|
26
|
+
a.define("zap-o", { wave: "square", f0: 1300, f1: 500, ms: 50, gain: 0.35 }, { minGapMs: 40, voices: 3 });
|
|
27
|
+
a.define("wide", { wave: "square", f0: 260, f1: 60, ms: 140, gain: 0.6 }, { minGapMs: 60, voices: 2 });
|
|
28
|
+
a.define("wide-o", { wave: "square", f0: 260, f1: 60, ms: 140, gain: 0.6 }, { minGapMs: 60, voices: 2 });
|
|
29
|
+
a.define("beam", { wave: "sawtooth", f0: 1800, f1: 120, ms: 220, gain: 0.7 }, { minGapMs: 60, voices: 2 });
|
|
30
|
+
a.define("beam-o", { wave: "sawtooth", f0: 1800, f1: 120, ms: 220, gain: 0.7 }, { minGapMs: 60, voices: 2 });
|
|
31
|
+
a.define("hitmark", { wave: "triangle", f0: 2600, f1: 1900, ms: 40, gain: 0.55 }, { minGapMs: 35, voices: 2, pitchVar: 0.03 });
|
|
32
|
+
a.define("confirm", { wave: "triangle", f0: 660, f1: 990, ms: 120, gain: 0.8 }, { minGapMs: 80, voices: 2, pitchVar: 0 });
|
|
33
|
+
a.define("confirm2", { wave: "triangle", f0: 990, f1: 1980, ms: 200, gain: 0.8 }, { minGapMs: 80, voices: 2, pitchVar: 0 });
|
|
34
|
+
a.define("down-o", { wave: "triangle", f0: 520, f1: 260, ms: 160, gain: 0.3 }, { minGapMs: 120, voices: 2 });
|
|
35
|
+
a.define("down", { wave: "sawtooth", f0: 300, f1: 40, ms: 600, gain: 0.8 }, { minGapMs: 300, voices: 1 });
|
|
36
|
+
a.define("heal", { wave: "sine", f0: 440, f1: 880, ms: 180, gain: 0.6 }, { minGapMs: 150, voices: 1 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Our kill: two rising notes. */
|
|
40
|
+
export function killConfirm(a: FeelAudio): void {
|
|
41
|
+
a.play("confirm");
|
|
42
|
+
setTimeout(() => a.play("confirm2"), 90);
|
|
43
|
+
}
|
package/client/decor.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Library models the VOLTYARD scene draws on its own (template machinery,
|
|
3
|
+
* not game content). The build copies these with the models game.ts uses.
|
|
4
|
+
*/
|
|
5
|
+
/** Seat bots: a different townsperson from the players' model. */
|
|
6
|
+
export const BOT_MODEL = "style.lowpoly.mini_male_e";
|
|
7
|
+
/** The blaster every wirejack holds. */
|
|
8
|
+
export const HELD_MODEL = "style.lowpoly.blaster_h";
|
|
9
|
+
/** Each transformer pillar: a generator machine. */
|
|
10
|
+
export const PILLAR_MODEL = "style.lowpoly.generator";
|
|
11
|
+
/** Wirejacks are drawn this tall, m. */
|
|
12
|
+
export const PLAYER_HEIGHT = 1.9;
|
|
13
|
+
|
|
14
|
+
/** [library id, height m, how many] parked outside the fence. */
|
|
15
|
+
export const YARD_DECOR: readonly (readonly [string, number, number])[] = [
|
|
16
|
+
["style.lowpoly.truck_flat", 2, 3],
|
|
17
|
+
["style.lowpoly.van", 2, 3],
|
|
18
|
+
["style.lowpoly.barrels", 1.3, 6],
|
|
19
|
+
["style.lowpoly.satellite_dish", 3.2, 2],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
export const SCENE_MODELS: readonly string[] = [BOT_MODEL, HELD_MODEL, PILLAR_MODEL, ...YARD_DECOR.map(([id]) => id)];
|
package/client/host.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Net Forge as host. Framed by Net Forge, the page URL carries
|
|
3
|
+
* the Kit flags and the host posts one message; played on its own, none of
|
|
4
|
+
* this applies and the game behaves exactly as before.
|
|
5
|
+
*
|
|
6
|
+
* ?host=netnet the host draws Invite and sign-in; the game hides its own
|
|
7
|
+
* ?coin=TICKER the coin the HUD names (2 to 6 capitals or digits)
|
|
8
|
+
* ?title=NAME the game's own name for the wordmark and the tab title
|
|
9
|
+
* ?player=guest no wallet is connected: play pays nothing, and the HUD
|
|
10
|
+
* says "not earning" instead of a balance
|
|
11
|
+
* ?player=play the token has not launched: coins are play money
|
|
12
|
+
*
|
|
13
|
+
* The player flag changes while the game runs (a wallet connects), so the
|
|
14
|
+
* host also posts { kind: "netnet-kit", type: "player", mode }. Only the
|
|
15
|
+
* parent window is listened to, and only when the URL says it is the host.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type PlayerMode = "earning" | "guest" | "play";
|
|
19
|
+
|
|
20
|
+
export interface HostFlags {
|
|
21
|
+
/** Framed by Net Forge. */
|
|
22
|
+
readonly framed: boolean;
|
|
23
|
+
/** The coin's ticker, when the host names one. */
|
|
24
|
+
readonly coin: string | null;
|
|
25
|
+
/** The game's own name, when the host names one. */
|
|
26
|
+
readonly title: string | null;
|
|
27
|
+
/** What play pays the person at the screen. Standalone play is "earning" (the game's own kit decides). */
|
|
28
|
+
readonly player: PlayerMode;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const TICKER_RE = /^[A-Z0-9]{2,6}$/;
|
|
32
|
+
|
|
33
|
+
export function hostFlags(params: URLSearchParams): HostFlags {
|
|
34
|
+
const framed = params.get("host") === "netnet";
|
|
35
|
+
const coin = params.get("coin");
|
|
36
|
+
// eslint-disable-next-line no-control-regex
|
|
37
|
+
const title = (params.get("title") ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").replace(/\s+/g, " ").trim().slice(0, 40);
|
|
38
|
+
const player = params.get("player");
|
|
39
|
+
return {
|
|
40
|
+
framed,
|
|
41
|
+
coin: coin && TICKER_RE.test(coin) ? coin : null,
|
|
42
|
+
title: title || null,
|
|
43
|
+
player: framed && (player === "guest" || player === "play") ? player : "earning",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* The player mode the host named on the URL, or null when it named none (a
|
|
49
|
+
* connected player on a live game gets no flag, only the posted message) or
|
|
50
|
+
* the page is not framed by Net Forge.
|
|
51
|
+
*/
|
|
52
|
+
export function namedPlayer(params: URLSearchParams): PlayerMode | null {
|
|
53
|
+
if (params.get("host") !== "netnet") return null;
|
|
54
|
+
const player = params.get("player");
|
|
55
|
+
return player === "earning" || player === "guest" || player === "play" ? player : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The store's note under its prices, from one source of truth. Once the host
|
|
60
|
+
* has named the player mode (on the URL or in a message), that mode decides,
|
|
61
|
+
* exactly as it does for the coin box: a live game never calls its coins play
|
|
62
|
+
* money. Standalone play, or a host that names no mode (the studio's drafts),
|
|
63
|
+
* keeps the room's own word (`welcome.earnLive`).
|
|
64
|
+
*/
|
|
65
|
+
export function storeNote(hostMode: PlayerMode | null, earnLive: boolean, symbol: string): string {
|
|
66
|
+
if (hostMode === "play") return "Play money until launch";
|
|
67
|
+
if (hostMode === "guest") return `Connect a wallet to earn ${symbol}`;
|
|
68
|
+
if (hostMode === "earning") return "";
|
|
69
|
+
return earnLive ? "" : "Play money until launch";
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A host message, checked: anything else is ignored. */
|
|
73
|
+
export function playerFrom(data: unknown): PlayerMode | null {
|
|
74
|
+
if (typeof data !== "object" || data === null) return null;
|
|
75
|
+
const m = data as Record<string, unknown>;
|
|
76
|
+
if (m["kind"] !== "netnet-kit" || m["type"] !== "player") return null;
|
|
77
|
+
const mode = m["mode"];
|
|
78
|
+
return mode === "earning" || mode === "guest" || mode === "play" ? mode : null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Calls `cb` with each player mode the host posts. Framed pages only. */
|
|
82
|
+
export function onHostPlayer(flags: HostFlags, cb: (mode: PlayerMode) => void): void {
|
|
83
|
+
if (!flags.framed || window.parent === window) return;
|
|
84
|
+
window.addEventListener("message", (e: MessageEvent<unknown>) => {
|
|
85
|
+
if (e.source !== window.parent) return;
|
|
86
|
+
const mode = playerFrom(e.data);
|
|
87
|
+
if (mode) cb(mode);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Tells the host the game is up, so it can post the current player mode. */
|
|
92
|
+
export function postReady(flags: HostFlags): void {
|
|
93
|
+
if (!flags.framed || window.parent === window) return;
|
|
94
|
+
// The host's origin is not known to an opaque-origin frame; the message is one public flag.
|
|
95
|
+
window.parent.postMessage({ kind: "netnet-kit", type: "ready" }, "*");
|
|
96
|
+
}
|