@netforge/kit-play 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/LICENSE +21 -0
- package/NOTICE +13 -0
- package/README.md +85 -2
- package/dist/check.d.ts +43 -0
- package/dist/check.js +106 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +229 -0
- package/dist/host/core.d.ts +50 -0
- package/dist/host/core.js +247 -0
- package/dist/host/entry.d.ts +20 -0
- package/dist/host/entry.js +37 -0
- package/dist/host/profile.d.ts +29 -0
- package/dist/host/profile.js +163 -0
- package/dist/host/room.d.ts +37 -0
- package/dist/host/room.js +199 -0
- package/dist/host/server.d.ts +28 -0
- package/dist/host/server.js +255 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/names.d.ts +18 -0
- package/dist/names.js +64 -0
- package/dist/play.d.ts +51 -0
- package/dist/play.js +237 -0
- package/dist/templates.d.ts +21 -0
- package/dist/templates.js +34 -0
- package/llms.txt +41 -0
- package/package.json +87 -3
package/dist/play.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// `kit play`: run a downloaded Net Forge game on this computer with play
|
|
2
|
+
// money. Inside the game's folder (netnet.json, src/game.ts, src/names.json)
|
|
3
|
+
// it builds the template's browser client with the creator's game module
|
|
4
|
+
// and labels swapped in, bundles a local room host around the same game
|
|
5
|
+
// module, serves both on one port and opens the browser.
|
|
6
|
+
//
|
|
7
|
+
// What it never does: connect a wallet, read or write a chain, talk to a
|
|
8
|
+
// Net Forge server, run the referee, or pay anyone. The page is built with
|
|
9
|
+
// no VITE_* environment, so a hosted room or library URL in the shell can
|
|
10
|
+
// never be baked in; the host binds to 127.0.0.1 unless --host says
|
|
11
|
+
// otherwise, and answers every sign-in and money route with 403.
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
|
|
13
|
+
import { createRequire } from "node:module";
|
|
14
|
+
import { dirname, join, resolve } from "node:path";
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
16
|
+
import { spawn } from "node:child_process";
|
|
17
|
+
import { networkInterfaces } from "node:os";
|
|
18
|
+
import { namesModule, parseNames } from "./names.js";
|
|
19
|
+
import { templateFor } from "./templates.js";
|
|
20
|
+
/** The only import a game module may make (the studio's rule, kept here so a game that plays locally plays in the studio). */
|
|
21
|
+
export const ALLOWED_IMPORT = "@netforge/engine";
|
|
22
|
+
export class PlayError extends Error {
|
|
23
|
+
}
|
|
24
|
+
/** Reads and checks the game folder. */
|
|
25
|
+
export function readGameFolder(dir) {
|
|
26
|
+
const metaFile = join(dir, "netnet.json");
|
|
27
|
+
if (!existsSync(metaFile))
|
|
28
|
+
throw new PlayError(`No netnet.json in ${dir}. Run kit play inside a game downloaded from Net Forge.`);
|
|
29
|
+
let meta;
|
|
30
|
+
try {
|
|
31
|
+
meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new PlayError("netnet.json is not valid JSON.");
|
|
35
|
+
}
|
|
36
|
+
const template = templateFor(meta.template);
|
|
37
|
+
if (!template)
|
|
38
|
+
throw new PlayError(`netnet.json names an unknown template: ${String(meta.template)}`);
|
|
39
|
+
const gameFile = join(dir, "src", "game.ts");
|
|
40
|
+
if (!existsSync(gameFile))
|
|
41
|
+
throw new PlayError("src/game.ts is missing.");
|
|
42
|
+
const namesFile = join(dir, "src", "names.json");
|
|
43
|
+
let names = null;
|
|
44
|
+
if (existsSync(namesFile)) {
|
|
45
|
+
try {
|
|
46
|
+
names = parseNames(readFileSync(namesFile, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch (e) {
|
|
49
|
+
throw new PlayError(e instanceof Error ? e.message : String(e));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return { dir, template, gameFile, names };
|
|
53
|
+
}
|
|
54
|
+
/** Where a package is installed, looked up from the game folder first, then from kit-play itself. */
|
|
55
|
+
function packageDir(pkg, from, entryToRoot) {
|
|
56
|
+
const bases = [join(from, "package.json"), fileURLToPath(import.meta.url)];
|
|
57
|
+
for (const base of bases) {
|
|
58
|
+
try {
|
|
59
|
+
return entryToRoot(createRequire(base).resolve(pkg));
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
// next
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw new PlayError(`${pkg} is not installed. Run npm install in the game folder.`);
|
|
66
|
+
}
|
|
67
|
+
/** The template package's root (its "." export is src/manifest.ts). */
|
|
68
|
+
export function templateDir(t, from) {
|
|
69
|
+
return packageDir(t.pkg, from, (p) => dirname(dirname(p)));
|
|
70
|
+
}
|
|
71
|
+
/** The built asset library shipped in @netforge/asset-library-files, or null when it is not installed. */
|
|
72
|
+
export function libraryDir(from) {
|
|
73
|
+
try {
|
|
74
|
+
const dir = packageDir("@netforge/asset-library-files/package.json", from, (p) => dirname(p));
|
|
75
|
+
const lib = join(dir, "library");
|
|
76
|
+
return existsSync(join(lib, "index.json")) ? lib : null;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The page link for a host base URL. The template clients connect to the
|
|
84
|
+
* room named by ?server= (without it they fall back to their dev port), so
|
|
85
|
+
* every link carries the server it was served from.
|
|
86
|
+
*/
|
|
87
|
+
export function playLink(base, libraryUrl) {
|
|
88
|
+
const params = new URLSearchParams({ server: base });
|
|
89
|
+
if (libraryUrl)
|
|
90
|
+
params.set("library", libraryUrl);
|
|
91
|
+
return `${base}/?${params.toString()}`;
|
|
92
|
+
}
|
|
93
|
+
/** Links other devices can open when the host listens on every interface: one per IPv4 network address. */
|
|
94
|
+
export function lanLinks(host, port, interfaces, libraryUrl) {
|
|
95
|
+
const wildcard = host === "0.0.0.0" || host === "::";
|
|
96
|
+
const addrs = wildcard
|
|
97
|
+
? Object.values(interfaces).flatMap((list) => (list ?? []).flatMap(({ family, internal, address }) => (family === "IPv4" && !internal ? [address] : [])))
|
|
98
|
+
: host !== "127.0.0.1" && host !== "localhost" && host !== "::1" && /^\d+\.\d+\.\d+\.\d+$/.test(host)
|
|
99
|
+
? [host]
|
|
100
|
+
: [];
|
|
101
|
+
return [...new Set(addrs)].map((a) => playLink(`http://${a}:${port}`, libraryUrl));
|
|
102
|
+
}
|
|
103
|
+
/** Builds the template client with the game module and labels swapped in. */
|
|
104
|
+
export async function buildClient(g, tplDir, outDir, cacheDir) {
|
|
105
|
+
const vite = await import("vite");
|
|
106
|
+
// Vite hands plugins real, forward-slash paths: compare in that form (Windows, symlinked folders).
|
|
107
|
+
const norm = (p) => vite.normalizePath(realpathSync(p));
|
|
108
|
+
const manifest = norm(join(tplDir, "src", "manifest.ts"));
|
|
109
|
+
const clientDir = norm(join(tplDir, "client"));
|
|
110
|
+
const templateNames = norm(join(tplDir, "client", "names.ts"));
|
|
111
|
+
const gameFile = norm(g.gameFile);
|
|
112
|
+
const NAMES_ID = "\0kit-play-names.js";
|
|
113
|
+
const plugin = {
|
|
114
|
+
name: "kit-play-game",
|
|
115
|
+
enforce: "pre",
|
|
116
|
+
async resolveId(source, importer) {
|
|
117
|
+
if (!importer)
|
|
118
|
+
return null;
|
|
119
|
+
if (importer === manifest && (source === "./game.js" || source === "./game.ts"))
|
|
120
|
+
return gameFile;
|
|
121
|
+
if (importer === gameFile) {
|
|
122
|
+
if (source === ALLOWED_IMPORT)
|
|
123
|
+
return this.resolve(source, manifest, { skipSelf: true });
|
|
124
|
+
this.error(`src/game.ts may import only "${ALLOWED_IMPORT}", not "${source}"`);
|
|
125
|
+
}
|
|
126
|
+
if (g.names && importer.startsWith(clientDir) && importer !== templateNames && (source === "./names.js" || source === "./names.ts"))
|
|
127
|
+
return NAMES_ID;
|
|
128
|
+
return null;
|
|
129
|
+
},
|
|
130
|
+
load(id) {
|
|
131
|
+
return id === NAMES_ID && g.names ? namesModule(g.names, templateNames) : null;
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
await vite.build({
|
|
135
|
+
configFile: false,
|
|
136
|
+
root: clientDir,
|
|
137
|
+
base: "./",
|
|
138
|
+
logLevel: "warn",
|
|
139
|
+
publicDir: false,
|
|
140
|
+
cacheDir,
|
|
141
|
+
// No VITE_* variables from the shell or a .env file reach the page: it always talks to this host.
|
|
142
|
+
envDir: join(cacheDir, "no-env"),
|
|
143
|
+
envPrefix: "KIT_PLAY_NEVER_",
|
|
144
|
+
plugins: [plugin],
|
|
145
|
+
build: { outDir, emptyOutDir: true, target: "es2022", chunkSizeWarningLimit: 16384, reportCompressedSize: false },
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
/** The esbuild plugin that puts the game folder's src/game.ts in place of the template's and holds it to ALLOWED_IMPORT. */
|
|
149
|
+
export function gameSwap(manifest, gameFile, tplDir) {
|
|
150
|
+
return {
|
|
151
|
+
name: "kit-play-swap",
|
|
152
|
+
setup(b) {
|
|
153
|
+
b.onResolve({ filter: /^\.\/game\.(js|ts)$/ }, (args) => (args.importer === manifest ? { path: gameFile } : undefined));
|
|
154
|
+
b.onResolve({ filter: /.*/ }, async (args) => {
|
|
155
|
+
if (args.importer !== gameFile)
|
|
156
|
+
return undefined;
|
|
157
|
+
if (args.path === ALLOWED_IMPORT)
|
|
158
|
+
return b.resolve(args.path, { resolveDir: join(tplDir, "src"), kind: args.kind });
|
|
159
|
+
return { errors: [{ text: `src/game.ts may import only "${ALLOWED_IMPORT}", not "${args.path}"` }] };
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
/** Bundles the local host with the game module (one copy of the engine and the sim). Returns the bundle path. */
|
|
165
|
+
export async function bundleHost(g, tplDir, outFile) {
|
|
166
|
+
const esbuild = await import("esbuild");
|
|
167
|
+
const manifest = realpathSync(join(tplDir, "src", "manifest.ts"));
|
|
168
|
+
const gameFile = realpathSync(g.gameFile);
|
|
169
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
170
|
+
const entryFile = existsSync(join(here, "host", "entry.js")) ? join(here, "host", "entry.js") : join(here, "host", "entry.ts");
|
|
171
|
+
const swap = gameSwap(manifest, gameFile, tplDir);
|
|
172
|
+
const contents = [`export { startHost } from ${JSON.stringify(entryFile)};`, `export * as template from ${JSON.stringify(manifest)};`].join("\n");
|
|
173
|
+
const out = await esbuild.build({
|
|
174
|
+
stdin: { contents, resolveDir: here, sourcefile: "kit-play-host.js", loader: "js" },
|
|
175
|
+
bundle: true,
|
|
176
|
+
platform: "node",
|
|
177
|
+
format: "esm",
|
|
178
|
+
target: "node20",
|
|
179
|
+
write: false,
|
|
180
|
+
logLevel: "silent",
|
|
181
|
+
external: ["bufferutil", "utf-8-validate"],
|
|
182
|
+
plugins: [swap],
|
|
183
|
+
banner: { js: "import { createRequire as __kpRequire } from 'node:module'; const require = __kpRequire(import.meta.url);" },
|
|
184
|
+
});
|
|
185
|
+
const file = out.outputFiles[0];
|
|
186
|
+
if (!file)
|
|
187
|
+
throw new PlayError("the host bundle produced no output");
|
|
188
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
189
|
+
writeFileSync(outFile, file.text);
|
|
190
|
+
return outFile;
|
|
191
|
+
}
|
|
192
|
+
function openBrowser(url) {
|
|
193
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
194
|
+
try {
|
|
195
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// the URL is printed either way
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export async function play(o) {
|
|
202
|
+
const dir = resolve(o.dir);
|
|
203
|
+
const g = readGameFolder(dir);
|
|
204
|
+
const tplDir = templateDir(g.template, dir);
|
|
205
|
+
const work = join(dir, ".netnet", "play");
|
|
206
|
+
const clientOut = join(work, "client");
|
|
207
|
+
const lib = o.libraryUrl ? null : libraryDir(dir);
|
|
208
|
+
if (!o.libraryUrl && !lib)
|
|
209
|
+
o.log("No asset library installed (@netforge/asset-library-files): the game draws simple shapes. Pass --library-url <url> to load one from a mirror.");
|
|
210
|
+
o.log(`Building ${g.names?.title ?? "your game"} (${g.template.label})...`);
|
|
211
|
+
await buildClient(g, tplDir, clientOut, join(work, "vite-cache"));
|
|
212
|
+
const bundle = await bundleHost(g, tplDir, join(work, "host.mjs"));
|
|
213
|
+
const mod = (await import(pathToFileURL(bundle).href + `?t=${Date.now()}`));
|
|
214
|
+
const running = await mod.startHost(mod.template, {
|
|
215
|
+
adapter: g.template.adapter,
|
|
216
|
+
manifest: g.template.manifest,
|
|
217
|
+
seats: g.template.seats,
|
|
218
|
+
aoi: g.template.aoi,
|
|
219
|
+
port: o.port,
|
|
220
|
+
host: o.host,
|
|
221
|
+
clientDir: clientOut,
|
|
222
|
+
libraryDir: lib,
|
|
223
|
+
});
|
|
224
|
+
const url = playLink(running.url, o.libraryUrl);
|
|
225
|
+
const lan = lanLinks(o.host, o.port, networkInterfaces(), o.libraryUrl);
|
|
226
|
+
o.log(`Playing locally with play money at ${url}`);
|
|
227
|
+
for (const l of lan)
|
|
228
|
+
o.log(`On your network (phones, other computers): ${l}`);
|
|
229
|
+
if (lan.length === 0 && o.host !== "127.0.0.1" && o.host !== "localhost" && o.host !== "::1")
|
|
230
|
+
o.log(`Listening on ${o.host}: share a link of the form http://<this computer's address>:${o.port}/?server=http://<this computer's address>:${o.port}`);
|
|
231
|
+
o.log("Open the link in more tabs or windows for more players. Ctrl+C stops.");
|
|
232
|
+
o.log("kit play runs the game's code on this computer with your permissions: play only games whose code you trust.");
|
|
233
|
+
if (o.open)
|
|
234
|
+
for (let i = 0; i < Math.max(1, o.players); i++)
|
|
235
|
+
openBrowser(url);
|
|
236
|
+
return { url, lan, close: running.close };
|
|
237
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { LocalAoi } from "./host/core.js";
|
|
2
|
+
export type TemplateId = "survivor" | "arena" | "tower-defense" | "dungeon" | "idle" | "platformer" | "cards" | "collector" | "racer" | "party";
|
|
3
|
+
export interface TemplateEntry {
|
|
4
|
+
readonly id: TemplateId;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
/** The npm package holding the template's game module (src/) and browser client (client/). */
|
|
7
|
+
readonly pkg: string;
|
|
8
|
+
/** The export of src/manifest.ts that builds the room sim (a SimAdapter). */
|
|
9
|
+
readonly adapter: string;
|
|
10
|
+
/** The export of src/manifest.ts that gives the game manifest: `fn().manifest` or `fn()`. */
|
|
11
|
+
readonly manifest: {
|
|
12
|
+
readonly fn: string;
|
|
13
|
+
readonly field?: "manifest";
|
|
14
|
+
};
|
|
15
|
+
/** Most players one room seats. */
|
|
16
|
+
readonly seats: number;
|
|
17
|
+
readonly aoi: LocalAoi;
|
|
18
|
+
}
|
|
19
|
+
export declare const TEMPLATES: Readonly<Record<TemplateId, TemplateEntry>>;
|
|
20
|
+
/** netnet.json's template field (the room template names are accepted too). */
|
|
21
|
+
export declare function templateFor(name: unknown): TemplateEntry | null;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// The Net Forge templates a downloaded game can name (netnet.json
|
|
2
|
+
// "template"), and how local play runs each one: its package, the factory
|
|
3
|
+
// its manifest exports for the room sim, its manifest accessor, the room's
|
|
4
|
+
// seat count and what each player is sent.
|
|
5
|
+
/** engine-sim entity kinds: player 1, enemy 2, projectile 3, pickup 4. */
|
|
6
|
+
const K = { player: 1, enemy: 2, projectile: 3, pickup: 4 };
|
|
7
|
+
const crowd = (radius, enemies) => ({
|
|
8
|
+
radius,
|
|
9
|
+
maxEntities: enemies + 140,
|
|
10
|
+
kindCaps: { [K.enemy]: enemies, [K.pickup]: 60, [K.projectile]: 80 },
|
|
11
|
+
alwaysKinds: [K.player],
|
|
12
|
+
});
|
|
13
|
+
const board = { radius: 60, maxEntities: 400, kindCaps: { [K.enemy]: 240 }, alwaysKinds: [K.player] };
|
|
14
|
+
const race = (radius) => ({ radius, maxEntities: 16, alwaysKinds: [K.player] });
|
|
15
|
+
export const TEMPLATES = {
|
|
16
|
+
survivor: { id: "survivor", label: "survivor-like co-op", pkg: "@netforge/template-survivor", adapter: "survivorAdapter", manifest: { fn: "survivorGame", field: "manifest" }, seats: 4, aoi: crowd(40, 250) },
|
|
17
|
+
arena: { id: "arena", label: "arena shooter", pkg: "@netforge/template-arena", adapter: "arenaAdapter", manifest: { fn: "arenaGame", field: "manifest" }, seats: 16, aoi: { radius: 34, maxEntities: 260, kindCaps: { [K.projectile]: 140, [K.pickup]: 40, [K.enemy]: 60 }, alwaysKinds: [K.player] } },
|
|
18
|
+
"tower-defense": { id: "tower-defense", label: "tower defense", pkg: "@netforge/template-tower", adapter: "towerAdapter", manifest: { fn: "towerManifest" }, seats: 4, aoi: board },
|
|
19
|
+
dungeon: { id: "dungeon", label: "co-op dungeon crawl", pkg: "@netforge/template-dungeon", adapter: "dungeonAdapter", manifest: { fn: "dungeonGame", field: "manifest" }, seats: 4, aoi: crowd(36, 140) },
|
|
20
|
+
idle: { id: "idle", label: "idle", pkg: "@netforge/template-idle", adapter: "idleAdapter", manifest: { fn: "idleManifest" }, seats: 8, aoi: board },
|
|
21
|
+
platformer: { id: "platformer", label: "platformer", pkg: "@netforge/template-platformer", adapter: "platformerAdapter", manifest: { fn: "platformerGame", field: "manifest" }, seats: 8, aoi: race(200) },
|
|
22
|
+
cards: { id: "cards", label: "card battler", pkg: "@netforge/template-cards", adapter: "cardsAdapter", manifest: { fn: "cardsManifest" }, seats: 2, aoi: board },
|
|
23
|
+
collector: { id: "collector", label: "monster collector", pkg: "@netforge/template-collector", adapter: "collectorAdapter", manifest: { fn: "collectorManifest" }, seats: 8, aoi: board },
|
|
24
|
+
racer: { id: "racer", label: "kart racer", pkg: "@netforge/template-racer", adapter: "racerAdapter", manifest: { fn: "racerGame", field: "manifest" }, seats: 8, aoi: race(400) },
|
|
25
|
+
party: { id: "party", label: "party co-op", pkg: "@netforge/template-party", adapter: "partyAdapter", manifest: { fn: "partyGame", field: "manifest" }, seats: 8, aoi: crowd(40, 200) },
|
|
26
|
+
};
|
|
27
|
+
/** netnet.json's template field (the room template names are accepted too). */
|
|
28
|
+
export function templateFor(name) {
|
|
29
|
+
if (typeof name !== "string")
|
|
30
|
+
return null;
|
|
31
|
+
const alias = { shooter: "arena", tower: "tower-defense" };
|
|
32
|
+
const id = (alias[name] ?? name);
|
|
33
|
+
return Object.prototype.hasOwnProperty.call(TEMPLATES, id) ? TEMPLATES[id] : null;
|
|
34
|
+
}
|
package/llms.txt
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# @netforge/kit-play
|
|
2
|
+
|
|
3
|
+
> kit play runs a Net Forge game on your own computer with play money. Inside a game folder (netnet.json and src/game.ts) it builds the template's browser client with that game module, starts a local room host for the same module, and opens http://127.0.0.1:4663. No wallet, no chain, no Net Forge servers; earn rules fire and pay nothing.
|
|
4
|
+
|
|
5
|
+
## Run a game
|
|
6
|
+
|
|
7
|
+
- Install: `npm install @netforge/kit-play @netforge/engine` in the game folder (Node 20 or later).
|
|
8
|
+
- Run: `npx kit-play [dir]`, or `kit play` through @netforge/kit. It exits on Ctrl+C.
|
|
9
|
+
- The folder needs netnet.json with `{"template":"<id>"}` and src/game.ts. src/names.json (title and labels) is optional.
|
|
10
|
+
- Template ids: survivor, arena, tower-defense, dungeon, idle, platformer, cards, collector, racer, party.
|
|
11
|
+
- Start from a template: copy node_modules/@netforge/template-<name>/src/game.ts to src/game.ts (the package names are template-survivor, template-arena, template-tower, template-dungeon, template-idle, template-platformer, template-cards, template-collector, template-racer, template-party).
|
|
12
|
+
- The build lands in .netnet/play/ inside the game folder.
|
|
13
|
+
|
|
14
|
+
## Flags
|
|
15
|
+
|
|
16
|
+
- `--port <n>`: the page and the room on one port (default 4663).
|
|
17
|
+
- `--host <addr>`: interface to listen on (default 127.0.0.1; 0.0.0.0 lets devices on your network join).
|
|
18
|
+
- `--players <n>`: browser tabs to open, one per local player (1 to 16, default 1).
|
|
19
|
+
- `--no-open`: print the link instead of opening a browser.
|
|
20
|
+
- `--library-url <url>`: load the asset library from an http(s) mirror instead of the installed @netforge/asset-library-files.
|
|
21
|
+
|
|
22
|
+
## Programmatic API
|
|
23
|
+
|
|
24
|
+
- `play({ dir, port, host, open, players, libraryUrl, log })` resolves to `{ url, close() }`.
|
|
25
|
+
- `readGameFolder(dir)` checks a folder and throws `PlayError` with a readable message (no netnet.json, unknown template, missing src/game.ts, bad names.json).
|
|
26
|
+
- `runPlayCli(argv, io)` is the CLI; `HELP` is its help text.
|
|
27
|
+
- `TEMPLATES` and `templateFor(id)` describe the runnable templates.
|
|
28
|
+
- `ALLOWED_IMPORT` is "@netforge/engine", the only import a game module may have.
|
|
29
|
+
|
|
30
|
+
## Rules for agents
|
|
31
|
+
|
|
32
|
+
- src/game.ts imports only @netforge/engine, the same rule the Net Forge studio enforces.
|
|
33
|
+
- Do not add renderers, netcode, physics or asset URLs to a game; the template draws and runs everything.
|
|
34
|
+
- kit play runs the game's code with the user's permissions. It is not a sandbox.
|
|
35
|
+
- Validate and playtest before playing: the MCP tools netforge_validate_game and netforge_run_playtest, or engine.test.run from @netforge/engine. netforge_play_locally runs kit play for an agent.
|
|
36
|
+
|
|
37
|
+
## Docs
|
|
38
|
+
|
|
39
|
+
- https://forge.netnet.capital/docs/packages/kit-play
|
|
40
|
+
- MCP server: `npx -y @netforge/mcp` (package @netforge/mcp, bin netforge-mcp). Tools: netforge_list_templates, netforge_create_game, netforge_search_docs, netforge_explain_api, netforge_validate_game, netforge_run_playtest, netforge_play_locally.
|
|
41
|
+
- Agent setup in a project: `npx netforge setup agent`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,90 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netforge/kit-play",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Make, test and run Net Forge games on your own computer with play money: start a game from a template, run playtest bots, and play it in the browser with a local room host, with no wallet, chain or servers.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"game-engine",
|
|
7
|
+
"multiplayer",
|
|
8
|
+
"browser-game",
|
|
9
|
+
"game-server",
|
|
10
|
+
"websocket",
|
|
11
|
+
"game-template",
|
|
12
|
+
"threejs",
|
|
13
|
+
"typescript",
|
|
14
|
+
"gamedev",
|
|
15
|
+
"ai-agents",
|
|
16
|
+
"mcp",
|
|
17
|
+
"vibe-coding",
|
|
18
|
+
"net-forge"
|
|
19
|
+
],
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"main": "./dist/index.js",
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"exports": {
|
|
25
|
+
".": {
|
|
26
|
+
"types": "./dist/index.d.ts",
|
|
27
|
+
"default": "./dist/index.js"
|
|
28
|
+
},
|
|
29
|
+
"./package.json": "./package.json",
|
|
30
|
+
"./llms.txt": "./llms.txt"
|
|
31
|
+
},
|
|
32
|
+
"bin": {
|
|
33
|
+
"kit-play": "./dist/cli.js"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"dist",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"NOTICE",
|
|
40
|
+
"llms.txt"
|
|
41
|
+
],
|
|
42
|
+
"sideEffects": false,
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://forge.netnet.capital/docs/packages/kit-play",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/netnetdev/net-forge/issues"
|
|
49
|
+
},
|
|
50
|
+
"repository": {
|
|
51
|
+
"type": "git",
|
|
52
|
+
"url": "git+https://github.com/netnetdev/net-forge.git",
|
|
53
|
+
"directory": "packages/kit-play"
|
|
54
|
+
},
|
|
55
|
+
"engines": {
|
|
56
|
+
"node": ">=20"
|
|
57
|
+
},
|
|
58
|
+
"dependencies": {
|
|
59
|
+
"@colyseus/core": "0.18.18",
|
|
60
|
+
"@colyseus/ws-transport": "0.18.4",
|
|
61
|
+
"esbuild": "0.25.12",
|
|
62
|
+
"vite": "6.4.3",
|
|
63
|
+
"@netforge/engine-sim": "0.2.0",
|
|
64
|
+
"@netforge/asset-library": "0.2.0",
|
|
65
|
+
"@netforge/tabletop": "0.2.0",
|
|
66
|
+
"@netforge/asset-library-files": "0.2.0",
|
|
67
|
+
"@netforge/template-arena": "0.2.0",
|
|
68
|
+
"@netforge/engine": "0.2.0",
|
|
69
|
+
"@netforge/template-cards": "0.2.0",
|
|
70
|
+
"@netforge/template-dungeon": "0.2.0",
|
|
71
|
+
"@netforge/template-idle": "0.2.0",
|
|
72
|
+
"@netforge/template-collector": "0.2.0",
|
|
73
|
+
"@netforge/template-party": "0.2.0",
|
|
74
|
+
"@netforge/template-platformer": "0.2.0",
|
|
75
|
+
"@netforge/template-survivor": "0.2.0",
|
|
76
|
+
"@netforge/template-tower": "0.2.0",
|
|
77
|
+
"create-netforge": "0.2.0",
|
|
78
|
+
"@netforge/template-racer": "0.2.0"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"@types/node": "22.20.1",
|
|
82
|
+
"typescript": "5.8.3",
|
|
83
|
+
"vitest": "3.2.7"
|
|
84
|
+
},
|
|
85
|
+
"scripts": {
|
|
86
|
+
"build": "node ../engine/scripts/fresh-dist.mjs && tsc -p tsconfig.json",
|
|
87
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
88
|
+
"test": "vitest run"
|
|
89
|
+
}
|
|
6
90
|
}
|