@jgengine/assets 0.10.0 → 0.12.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/CHANGELOG.md +39 -2
- package/README.md +103 -50
- package/dist/aliases.d.ts +5 -0
- package/dist/aliases.js +28 -9
- package/dist/catalogs/build.d.ts +27 -0
- package/dist/catalogs/build.js +28 -2
- package/dist/catalogs/buildSprites.d.ts +10 -0
- package/dist/catalogs/buildSprites.js +15 -0
- package/dist/catalogs/starter.d.ts +5 -0
- package/dist/catalogs/starter.js +10 -1
- package/dist/cli/paths.d.ts +9 -1
- package/dist/cli/paths.js +11 -1
- package/dist/cli/pull.d.ts +12 -0
- package/dist/cli/pull.js +216 -25
- package/dist/collisionMeshAssets.d.ts +2 -0
- package/dist/collisionMeshAssets.js +6 -0
- package/dist/dims.d.ts +7 -0
- package/dist/dims.js +154 -0
- package/dist/download.d.ts +66 -7
- package/dist/download.js +264 -18
- package/dist/find.d.ts +20 -1
- package/dist/find.js +52 -2
- package/dist/generated/index.js +18 -6
- package/dist/generated/kaykit-adventurers.json +706 -0
- package/dist/generated/kaykit-city-builder.json +904 -0
- package/dist/generated/kaykit-dungeon.json +5002 -0
- package/dist/generated/kaykit-furniture.json +1168 -0
- package/dist/generated/kaykit-skeletons.json +410 -0
- package/dist/generated/kaykit-space-base.json +1199 -0
- package/dist/generated/quaternius-medieval-village.json +3698 -0
- package/dist/generated/quaternius-modular-scifi.json +4013 -0
- package/dist/generated/quaternius-stylized-nature.json +1498 -0
- package/dist/generated-sprites/index.d.ts +5 -0
- package/dist/generated-sprites/index.js +4 -0
- package/dist/index.d.ts +9 -2
- package/dist/index.js +9 -2
- package/dist/indexGen.d.ts +2 -2
- package/dist/indexGen.js +34 -10
- package/dist/manifest.d.ts +16 -1
- package/dist/materials.d.ts +61 -0
- package/dist/materials.js +98 -0
- package/dist/packs/starter.d.ts +46 -0
- package/dist/packs/starter.js +248 -0
- package/dist/provisioning.d.ts +88 -0
- package/dist/provisioning.js +74 -0
- package/dist/registry-catalog.json +35 -0
- package/dist/snippet.d.ts +4 -0
- package/dist/snippet.js +27 -0
- package/dist/sources/ambientcg.d.ts +5 -0
- package/dist/sources/ambientcg.js +64 -0
- package/dist/sources/gameicons.d.ts +10 -0
- package/dist/sources/gameicons.js +21 -0
- package/dist/sources/index.d.ts +9 -2
- package/dist/sources/index.js +11 -3
- package/dist/sources/kaykit.js +55 -6
- package/dist/sources/quaternius.js +60 -9
- package/dist/spriteIndexGen.d.ts +17 -0
- package/dist/spriteIndexGen.js +108 -0
- package/dist/verify.d.ts +1 -0
- package/dist/verify.js +38 -1
- package/package.json +4 -4
- package/skills/jgengine/SKILL.md +54 -0
- package/skills/jgengine/api.md +527 -0
- package/skills/jgengine/capabilities.md +131 -0
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-assets/SKILL.md +45 -0
- package/skills/jgengine-assets/api.md +282 -0
- package/skills/jgengine-assets/capabilities.md +13 -0
- package/dist/generated/kenney-blaster.json +0 -362
- package/dist/generated/kenney-nature.json +0 -3292
- package/dist/generated/kenney-space.json +0 -1532
- package/dist/sources/kenney.d.ts +0 -2
- package/dist/sources/kenney.js +0 -36
- package/llms.txt +0 -1010
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { sourceById } from "./sources/index.js";
|
|
4
|
+
/** Strips the `.svg`/`.png` extension off a pulled sprite/icon filename to get its id suffix. */
|
|
5
|
+
export function keyFromSpriteFile(file) {
|
|
6
|
+
return file.replace(/\.(svg|png)$/i, "");
|
|
7
|
+
}
|
|
8
|
+
/** Builds one sprite/icon `IndexEntry` — same shape as a model entry, minus `dims`. */
|
|
9
|
+
export function entryForSpriteFile(source, file) {
|
|
10
|
+
return {
|
|
11
|
+
id: `${source.id}/${keyFromSpriteFile(file)}`,
|
|
12
|
+
source: source.id,
|
|
13
|
+
categories: source.categories,
|
|
14
|
+
file,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function collectSpriteFiles(dir) {
|
|
18
|
+
const found = [];
|
|
19
|
+
for (const name of readdirSync(dir)) {
|
|
20
|
+
const full = join(dir, name);
|
|
21
|
+
if (statSync(full).isDirectory()) {
|
|
22
|
+
found.push(...collectSpriteFiles(full));
|
|
23
|
+
}
|
|
24
|
+
else if (/\.(svg|png)$/i.test(name)) {
|
|
25
|
+
found.push({ file: name, full });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return found;
|
|
29
|
+
}
|
|
30
|
+
/** Walks a pulled sprite-pack directory and returns one `IndexEntry` per SVG/PNG, deduped by filename. */
|
|
31
|
+
export function indexSpriteSourceDir(source, dir) {
|
|
32
|
+
const seen = new Set();
|
|
33
|
+
const entries = [];
|
|
34
|
+
for (const { file } of collectSpriteFiles(dir).sort((a, b) => a.file.localeCompare(b.file))) {
|
|
35
|
+
if (seen.has(file))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(file);
|
|
38
|
+
entries.push(entryForSpriteFile(source, file));
|
|
39
|
+
}
|
|
40
|
+
return entries;
|
|
41
|
+
}
|
|
42
|
+
function safeIdentifier(sourceId) {
|
|
43
|
+
return `s_${sourceId.replace(/[^a-zA-Z0-9]/g, "_")}`;
|
|
44
|
+
}
|
|
45
|
+
function renderTsBarrel(sourceIds) {
|
|
46
|
+
const ordered = [...sourceIds].sort((a, b) => a.localeCompare(b));
|
|
47
|
+
const imports = ordered
|
|
48
|
+
.map((id) => `import ${safeIdentifier(id)} from "./${id}.json" with { type: "json" };`)
|
|
49
|
+
.join("\n");
|
|
50
|
+
const table = ordered
|
|
51
|
+
.map((id) => ` ${JSON.stringify(id)}: ${safeIdentifier(id)} as IndexEntry[],`)
|
|
52
|
+
.join("\n");
|
|
53
|
+
return `// AUTO-GENERATED by \`assets reindex-sprites\`. Do not edit by hand.
|
|
54
|
+
import type { IndexEntry } from "../manifest";
|
|
55
|
+
${imports}
|
|
56
|
+
|
|
57
|
+
/** Pulled sprite/icon entries grouped by source id. */
|
|
58
|
+
export const generatedSpriteBySource: Record<string, IndexEntry[]> = {
|
|
59
|
+
${table}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/** Every pulled sprite/icon entry across all sources, flattened. */
|
|
63
|
+
export const generatedSpriteIndex: IndexEntry[] = Object.values(generatedSpriteBySource).flat();
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
function renderJsBarrel(sourceIds) {
|
|
67
|
+
const ordered = [...sourceIds].sort((a, b) => a.localeCompare(b));
|
|
68
|
+
const imports = ordered
|
|
69
|
+
.map((id) => `import ${safeIdentifier(id)} from "./${id}.json" with { type: "json" };`)
|
|
70
|
+
.join("\n");
|
|
71
|
+
const table = ordered.map((id) => ` ${JSON.stringify(id)}: ${safeIdentifier(id)},`).join("\n");
|
|
72
|
+
return `// AUTO-GENERATED by \`assets reindex-sprites\`. Do not edit by hand.
|
|
73
|
+
${imports}
|
|
74
|
+
|
|
75
|
+
export const generatedSpriteBySource = {
|
|
76
|
+
${table}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const generatedSpriteIndex = Object.values(generatedSpriteBySource).flat();
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
/** Same shape as `reindex` (models) but walks SVG/PNG files and skips dims measurement — sprites have no footprint. */
|
|
83
|
+
export function reindexSprites(spritesDir, outDir) {
|
|
84
|
+
mkdirSync(outDir, { recursive: true });
|
|
85
|
+
const perSource = [];
|
|
86
|
+
const subdirs = existsSync(spritesDir)
|
|
87
|
+
? readdirSync(spritesDir).filter((name) => statSync(join(spritesDir, name)).isDirectory())
|
|
88
|
+
: [];
|
|
89
|
+
for (const sourceId of subdirs.sort((a, b) => a.localeCompare(b))) {
|
|
90
|
+
const source = sourceById.get(sourceId);
|
|
91
|
+
if (source === undefined || source.kind !== "sprite") {
|
|
92
|
+
perSource.push({ source: `${sourceId} (unknown, skipped)`, count: 0 });
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const entries = indexSpriteSourceDir(source, join(spritesDir, sourceId));
|
|
96
|
+
writeFileSync(join(outDir, `${sourceId}.json`), `${JSON.stringify(entries, null, 2)}\n`);
|
|
97
|
+
perSource.push({ source: sourceId, count: entries.length });
|
|
98
|
+
}
|
|
99
|
+
const barrelIds = readdirSync(outDir)
|
|
100
|
+
.filter((name) => name.endsWith(".json"))
|
|
101
|
+
.map((name) => name.slice(0, -".json".length))
|
|
102
|
+
.filter((id) => sourceById.has(id));
|
|
103
|
+
writeFileSync(join(outDir, "index.ts"), renderTsBarrel(barrelIds));
|
|
104
|
+
if (!/[/\\]src[/\\]generated-sprites$/i.test(outDir)) {
|
|
105
|
+
writeFileSync(join(outDir, "index.js"), renderJsBarrel(barrelIds));
|
|
106
|
+
}
|
|
107
|
+
return { perSource, total: perSource.reduce((sum, entry) => sum + entry.count, 0) };
|
|
108
|
+
}
|
package/dist/verify.d.ts
CHANGED
|
@@ -8,6 +8,7 @@ export interface VerifyInput {
|
|
|
8
8
|
singles: readonly SingleAsset[];
|
|
9
9
|
aliases: readonly AssetAlias[];
|
|
10
10
|
index: readonly IndexEntry[];
|
|
11
|
+
materialAliases?: readonly AssetAlias[];
|
|
11
12
|
}
|
|
12
13
|
export declare function verifyData(input: VerifyInput): VerifyResult;
|
|
13
14
|
export declare function verifyManifest(): VerifyResult;
|
package/dist/verify.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import { aliases } from "./aliases.js";
|
|
2
2
|
import { generatedIndex } from "./generated/index.js";
|
|
3
|
+
import { materialAliases } from "./materials.js";
|
|
3
4
|
import { singles } from "./singles.js";
|
|
4
5
|
import { sources } from "./sources/index.js";
|
|
6
|
+
/** Providers allowed repo-wide; Kenney.nl is permanently barred — see CLAUDE.md "Never Kenney". */
|
|
7
|
+
const ALLOWED_PROVIDERS = ["quaternius", "kaykit", "ambientcg", "gameicons"];
|
|
8
|
+
function containsKenney(value) {
|
|
9
|
+
return value.toLowerCase().includes("kenney");
|
|
10
|
+
}
|
|
5
11
|
export function verifyData(input) {
|
|
6
12
|
const errors = [];
|
|
7
13
|
for (const source of input.sources) {
|
|
@@ -9,6 +15,11 @@ export function verifyData(input) {
|
|
|
9
15
|
errors.push(`source ${source.id}: missing license`);
|
|
10
16
|
if (source.author.trim().length === 0)
|
|
11
17
|
errors.push(`source ${source.id}: missing author`);
|
|
18
|
+
if (!ALLOWED_PROVIDERS.includes(source.provider)) {
|
|
19
|
+
errors.push(`source ${source.id}: provider "${source.provider}" is not allowlisted (Never Kenney — see CLAUDE.md)`);
|
|
20
|
+
}
|
|
21
|
+
if (containsKenney(source.id))
|
|
22
|
+
errors.push(`source ${source.id}: id contains banned "kenney" substring`);
|
|
12
23
|
}
|
|
13
24
|
for (const single of input.singles) {
|
|
14
25
|
if (single.license.trim().length === 0)
|
|
@@ -17,6 +28,20 @@ export function verifyData(input) {
|
|
|
17
28
|
errors.push(`single ${single.id}: missing author`);
|
|
18
29
|
if (single.url.trim().length === 0)
|
|
19
30
|
errors.push(`single ${single.id}: missing url`);
|
|
31
|
+
if (containsKenney(single.id))
|
|
32
|
+
errors.push(`single ${single.id}: id contains banned "kenney" substring`);
|
|
33
|
+
if (containsKenney(single.url))
|
|
34
|
+
errors.push(`single ${single.id}: url contains banned "kenney" substring`);
|
|
35
|
+
}
|
|
36
|
+
for (const entry of input.index) {
|
|
37
|
+
if (containsKenney(entry.id))
|
|
38
|
+
errors.push(`index entry ${entry.id}: id contains banned "kenney" substring`);
|
|
39
|
+
}
|
|
40
|
+
for (const alias of [...input.aliases, ...(input.materialAliases ?? [])]) {
|
|
41
|
+
if (containsKenney(alias.key))
|
|
42
|
+
errors.push(`alias ${alias.key}: key contains banned "kenney" substring`);
|
|
43
|
+
if (containsKenney(alias.target))
|
|
44
|
+
errors.push(`alias ${alias.key}: target contains banned "kenney" substring`);
|
|
20
45
|
}
|
|
21
46
|
const knownIds = new Set([
|
|
22
47
|
...input.index.map((entry) => entry.id),
|
|
@@ -27,8 +52,20 @@ export function verifyData(input) {
|
|
|
27
52
|
errors.push(`alias ${alias.key}: target "${alias.target}" not found in index or singles`);
|
|
28
53
|
}
|
|
29
54
|
}
|
|
55
|
+
const materialIds = new Set(input.sources.filter((source) => source.kind === "material").map((source) => source.id));
|
|
56
|
+
for (const alias of input.materialAliases ?? []) {
|
|
57
|
+
if (!materialIds.has(alias.target)) {
|
|
58
|
+
errors.push(`material alias ${alias.key}: target "${alias.target}" is not a material source`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const seenIds = new Set();
|
|
62
|
+
for (const source of input.sources) {
|
|
63
|
+
if (seenIds.has(source.id))
|
|
64
|
+
errors.push(`source ${source.id}: duplicate id`);
|
|
65
|
+
seenIds.add(source.id);
|
|
66
|
+
}
|
|
30
67
|
return { ok: errors.length === 0, errors };
|
|
31
68
|
}
|
|
32
69
|
export function verifyManifest() {
|
|
33
|
-
return verifyData({ sources, singles, aliases, index: generatedIndex });
|
|
70
|
+
return verifyData({ sources, singles, aliases, index: generatedIndex, materialAliases });
|
|
34
71
|
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/assets",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Self-generating, license-verified index of thousands of CC0 3D models for JGengine. Sources fetch from providers' own CDNs at pull time; the npm tarball ships only the typed index and CLI (no GLB bytes).",
|
|
5
|
-
"license": "
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"repository": {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"files": [
|
|
14
14
|
"dist",
|
|
15
15
|
"CHANGELOG.md",
|
|
16
|
-
"
|
|
16
|
+
"skills"
|
|
17
17
|
],
|
|
18
18
|
"bin": {
|
|
19
19
|
"assets": "dist/cli/pull.js"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
38
|
+
"@jgengine/core": "^0.12.0",
|
|
39
39
|
"fflate": "^0.8.2"
|
|
40
40
|
},
|
|
41
41
|
"devDependencies": {
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: jgengine
|
|
3
|
+
description: Route game work to the smallest useful JGengine guidance set.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# JGengine intake and router
|
|
7
|
+
|
|
8
|
+
Read the repository [README](../../../README.md) for package truth, commands, layering, and license. This skill decides what to load; it is not an engine manual.
|
|
9
|
+
|
|
10
|
+
## 1. Establish the target
|
|
11
|
+
|
|
12
|
+
Treat the pitch as a unique composition of needs, not a genre to fill in — never reach for a genre kit, preset, archetype, or class template ("default sports car", "default RPG", "default FPS") in the SDK or skills (see [AGENTS.md](../../../AGENTS.md)). Use `game-design` to turn the pitch into a testable player promise, verbs, loops, progression, failure, and completion scenario. Use `level-design` when play depends on authored spaces, routes, encounters, exploration, or spatial pacing. Capture only decisions that change architecture:
|
|
13
|
+
|
|
14
|
+
- player point of view and controls
|
|
15
|
+
- world shape and authored content
|
|
16
|
+
- core loop, interaction, combat, and progression
|
|
17
|
+
- solo, local, hosted, or persistent multiplayer
|
|
18
|
+
- HUD/menu states and **custom** UI art direction (every game owns its presentation — no stock drop-in face)
|
|
19
|
+
- one observable completion scenario
|
|
20
|
+
|
|
21
|
+
For a new in-repo game, scaffold with `bun run new:game "<id-or-title>"` (thin script alias for `jgengine create`, auto-detects the in-repo variant). Outside this monorepo, start with `npx jgengine create`; never copy `Games/*`.
|
|
22
|
+
|
|
23
|
+
## 2. Select domains
|
|
24
|
+
|
|
25
|
+
Load only rows the target needs. Do not preload every domain.
|
|
26
|
+
|
|
27
|
+
| Need | Skill | Owner |
|
|
28
|
+
| --- | --- | --- |
|
|
29
|
+
| player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
|
|
30
|
+
| metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
|
|
31
|
+
| scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
|
|
32
|
+
| runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
|
|
33
|
+
| game state, systems, inventory, crafting, quests, economy, turns | `jgengine-gameplay` | non-combat rules and state |
|
|
34
|
+
| targeting, stats, damage, effects, abilities, projectiles, loot | `jgengine-combat` | combat resolution |
|
|
35
|
+
| HUD, menus, layout, accessibility, touch presentation | `jgengine-ui` | player-facing interface |
|
|
36
|
+
| models, sprites, materials, audio files, source licensing | `jgengine-assets` | asset discovery and catalogs |
|
|
37
|
+
| authority, transport, replication, sessions, host persistence | `jgengine-multiplayer` | network topology and persistence adapters |
|
|
38
|
+
|
|
39
|
+
World content routes editor-first: any request that adds, moves, restyles, or removes something visible in the world — however phrased ("design this world", "place the enemies", "make it look better") — starts in `jgengine-editor`; the other domains consume the authored document. Scenes normally require both `jgengine-editor` for authoring and `jgengine-world` for runtime consumption. Combat and multiplayer are opt-in, not default intake.
|
|
40
|
+
|
|
41
|
+
## 3. Discover before designing
|
|
42
|
+
|
|
43
|
+
Within each selected domain:
|
|
44
|
+
|
|
45
|
+
1. Search `capabilities.md` by intent; it maps needs to imports.
|
|
46
|
+
2. Open `api.md` only when exact signatures or export inventory are needed.
|
|
47
|
+
3. Open the linked reference only for a deeper recipe or trap.
|
|
48
|
+
4. For how several primitives wire into a running loop, read the domain's `recipes/` — connected, genre-free walkthroughs organized by composition seam; this skill's own [recipes/minimal-game.md](recipes/minimal-game.md) is the default whole-game walkthrough. Prefer a recipe over reading a game's source; the games under `Games/*` are not templates or references, and a hybrid game is just a different composition of the same primitives.
|
|
49
|
+
|
|
50
|
+
If no capability fits, identify the upstream package seam before writing game-local code. In this repository, implement a reusable primitive with its first adopter; if editor-owned content cannot be expressed, file the editor gap first.
|
|
51
|
+
|
|
52
|
+
## 4. Build and finish
|
|
53
|
+
|
|
54
|
+
Keep scene coordinates in the editor document, state serializable, randomness injected, and hot-path work bounded. Use `jgengine-verify` for evidence appropriate to the change. Inside this monorepo, `workflow` owns issue/PR completion. Visual polish work also uses the `jgengine-ui` quality reference.
|