@netforge/kit-play 0.0.0-stage → 0.1.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 +47 -2
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +93 -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 +25 -0
- package/dist/host/server.js +233 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +4 -0
- package/dist/names.d.ts +18 -0
- package/dist/names.js +64 -0
- package/dist/play.d.ts +38 -0
- package/dist/play.js +209 -0
- package/dist/templates.d.ts +21 -0
- package/dist/templates.js +34 -0
- package/package.json +75 -3
package/dist/names.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Names {
|
|
2
|
+
readonly title: string;
|
|
3
|
+
readonly weapons: Readonly<Record<string, {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly line: string;
|
|
6
|
+
}>>;
|
|
7
|
+
readonly upgrades: Readonly<Record<string, {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly line: string;
|
|
10
|
+
}>>;
|
|
11
|
+
readonly enemies: Readonly<Record<string, string>>;
|
|
12
|
+
readonly rules: Readonly<Record<string, string>>;
|
|
13
|
+
readonly skus: Readonly<Record<string, string>>;
|
|
14
|
+
}
|
|
15
|
+
/** Parses src/names.json; throws with the first problem. */
|
|
16
|
+
export declare function parseNames(text: string): Names;
|
|
17
|
+
/** The names module: everything the template's names.ts exports, with these labels in place of its tables. */
|
|
18
|
+
export declare function namesModule(n: Names, templateNamesPath: string): string;
|
package/dist/names.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// src/names.json, the labels file next to game.ts, as the template client's
|
|
2
|
+
// names module: the template's own names.ts with the creator's labels in
|
|
3
|
+
// place of its tables. The labels are data written as JSON literals, so
|
|
4
|
+
// nothing in the file can execute.
|
|
5
|
+
const ID = /^[a-z][a-z0-9_]{0,47}$/;
|
|
6
|
+
const label = (v) => typeof v === "string" && v.trim().length > 0 && v.length <= 40;
|
|
7
|
+
function table(v, ok, what) {
|
|
8
|
+
if (v === undefined)
|
|
9
|
+
return {};
|
|
10
|
+
if (typeof v !== "object" || v === null || Array.isArray(v))
|
|
11
|
+
throw new Error(`src/names.json ${what} must be an object`);
|
|
12
|
+
const out = {};
|
|
13
|
+
for (const [k, x] of Object.entries(v)) {
|
|
14
|
+
if (!ID.test(k))
|
|
15
|
+
throw new Error(`src/names.json ${what}.${k}: ids are lower case letters, digits and _`);
|
|
16
|
+
if (!ok(x))
|
|
17
|
+
throw new Error(`src/names.json ${what}.${k}: not a label (1 to 40 characters)`);
|
|
18
|
+
out[k] = x;
|
|
19
|
+
}
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
const named = (x) => {
|
|
23
|
+
if (typeof x !== "object" || x === null)
|
|
24
|
+
return false;
|
|
25
|
+
const o = x;
|
|
26
|
+
return label(o.name) && label(o.line) && Object.keys(o).length === 2;
|
|
27
|
+
};
|
|
28
|
+
/** Parses src/names.json; throws with the first problem. */
|
|
29
|
+
export function parseNames(text) {
|
|
30
|
+
let raw;
|
|
31
|
+
try {
|
|
32
|
+
raw = JSON.parse(text);
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
throw new Error(`src/names.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
|
|
36
|
+
}
|
|
37
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw))
|
|
38
|
+
throw new Error("src/names.json must be an object");
|
|
39
|
+
const o = raw;
|
|
40
|
+
if (!label(o.title))
|
|
41
|
+
throw new Error("src/names.json title: 1 to 40 characters");
|
|
42
|
+
return {
|
|
43
|
+
title: o.title.trim(),
|
|
44
|
+
weapons: table(o.weapons, named, "weapons"),
|
|
45
|
+
upgrades: table(o.upgrades, named, "upgrades"),
|
|
46
|
+
enemies: table(o.enemies, label, "enemies"),
|
|
47
|
+
rules: table(o.rules, label, "rules"),
|
|
48
|
+
skus: table(o.skus, label, "skus"),
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** The names module: everything the template's names.ts exports, with these labels in place of its tables. */
|
|
52
|
+
export function namesModule(n, templateNamesPath) {
|
|
53
|
+
const j = (v) => JSON.stringify(v);
|
|
54
|
+
return [
|
|
55
|
+
`export * from ${j(templateNamesPath)};`,
|
|
56
|
+
`export const TITLE = ${j(n.title)};`,
|
|
57
|
+
`export const WEAPON_NAMES = ${j(n.weapons)};`,
|
|
58
|
+
`export const UPGRADE_NAMES = ${j(n.upgrades)};`,
|
|
59
|
+
`export const ENEMY_NAMES = ${j(n.enemies)};`,
|
|
60
|
+
`export const RULE_NAMES = ${j(n.rules)};`,
|
|
61
|
+
`export const SKU_NAMES = ${j(n.skus)};`,
|
|
62
|
+
"",
|
|
63
|
+
].join("\n");
|
|
64
|
+
}
|
package/dist/play.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { type Names } from "./names.js";
|
|
2
|
+
import { type TemplateEntry } from "./templates.js";
|
|
3
|
+
/** The only import a game module may make (the studio's rule, kept here so a game that plays locally plays in the studio). */
|
|
4
|
+
export declare const ALLOWED_IMPORT = "@netforge/engine";
|
|
5
|
+
export interface PlayOptions {
|
|
6
|
+
readonly dir: string;
|
|
7
|
+
readonly port: number;
|
|
8
|
+
readonly host: string;
|
|
9
|
+
readonly open: boolean;
|
|
10
|
+
/** Browser tabs to open, one per local player. */
|
|
11
|
+
readonly players: number;
|
|
12
|
+
/** A base URL the page loads the asset library from instead of this host (any mirror of the published library). */
|
|
13
|
+
readonly libraryUrl: string | null;
|
|
14
|
+
readonly log: (s: string) => void;
|
|
15
|
+
}
|
|
16
|
+
export interface GameFolder {
|
|
17
|
+
readonly dir: string;
|
|
18
|
+
readonly template: TemplateEntry;
|
|
19
|
+
readonly gameFile: string;
|
|
20
|
+
readonly names: Names | null;
|
|
21
|
+
}
|
|
22
|
+
export declare class PlayError extends Error {
|
|
23
|
+
}
|
|
24
|
+
/** Reads and checks the game folder. */
|
|
25
|
+
export declare function readGameFolder(dir: string): GameFolder;
|
|
26
|
+
/** The template package's root (its "." export is src/manifest.ts). */
|
|
27
|
+
export declare function templateDir(t: TemplateEntry, from: string): string;
|
|
28
|
+
/** The built asset library shipped in @netforge/asset-library-files, or null when it is not installed. */
|
|
29
|
+
export declare function libraryDir(from: string): string | null;
|
|
30
|
+
/** Builds the template client with the game module and labels swapped in. */
|
|
31
|
+
export declare function buildClient(g: GameFolder, tplDir: string, outDir: string, cacheDir: string): Promise<void>;
|
|
32
|
+
/** Bundles the local host with the game module (one copy of the engine and the sim). Returns the bundle path. */
|
|
33
|
+
export declare function bundleHost(g: GameFolder, tplDir: string, outFile: string): Promise<string>;
|
|
34
|
+
export interface Playing {
|
|
35
|
+
readonly url: string;
|
|
36
|
+
close(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export declare function play(o: PlayOptions): Promise<Playing>;
|
package/dist/play.js
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
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 { namesModule, parseNames } from "./names.js";
|
|
18
|
+
import { templateFor } from "./templates.js";
|
|
19
|
+
/** The only import a game module may make (the studio's rule, kept here so a game that plays locally plays in the studio). */
|
|
20
|
+
export const ALLOWED_IMPORT = "@netforge/engine";
|
|
21
|
+
export class PlayError extends Error {
|
|
22
|
+
}
|
|
23
|
+
/** Reads and checks the game folder. */
|
|
24
|
+
export function readGameFolder(dir) {
|
|
25
|
+
const metaFile = join(dir, "netnet.json");
|
|
26
|
+
if (!existsSync(metaFile))
|
|
27
|
+
throw new PlayError(`No netnet.json in ${dir}. Run kit play inside a game downloaded from Net Forge.`);
|
|
28
|
+
let meta;
|
|
29
|
+
try {
|
|
30
|
+
meta = JSON.parse(readFileSync(metaFile, "utf8"));
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new PlayError("netnet.json is not valid JSON.");
|
|
34
|
+
}
|
|
35
|
+
const template = templateFor(meta.template);
|
|
36
|
+
if (!template)
|
|
37
|
+
throw new PlayError(`netnet.json names an unknown template: ${String(meta.template)}`);
|
|
38
|
+
const gameFile = join(dir, "src", "game.ts");
|
|
39
|
+
if (!existsSync(gameFile))
|
|
40
|
+
throw new PlayError("src/game.ts is missing.");
|
|
41
|
+
const namesFile = join(dir, "src", "names.json");
|
|
42
|
+
let names = null;
|
|
43
|
+
if (existsSync(namesFile)) {
|
|
44
|
+
try {
|
|
45
|
+
names = parseNames(readFileSync(namesFile, "utf8"));
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
throw new PlayError(e instanceof Error ? e.message : String(e));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return { dir, template, gameFile, names };
|
|
52
|
+
}
|
|
53
|
+
/** Where a package is installed, looked up from the game folder first, then from kit-play itself. */
|
|
54
|
+
function packageDir(pkg, from, entryToRoot) {
|
|
55
|
+
const bases = [join(from, "package.json"), fileURLToPath(import.meta.url)];
|
|
56
|
+
for (const base of bases) {
|
|
57
|
+
try {
|
|
58
|
+
return entryToRoot(createRequire(base).resolve(pkg));
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// next
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
throw new PlayError(`${pkg} is not installed. Run npm install in the game folder.`);
|
|
65
|
+
}
|
|
66
|
+
/** The template package's root (its "." export is src/manifest.ts). */
|
|
67
|
+
export function templateDir(t, from) {
|
|
68
|
+
return packageDir(t.pkg, from, (p) => dirname(dirname(p)));
|
|
69
|
+
}
|
|
70
|
+
/** The built asset library shipped in @netforge/asset-library-files, or null when it is not installed. */
|
|
71
|
+
export function libraryDir(from) {
|
|
72
|
+
try {
|
|
73
|
+
const dir = packageDir("@netforge/asset-library-files/package.json", from, (p) => dirname(p));
|
|
74
|
+
const lib = join(dir, "library");
|
|
75
|
+
return existsSync(join(lib, "index.json")) ? lib : null;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/** Builds the template client with the game module and labels swapped in. */
|
|
82
|
+
export async function buildClient(g, tplDir, outDir, cacheDir) {
|
|
83
|
+
const vite = await import("vite");
|
|
84
|
+
// Vite hands plugins real, forward-slash paths: compare in that form (Windows, symlinked folders).
|
|
85
|
+
const norm = (p) => vite.normalizePath(realpathSync(p));
|
|
86
|
+
const manifest = norm(join(tplDir, "src", "manifest.ts"));
|
|
87
|
+
const clientDir = norm(join(tplDir, "client"));
|
|
88
|
+
const templateNames = norm(join(tplDir, "client", "names.ts"));
|
|
89
|
+
const gameFile = norm(g.gameFile);
|
|
90
|
+
const NAMES_ID = "\0kit-play-names.js";
|
|
91
|
+
const plugin = {
|
|
92
|
+
name: "kit-play-game",
|
|
93
|
+
enforce: "pre",
|
|
94
|
+
async resolveId(source, importer) {
|
|
95
|
+
if (!importer)
|
|
96
|
+
return null;
|
|
97
|
+
if (importer === manifest && (source === "./game.js" || source === "./game.ts"))
|
|
98
|
+
return gameFile;
|
|
99
|
+
if (importer === gameFile) {
|
|
100
|
+
if (source === ALLOWED_IMPORT)
|
|
101
|
+
return this.resolve(source, manifest, { skipSelf: true });
|
|
102
|
+
this.error(`src/game.ts may import only "${ALLOWED_IMPORT}", not "${source}"`);
|
|
103
|
+
}
|
|
104
|
+
if (g.names && importer.startsWith(clientDir) && importer !== templateNames && (source === "./names.js" || source === "./names.ts"))
|
|
105
|
+
return NAMES_ID;
|
|
106
|
+
return null;
|
|
107
|
+
},
|
|
108
|
+
load(id) {
|
|
109
|
+
return id === NAMES_ID && g.names ? namesModule(g.names, templateNames) : null;
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
await vite.build({
|
|
113
|
+
configFile: false,
|
|
114
|
+
root: clientDir,
|
|
115
|
+
base: "./",
|
|
116
|
+
logLevel: "warn",
|
|
117
|
+
publicDir: false,
|
|
118
|
+
cacheDir,
|
|
119
|
+
// No VITE_* variables from the shell or a .env file reach the page: it always talks to this host.
|
|
120
|
+
envDir: join(cacheDir, "no-env"),
|
|
121
|
+
envPrefix: "KIT_PLAY_NEVER_",
|
|
122
|
+
plugins: [plugin],
|
|
123
|
+
build: { outDir, emptyOutDir: true, target: "es2022", chunkSizeWarningLimit: 16384, reportCompressedSize: false },
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
/** Bundles the local host with the game module (one copy of the engine and the sim). Returns the bundle path. */
|
|
127
|
+
export async function bundleHost(g, tplDir, outFile) {
|
|
128
|
+
const esbuild = await import("esbuild");
|
|
129
|
+
const manifest = realpathSync(join(tplDir, "src", "manifest.ts"));
|
|
130
|
+
const gameFile = realpathSync(g.gameFile);
|
|
131
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
132
|
+
const entryFile = existsSync(join(here, "host", "entry.js")) ? join(here, "host", "entry.js") : join(here, "host", "entry.ts");
|
|
133
|
+
const swap = {
|
|
134
|
+
name: "kit-play-swap",
|
|
135
|
+
setup(b) {
|
|
136
|
+
b.onResolve({ filter: /^\.\/game\.(js|ts)$/ }, (args) => (args.importer === manifest ? { path: gameFile } : undefined));
|
|
137
|
+
b.onResolve({ filter: /.*/ }, async (args) => {
|
|
138
|
+
if (args.importer !== gameFile)
|
|
139
|
+
return undefined;
|
|
140
|
+
if (args.path === ALLOWED_IMPORT)
|
|
141
|
+
return b.resolve(args.path, { resolveDir: join(tplDir, "src"), kind: args.kind });
|
|
142
|
+
return { errors: [{ text: `src/game.ts may import only "${ALLOWED_IMPORT}"` }] };
|
|
143
|
+
});
|
|
144
|
+
},
|
|
145
|
+
};
|
|
146
|
+
const contents = [`export { startHost } from ${JSON.stringify(entryFile)};`, `export * as template from ${JSON.stringify(manifest)};`].join("\n");
|
|
147
|
+
const out = await esbuild.build({
|
|
148
|
+
stdin: { contents, resolveDir: here, sourcefile: "kit-play-host.js", loader: "js" },
|
|
149
|
+
bundle: true,
|
|
150
|
+
platform: "node",
|
|
151
|
+
format: "esm",
|
|
152
|
+
target: "node20",
|
|
153
|
+
write: false,
|
|
154
|
+
logLevel: "silent",
|
|
155
|
+
external: ["bufferutil", "utf-8-validate"],
|
|
156
|
+
plugins: [swap],
|
|
157
|
+
banner: { js: "import { createRequire as __kpRequire } from 'node:module'; const require = __kpRequire(import.meta.url);" },
|
|
158
|
+
});
|
|
159
|
+
const file = out.outputFiles[0];
|
|
160
|
+
if (!file)
|
|
161
|
+
throw new PlayError("the host bundle produced no output");
|
|
162
|
+
mkdirSync(dirname(outFile), { recursive: true });
|
|
163
|
+
writeFileSync(outFile, file.text);
|
|
164
|
+
return outFile;
|
|
165
|
+
}
|
|
166
|
+
function openBrowser(url) {
|
|
167
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
168
|
+
try {
|
|
169
|
+
spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// the URL is printed either way
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
export async function play(o) {
|
|
176
|
+
const dir = resolve(o.dir);
|
|
177
|
+
const g = readGameFolder(dir);
|
|
178
|
+
const tplDir = templateDir(g.template, dir);
|
|
179
|
+
const work = join(dir, ".netnet", "play");
|
|
180
|
+
const clientOut = join(work, "client");
|
|
181
|
+
const lib = o.libraryUrl ? null : libraryDir(dir);
|
|
182
|
+
if (!o.libraryUrl && !lib)
|
|
183
|
+
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.");
|
|
184
|
+
o.log(`Building ${g.names?.title ?? "your game"} (${g.template.label})...`);
|
|
185
|
+
await buildClient(g, tplDir, clientOut, join(work, "vite-cache"));
|
|
186
|
+
const bundle = await bundleHost(g, tplDir, join(work, "host.mjs"));
|
|
187
|
+
const mod = (await import(pathToFileURL(bundle).href + `?t=${Date.now()}`));
|
|
188
|
+
const running = await mod.startHost(mod.template, {
|
|
189
|
+
adapter: g.template.adapter,
|
|
190
|
+
manifest: g.template.manifest,
|
|
191
|
+
seats: g.template.seats,
|
|
192
|
+
aoi: g.template.aoi,
|
|
193
|
+
port: o.port,
|
|
194
|
+
host: o.host,
|
|
195
|
+
clientDir: clientOut,
|
|
196
|
+
libraryDir: lib,
|
|
197
|
+
});
|
|
198
|
+
const params = new URLSearchParams({ server: running.url });
|
|
199
|
+
if (o.libraryUrl)
|
|
200
|
+
params.set("library", o.libraryUrl);
|
|
201
|
+
const url = `${running.url}/?${params.toString()}`;
|
|
202
|
+
o.log(`Playing locally with play money at ${url}`);
|
|
203
|
+
o.log("Open the link in more tabs or windows for more players. Ctrl+C stops.");
|
|
204
|
+
o.log("kit play runs the game's code on this computer with your permissions: play only games whose code you trust.");
|
|
205
|
+
if (o.open)
|
|
206
|
+
for (let i = 0; i < Math.max(1, o.players); i++)
|
|
207
|
+
openBrowser(url);
|
|
208
|
+
return { url, close: running.close };
|
|
209
|
+
}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,78 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@netforge/kit-play",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "kit play: run a game downloaded from Net Forge on your own computer with play money. Builds the template client with your game module and runs a local room host from the engine's open simulation. No wallet, no chain, no Net Forge servers.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"bin": {
|
|
17
|
+
"kit-play": "./dist/cli.js"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"NOTICE"
|
|
24
|
+
],
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/netnetdev/net-forge.git",
|
|
31
|
+
"directory": "packages/kit-play"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/netnetdev/net-forge/tree/main/packages/kit-play#readme",
|
|
34
|
+
"keywords": [
|
|
35
|
+
"netnet",
|
|
36
|
+
"net-forge",
|
|
37
|
+
"game",
|
|
38
|
+
"local",
|
|
39
|
+
"three.js"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=20"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@colyseus/core": "0.18.18",
|
|
46
|
+
"@colyseus/ws-transport": "0.18.4",
|
|
47
|
+
"esbuild": "0.25.12",
|
|
48
|
+
"vite": "6.4.3",
|
|
49
|
+
"@netforge/asset-library": "0.1.0",
|
|
50
|
+
"@netforge/engine": "0.1.0",
|
|
51
|
+
"@netforge/engine-sim": "0.1.0",
|
|
52
|
+
"@netforge/asset-library-files": "0.1.0",
|
|
53
|
+
"@netforge/template-arena": "0.1.0",
|
|
54
|
+
"@netforge/tabletop": "0.1.0",
|
|
55
|
+
"@netforge/template-cards": "0.1.0",
|
|
56
|
+
"@netforge/template-collector": "0.1.0",
|
|
57
|
+
"@netforge/template-idle": "0.1.0",
|
|
58
|
+
"@netforge/template-dungeon": "0.1.0",
|
|
59
|
+
"@netforge/template-party": "0.1.0",
|
|
60
|
+
"@netforge/template-platformer": "0.1.0",
|
|
61
|
+
"@netforge/template-racer": "0.1.0",
|
|
62
|
+
"@netforge/template-tower": "0.1.0",
|
|
63
|
+
"@netforge/template-survivor": "0.1.0"
|
|
64
|
+
},
|
|
65
|
+
"devDependencies": {
|
|
66
|
+
"@types/node": "22.20.1",
|
|
67
|
+
"typescript": "5.8.3",
|
|
68
|
+
"vitest": "3.2.7"
|
|
69
|
+
},
|
|
70
|
+
"bugs": {
|
|
71
|
+
"url": "https://github.com/netnetdev/net-forge/issues"
|
|
72
|
+
},
|
|
73
|
+
"scripts": {
|
|
74
|
+
"build": "node ../engine/scripts/fresh-dist.mjs && tsc -p tsconfig.json",
|
|
75
|
+
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json",
|
|
76
|
+
"test": "vitest run"
|
|
77
|
+
}
|
|
6
78
|
}
|