@pop-party/create-game 1.0.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.
Files changed (25) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +30 -0
  3. package/bin/create-game.js +28 -0
  4. package/package.json +34 -0
  5. package/src/cli-arguments.js +47 -0
  6. package/src/generate-game.js +150 -0
  7. package/starter/ASSET-NOTICES.json +73 -0
  8. package/starter/content/art/manifest.json +13573 -0
  9. package/starter/content/audio/host-audios.json +3 -0
  10. package/starter/content/blobs/1c14e1db26cfb96105b2cbf60591a18329834078cc65e14373715d4ec06f07ef.svg +5 -0
  11. package/starter/content/blobs/2d1298fc8597b4b13dc74c8a4093ce7c8d6027ef955c4af31ed6a6d03034d527.svg +3 -0
  12. package/starter/content/blobs/4d1b3ad12d64d7f145e282dc44b2c510d8cc36c0a609fcef831fe9c59270bf35.svg +8 -0
  13. package/starter/content/blobs/7a3a06ea1280a916254e2af034762e798b11e45f5eb7a1f4e0ba1a3ad4b8af48.svg +3 -0
  14. package/starter/content/blobs/8785460bf8b2311bfd2a5c06e97f9faac0deafd584b8f3de428c38399e2df16d.svg +6 -0
  15. package/starter/content/blobs/8dbc40965debd1f9f516461ed2b6ca0dd95557f8a762dbeaf67459e4b343497a.svg +8 -0
  16. package/starter/content/blobs/a531c3ee6f5bfa5b478b6ecda5799f1031ceea367ca9d93585545b3de0ed34d7.svg +7 -0
  17. package/starter/content/blobs/d54e3eae53b88a705370aa028c3ea55297d549e3521aca6a41435f87e4a18167.svg +6 -0
  18. package/starter/content/constants.json +22 -0
  19. package/starter/content/content-bundle.json +96 -0
  20. package/starter/content/flow.json +1594 -0
  21. package/starter/content/game-data/runtime.json +39 -0
  22. package/starter/content/layouts/controller.json +737 -0
  23. package/starter/content/layouts/stage.json +195 -0
  24. package/starter/content/prompts/prompts.json +125 -0
  25. package/starter/content/semantic-roles.json +86 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Turowetz
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/README.md ADDED
@@ -0,0 +1,30 @@
1
+ # @pop-party/create-game
2
+
3
+ This package powers `npm create @pop-party/game MyGame`. It creates an independent game directory, pins an exact `@pop-party/engine` version, and byte-copies the canonical CC0-1.0 starter content without symlinks or references to the engine repository.
4
+
5
+ Current npm initializer resolution maps `npm create @pop-party/game` to this
6
+ package, `@pop-party/create-game`, and executes its sole `create-game` bin. The
7
+ packed executable is part of the release gate. Unknown flags, missing flag
8
+ values, and ambiguous game names fail instead of falling back to defaults.
9
+
10
+ Generated games use `pop-party validate content` for content-only checks and
11
+ `pop-party build` for the full game/engine/plugin/content readiness gate. Build
12
+ output is game-local and records the exact immutable content revision.
13
+ Runtime game data is materialized from that same validated content snapshot;
14
+ the generator does not create a second empty or drifting game-data source.
15
+
16
+ Generated games run through `pop-party dev` locally and `pop-party start` in
17
+ production. The first development run seeds ignored `.pop-party/content` from
18
+ the configured immutable release; later runs keep that independent local copy.
19
+ Production never reads the development workspace. Both commands validate the
20
+ complete selected release before opening a port.
21
+
22
+ `pop-party migrate` previews the registered, contiguous game migration path.
23
+ It writes nothing unless `--output <new-directory>` is supplied, never mutates
24
+ the source bundle, runs every migration twice to reject nondeterministic output,
25
+ and validates the complete result against the installed engine before writing.
26
+
27
+ Each generated game also owns a `render.yaml` and `DEPLOYMENT.md`. The Blueprint
28
+ declares one manually scaled Node web service, the engine build/start commands,
29
+ `/health`, and a 300-second graceful shutdown window. It contains no shared
30
+ engine deployment and no committed provider or administrator secrets.
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ const path = require("path");
5
+ const { generateGame } = require("../src/generate-game");
6
+ const { HELP_TEXT, parseCreateGameArguments } = require("../src/cli-arguments");
7
+
8
+ try {
9
+ const parsed = parseCreateGameArguments(process.argv.slice(2));
10
+ if (parsed.help) {
11
+ console.log(HELP_TEXT);
12
+ process.exitCode = 0;
13
+ return;
14
+ }
15
+ const result = generateGame({
16
+ ...parsed,
17
+ targetRoot: parsed.targetRoot || path.resolve(process.cwd(), resultName(parsed.displayName))
18
+ });
19
+ console.log(`Created ${result.displayName} at ${result.targetRoot}`);
20
+ console.log(`Engine: @pop-party/engine@${result.engineVersion}`);
21
+ } catch (error) {
22
+ console.error(`Could not create game: ${error.message}`);
23
+ process.exitCode = 1;
24
+ }
25
+
26
+ function resultName(value) {
27
+ return String(value || "game").trim().replace(/\s+/g, "-");
28
+ }
package/package.json ADDED
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "@pop-party/create-game",
3
+ "version": "1.0.0",
4
+ "description": "Create an independent Pop Party game pinned to an exact engine version.",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/MarkTurowetz/pop-party-engine.git",
9
+ "directory": "packages/create-game"
10
+ },
11
+ "homepage": "https://github.com/MarkTurowetz/pop-party-engine/tree/main/packages/create-game#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/MarkTurowetz/pop-party-engine/issues"
14
+ },
15
+ "type": "commonjs",
16
+ "main": "src/generate-game.js",
17
+ "bin": {
18
+ "create-game": "bin/create-game.js"
19
+ },
20
+ "files": [
21
+ "bin/*.js",
22
+ "src/**/*.js",
23
+ "starter/**/*",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "engines": {
28
+ "node": ">=22"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public",
32
+ "provenance": true
33
+ }
34
+ }
@@ -0,0 +1,47 @@
1
+ "use strict";
2
+
3
+ const HELP_TEXT = [
4
+ "Usage: npm create @pop-party/game <name> [options]",
5
+ "",
6
+ "Options:",
7
+ " --engine-version <version> Exact @pop-party/engine version",
8
+ " --output <directory> Empty target directory",
9
+ " --starter <directory> Alternate starter bundle (development/testing only)",
10
+ " --help Show this help"
11
+ ].join("\n");
12
+
13
+ const VALUE_FLAGS = Object.freeze({
14
+ "--engine-version": "engineVersion",
15
+ "--output": "targetRoot",
16
+ "--starter": "starterRoot"
17
+ });
18
+
19
+ function parseCreateGameArguments(argv = []) {
20
+ const options = {};
21
+ let displayName = "";
22
+ for (let index = 0; index < argv.length; index += 1) {
23
+ const value = String(argv[index]);
24
+ if (value === "--help" || value === "-h") return Object.freeze({ help: true });
25
+ const equalsIndex = value.indexOf("=");
26
+ const flag = equalsIndex > 0 ? value.slice(0, equalsIndex) : value;
27
+ if (VALUE_FLAGS[flag]) {
28
+ const flagValue = equalsIndex > 0 ? value.slice(equalsIndex + 1) : String(argv[index + 1] ?? "");
29
+ if (!flagValue || (equalsIndex < 0 && flagValue.startsWith("-"))) throw new Error(`${flag} requires a value`);
30
+ options[VALUE_FLAGS[flag]] = flagValue;
31
+ if (equalsIndex < 0) index += 1;
32
+ continue;
33
+ }
34
+ if (value.startsWith("-")) throw new Error(`Unknown create-game option: ${value}`);
35
+ if (displayName) throw new Error(`Unexpected game name argument: ${value}`);
36
+ displayName = value.trim();
37
+ }
38
+ if (!displayName) throw new Error("Game display name is required");
39
+ return Object.freeze({
40
+ displayName,
41
+ engineVersion: options.engineVersion || "1.0.0",
42
+ starterRoot: options.starterRoot || undefined,
43
+ targetRoot: options.targetRoot || undefined
44
+ });
45
+ }
46
+
47
+ module.exports = Object.freeze({ HELP_TEXT, parseCreateGameArguments });
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+
6
+ const EXACT_VERSION_PATTERN = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
7
+ const GAME_ID_PATTERN = /^[a-z][a-z0-9-]{2,63}$/;
8
+ function gameIdFromName(value) {
9
+ const normalized = String(value || "").normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase();
10
+ const gameId = normalized.replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64).replace(/-+$/g, "");
11
+ if (!GAME_ID_PATTERN.test(gameId)) throw new Error("Game name must produce a 3-64 character id beginning with a letter");
12
+ return gameId;
13
+ }
14
+
15
+ function assertExactEngineVersion(value) {
16
+ const version = String(value || "").trim();
17
+ if (!EXACT_VERSION_PATTERN.test(version)) throw new Error("Engine version must be an exact semantic version without a range");
18
+ return version;
19
+ }
20
+
21
+ function assertEmptyTarget(targetRoot) {
22
+ if (!fs.existsSync(targetRoot)) return;
23
+ const stat = fs.lstatSync(targetRoot);
24
+ if (stat.isSymbolicLink() || !stat.isDirectory()) throw new Error(`Game target is not an empty directory: ${targetRoot}`);
25
+ if (fs.readdirSync(targetRoot).length) throw new Error(`Game target is not empty: ${targetRoot}`);
26
+ }
27
+
28
+ function copyTree(sourceRoot, targetRoot) {
29
+ const sourceStat = fs.lstatSync(sourceRoot);
30
+ if (sourceStat.isSymbolicLink()) throw new Error(`Starter content cannot contain symlinks: ${sourceRoot}`);
31
+ if (!sourceStat.isDirectory()) throw new Error(`Starter content root is not a directory: ${sourceRoot}`);
32
+ fs.mkdirSync(targetRoot, { recursive: true });
33
+ for (const entry of fs.readdirSync(sourceRoot, { withFileTypes: true })) {
34
+ const sourcePath = path.join(sourceRoot, entry.name);
35
+ const targetPath = path.join(targetRoot, entry.name);
36
+ const stat = fs.lstatSync(sourcePath);
37
+ if (stat.isSymbolicLink()) throw new Error(`Starter content cannot contain symlinks: ${sourcePath}`);
38
+ if (stat.isDirectory()) copyTree(sourcePath, targetPath);
39
+ else if (stat.isFile()) fs.copyFileSync(sourcePath, targetPath, fs.constants.COPYFILE_EXCL);
40
+ else throw new Error(`Unsupported starter content entry: ${sourcePath}`);
41
+ }
42
+ }
43
+
44
+ function writeText(targetRoot, relativePath, text) {
45
+ const absolutePath = path.join(targetRoot, relativePath);
46
+ fs.mkdirSync(path.dirname(absolutePath), { recursive: true });
47
+ fs.writeFileSync(absolutePath, text, { encoding: "utf8", flag: "wx" });
48
+ }
49
+
50
+ function generatedConfigSource({ displayName, engineVersion, gameId }) {
51
+ return `"use strict";\n\nconst path = require("node:path");\nconst { createLocalContentBundleProvider } = require("@pop-party/engine");\nconst { defineGame } = require("@pop-party/engine/game");\nconst semanticRoles = require("./content/semantic-roles.json").roles;\nconst plugin = require("./src/plugin");\n\nfunction contentRoot() {\n const configured = process.env.POP_PARTY_CONTENT_ROOT;\n if (!configured) return path.join(__dirname, "content");\n const root = path.resolve(__dirname, configured);\n const relative = path.relative(__dirname, root);\n if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) {\n throw new Error("POP_PARTY_CONTENT_ROOT must remain inside the game workspace");\n }\n return root;\n}\n\nconst contentStore = createLocalContentBundleProvider({\n root: contentRoot(),\n gameBuild: "0.1.0",\n engineVersion: ${JSON.stringify(engineVersion)},\n pluginVersion: "0.1.0"\n});\n\nmodule.exports = defineGame({\n gameId: ${JSON.stringify(gameId)},\n displayName: ${JSON.stringify(displayName)},\n version: "0.1.0",\n engineCompatibility: ${JSON.stringify(engineVersion)},\n content: {\n mode: "bundle",\n schemaVersion: 1,\n store: contentStore\n },\n plugin,\n semanticRoles\n});\n`;
52
+ }
53
+
54
+ function generatedContributionSource(kind) {
55
+ return `"use strict";\n\n// Add ${kind} contributions as { id: "game-namespace.name", value: ... }.\nmodule.exports = Object.freeze([]);\n`;
56
+ }
57
+
58
+ function generatedBootstrapRendererSource({ gameId, role }) {
59
+ const kind = role === "stage" ? "Stage" : "Controller";
60
+ return `"use strict";\n\nmodule.exports = Object.freeze([{\n id: ${JSON.stringify(`${gameId}.bootstrap-${role}`)},\n value: Object.freeze({\n renderBootstrap({ game }) {\n if (game.id !== ${JSON.stringify(gameId)}) throw new Error("${kind} renderer received another game");\n return Object.freeze({\n heading: game.displayName,\n message: ${JSON.stringify(`${kind} service is ready.`)}\n });\n }\n })\n}]);\n`;
61
+ }
62
+
63
+ function generatedRenderBlueprintSource(gameId) {
64
+ return `services:\n - type: web\n name: ${gameId}\n runtime: node\n plan: starter\n numInstances: 1\n buildCommand: npm install --no-audit --no-fund && npm run build\n startCommand: npm start\n healthCheckPath: /health\n maxShutdownDelaySeconds: 300\n envVars:\n - key: NODE_ENV\n value: production\n`;
65
+ }
66
+
67
+ function generatedPluginSource(pluginNamespace) {
68
+ return `"use strict";\n\nconst { defineGamePlugin } = require("@pop-party/engine/plugin");\nconst actions = require("../actions");\nconst stageRenderers = require("../stage");\nconst controllerRenderers = require("../controller");\nconst toolPanels = require("../tools");\n\nconst contributionGroups = Object.freeze([\n ["actions", actions],\n ["stageRenderers", stageRenderers],\n ["controllerRenderers", controllerRenderers],\n ["toolPanels", toolPanels]\n]);\n\nmodule.exports = defineGamePlugin({\n namespace: ${JSON.stringify(pluginNamespace)},\n register(registry) {\n for (const [kind, contributions] of contributionGroups) {\n for (const contribution of contributions) registry[kind](contribution.id, contribution.value);\n }\n }\n});\n`;
69
+ }
70
+
71
+ function generatedConfigTestSource({ engineVersion, gameId, pluginNamespace }) {
72
+ return `"use strict";\n\nconst assert = require("node:assert/strict");\nconst test = require("node:test");\nconst { createGameApplicationRuntime } = require("@pop-party/engine/server/application");\nconst { createGameReadinessRuntime } = require("@pop-party/engine/server/readiness");\nconst game = require("../game.config");\n\ntest("game configuration owns an exact engine and plugin boundary", async () => {\n assert.equal(game.gameId, ${JSON.stringify(gameId)});\n assert.equal(game.engineCompatibility, ${JSON.stringify(engineVersion)});\n assert.equal(game.plugin.namespace, ${JSON.stringify(pluginNamespace)});\n assert.equal(game.gameData, null);\n assert.equal(game.registrations.stageRenderers.length, 1);\n assert.equal(game.registrations.stageRenderers[0].id, ${JSON.stringify(`${gameId}.bootstrap-stage`)});\n assert.equal(game.registrations.controllerRenderers.length, 1);\n assert.equal(game.registrations.controllerRenderers[0].id, ${JSON.stringify(`${gameId}.bootstrap-controller`)});\n assert.deepEqual({\n actions: game.registrations.actions,\n stateSchemas: game.registrations.stateSchemas,\n validators: game.registrations.validators,\n migrations: game.registrations.migrations,\n toolPanels: game.registrations.toolPanels,\n diagnostics: game.registrations.diagnostics\n }, {\n actions: [],\n stateSchemas: [],\n validators: [],\n migrations: [],\n toolPanels: [],\n diagnostics: []\n });\n const readiness = createGameReadinessRuntime({ gameDefinition: game, engineVersion: ${JSON.stringify(engineVersion)} });\n const active = await readiness.check();\n assert.equal(active.release.gameId, ${JSON.stringify(gameId)});\n assert.equal(active.release.engineVersion, ${JSON.stringify(engineVersion)});\n assert.ok(active.gameData.defaultGameFlow.states.length > 0);\n assert.ok(active.gameData.defaultArtCompositions.length > 0);\n assert.equal(readiness.state.status, "ready");\n});\n\ntest("generated game starts as an independent validated web service", async () => {\n const runtime = createGameApplicationRuntime({\n gameDefinition: game,\n engineVersion: ${JSON.stringify(engineVersion)},\n host: "127.0.0.1",\n port: 0\n });\n try {\n const startup = await runtime.start();\n const health = await fetch(\`${"${startup.localUrl}"}/health\`);\n const stage = await fetch(\`${"${startup.localUrl}"}/stage\`);\n const controller = await fetch(\`${"${startup.localUrl}"}/controller\`);\n const tools = await fetch(\`${"${startup.localUrl}"}/tools\`);\n assert.equal(health.status, 200);\n assert.equal((await health.json()).release.contentRevision, runtime.active.release.contentRevision);\n assert.equal(stage.status, 200);\n assert.match(await stage.text(), /data-pop-party-role="stage"/);\n assert.equal(controller.status, 200);\n assert.match(await controller.text(), /data-pop-party-role="controller"/);\n assert.equal(tools.status, 503);\n assert.equal((await tools.json()).diagnostic.code, "GAME_TOOLING_NOT_CONFIGURED");\n } finally {\n await runtime.stop();\n }\n});\n`;
73
+ }
74
+
75
+ function rewriteBundleGameId(contentRoot, gameId) {
76
+ const manifestPath = path.join(contentRoot, "content-bundle.json");
77
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
78
+ manifest.gameId = gameId;
79
+ fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8");
80
+ }
81
+
82
+ function generateGame(options = {}) {
83
+ const displayName = String(options.displayName || options.name || "").trim();
84
+ if (!displayName) throw new Error("Game display name is required");
85
+ const gameId = options.gameId ? String(options.gameId).trim() : gameIdFromName(displayName);
86
+ if (!GAME_ID_PATTERN.test(gameId)) throw new Error(`Game id must match ${GAME_ID_PATTERN}`);
87
+ const engineVersion = assertExactEngineVersion(options.engineVersion);
88
+ const targetRoot = path.resolve(options.targetRoot || gameId);
89
+ const starterRoot = path.resolve(options.starterRoot || path.join(__dirname, "..", "starter", "content"));
90
+ const pluginNamespace = gameId.slice(0, 48).replace(/-+$/g, "");
91
+ assertEmptyTarget(targetRoot);
92
+ const parentRoot = path.dirname(targetRoot);
93
+ fs.mkdirSync(parentRoot, { recursive: true });
94
+ const stagingRoot = fs.mkdtempSync(path.join(parentRoot, `.${path.basename(targetRoot)}.pop-party-`));
95
+ try {
96
+ copyTree(starterRoot, path.join(stagingRoot, "content"));
97
+ rewriteBundleGameId(path.join(stagingRoot, "content"), gameId);
98
+
99
+ const manifest = {
100
+ name: gameId,
101
+ version: "0.1.0",
102
+ private: true,
103
+ description: `${displayName} built with Pop Party Engine.`,
104
+ engines: { node: ">=22" },
105
+ scripts: {
106
+ build: "pop-party build",
107
+ dev: "pop-party dev",
108
+ migrate: "pop-party migrate",
109
+ start: "pop-party start",
110
+ test: "node --test",
111
+ validate: "pop-party validate content"
112
+ },
113
+ dependencies: { "@pop-party/engine": engineVersion }
114
+ };
115
+ writeText(stagingRoot, "package.json", `${JSON.stringify(manifest, null, 2)}\n`);
116
+ writeText(stagingRoot, "game.config.js", generatedConfigSource({ displayName, engineVersion, gameId }));
117
+ writeText(stagingRoot, "src/actions/index.js", generatedContributionSource("server and flow action"));
118
+ writeText(stagingRoot, "src/stage/index.js", generatedBootstrapRendererSource({ gameId, role: "stage" }));
119
+ writeText(stagingRoot, "src/controller/index.js", generatedBootstrapRendererSource({ gameId, role: "controller" }));
120
+ writeText(stagingRoot, "src/tools/index.js", generatedContributionSource("authenticated tool panel"));
121
+ writeText(stagingRoot, "src/plugin/index.js", generatedPluginSource(pluginNamespace));
122
+ writeText(stagingRoot, "tests/config.test.js", generatedConfigTestSource({ engineVersion, gameId, pluginNamespace }));
123
+ writeText(stagingRoot, ".gitignore", "node_modules/\n.env\n.pop-party/\ndist/\noutputs/\n");
124
+ writeText(stagingRoot, "README.md", `# ${displayName}\n\nIndependent Pop Party game using \`@pop-party/engine@${engineVersion}\`.\n\nRun \`npm run dev\` locally or \`npm start\` in production. Development seeds ignored \`.pop-party/content\` once and then preserves that independent local copy; production uses the configured active store. The engine validates the selected release before binding the service port. Game-owned contributions live under \`src/actions\`, \`src/stage\`, \`src/controller\`, and \`src/tools\`; register them through the namespaced plugin in \`src/plugin\`. Content and starter blobs under \`content\` are independent copies owned by this game. Authenticated tools fail closed until the game explicitly configures them.\n`);
125
+ writeText(stagingRoot, "render.yaml", generatedRenderBlueprintSource(gameId));
126
+ writeText(stagingRoot, "DEPLOYMENT.md", `# ${displayName} deployment\n\nThis game deploys as one independent Render web service defined by \`render.yaml\`. Keep \`numInstances: 1\` and autoscaling disabled while rooms are in-memory. Render builds with \`npm install --no-audit --no-fund && npm run build\`, starts with \`npm start\`, and checks \`/health\`.\n\nProduction must use reviewed provider credentials and an immutable active release. Do not point \`POP_PARTY_CONTENT_ROOT\` at \`.pop-party/content\`; that override is only for the engine-owned local development command. Configure provider, OAuth, and content-writer secrets in Render rather than committing them.\n`);
127
+ writeText(stagingRoot, "LICENSE", fs.readFileSync(path.join(__dirname, "..", "LICENSE"), "utf8"));
128
+ writeText(stagingRoot, "CONTENT-LICENSE", "Canonical starter art and content were copied into this game under CC0-1.0 (https://creativecommons.org/publicdomain/zero/1.0/). This copy is owned and editable by this game.\n");
129
+ writeText(
130
+ stagingRoot,
131
+ "STARTER-ASSET-NOTICES.json",
132
+ fs.readFileSync(path.join(__dirname, "..", "starter", "ASSET-NOTICES.json"), "utf8")
133
+ );
134
+ if (fs.existsSync(targetRoot)) fs.rmdirSync(targetRoot);
135
+ fs.renameSync(stagingRoot, targetRoot);
136
+ } catch (error) {
137
+ fs.rmSync(stagingRoot, { recursive: true, force: true });
138
+ throw error;
139
+ }
140
+ return Object.freeze({ displayName, engineVersion, gameId, pluginNamespace, targetRoot });
141
+ }
142
+
143
+ module.exports = Object.freeze({
144
+ EXACT_VERSION_PATTERN,
145
+ GAME_ID_PATTERN,
146
+ assertExactEngineVersion,
147
+ copyTree,
148
+ gameIdFromName,
149
+ generateGame
150
+ });
@@ -0,0 +1,73 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "collection": "Pop Party Engine canonical starter art",
4
+ "license": "CC0-1.0",
5
+ "licenseUrl": "https://creativecommons.org/publicdomain/zero/1.0/",
6
+ "publisher": "Mark Turowetz",
7
+ "assets": [
8
+ {
9
+ "id": "avatar-frame",
10
+ "name": "Shared Avatar Frame",
11
+ "sourceName": "avatar-frame.svg",
12
+ "blobPath": "blobs/2d1298fc8597b4b13dc74c8a4093ce7c8d6027ef955c4af31ed6a6d03034d527.svg",
13
+ "sha256": "2d1298fc8597b4b13dc74c8a4093ce7c8d6027ef955c4af31ed6a6d03034d527",
14
+ "mimeType": "image/svg+xml"
15
+ },
16
+ {
17
+ "id": "avatar-rex",
18
+ "name": "Rex Dinosaur",
19
+ "sourceName": "dino-rex.svg",
20
+ "blobPath": "blobs/8785460bf8b2311bfd2a5c06e97f9faac0deafd584b8f3de428c38399e2df16d.svg",
21
+ "sha256": "8785460bf8b2311bfd2a5c06e97f9faac0deafd584b8f3de428c38399e2df16d",
22
+ "mimeType": "image/svg+xml"
23
+ },
24
+ {
25
+ "id": "avatar-stego",
26
+ "name": "Stego Dinosaur",
27
+ "sourceName": "dino-stego.svg",
28
+ "blobPath": "blobs/4d1b3ad12d64d7f145e282dc44b2c510d8cc36c0a609fcef831fe9c59270bf35.svg",
29
+ "sha256": "4d1b3ad12d64d7f145e282dc44b2c510d8cc36c0a609fcef831fe9c59270bf35",
30
+ "mimeType": "image/svg+xml"
31
+ },
32
+ {
33
+ "id": "avatar-trike",
34
+ "name": "Trike Dinosaur",
35
+ "sourceName": "dino-trike.svg",
36
+ "blobPath": "blobs/a531c3ee6f5bfa5b478b6ecda5799f1031ceea367ca9d93585545b3de0ed34d7.svg",
37
+ "sha256": "a531c3ee6f5bfa5b478b6ecda5799f1031ceea367ca9d93585545b3de0ed34d7",
38
+ "mimeType": "image/svg+xml"
39
+ },
40
+ {
41
+ "id": "avatar-raptor",
42
+ "name": "Raptor Dinosaur",
43
+ "sourceName": "dino-raptor.svg",
44
+ "blobPath": "blobs/d54e3eae53b88a705370aa028c3ea55297d549e3521aca6a41435f87e4a18167.svg",
45
+ "sha256": "d54e3eae53b88a705370aa028c3ea55297d549e3521aca6a41435f87e4a18167",
46
+ "mimeType": "image/svg+xml"
47
+ },
48
+ {
49
+ "id": "avatar-bronto",
50
+ "name": "Bronto Dinosaur",
51
+ "sourceName": "dino-bronto.svg",
52
+ "blobPath": "blobs/1c14e1db26cfb96105b2cbf60591a18329834078cc65e14373715d4ec06f07ef.svg",
53
+ "sha256": "1c14e1db26cfb96105b2cbf60591a18329834078cc65e14373715d4ec06f07ef",
54
+ "mimeType": "image/svg+xml"
55
+ },
56
+ {
57
+ "id": "avatar-ankylo",
58
+ "name": "Ankylo Dinosaur",
59
+ "sourceName": "dino-ankylo.svg",
60
+ "blobPath": "blobs/8dbc40965debd1f9f516461ed2b6ca0dd95557f8a762dbeaf67459e4b343497a.svg",
61
+ "sha256": "8dbc40965debd1f9f516461ed2b6ca0dd95557f8a762dbeaf67459e4b343497a",
62
+ "mimeType": "image/svg+xml"
63
+ },
64
+ {
65
+ "id": "presentation-click-cursor",
66
+ "name": "Presentation Click Cursor",
67
+ "sourceName": "cursor-arrow.svg",
68
+ "blobPath": "blobs/7a3a06ea1280a916254e2af034762e798b11e45f5eb7a1f4e0ba1a3ad4b8af48.svg",
69
+ "sha256": "7a3a06ea1280a916254e2af034762e798b11e45f5eb7a1f4e0ba1a3ad4b8af48",
70
+ "mimeType": "image/svg+xml"
71
+ }
72
+ ]
73
+ }