@jgengine/assets 0.9.0 → 0.11.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 (68) hide show
  1. package/CHANGELOG.md +34 -3
  2. package/README.md +103 -50
  3. package/dist/aliases.d.ts +5 -0
  4. package/dist/aliases.js +28 -9
  5. package/dist/catalogs/build.js +9 -0
  6. package/dist/catalogs/buildSprites.d.ts +10 -0
  7. package/dist/catalogs/buildSprites.js +15 -0
  8. package/dist/catalogs/starter.d.ts +5 -0
  9. package/dist/catalogs/starter.js +10 -1
  10. package/dist/cli/paths.d.ts +9 -1
  11. package/dist/cli/paths.js +11 -1
  12. package/dist/cli/pull.d.ts +12 -0
  13. package/dist/cli/pull.js +210 -15
  14. package/dist/download.d.ts +46 -3
  15. package/dist/download.js +158 -9
  16. package/dist/find.d.ts +20 -1
  17. package/dist/find.js +52 -2
  18. package/dist/generated/index.js +18 -6
  19. package/dist/generated/kaykit-adventurers.json +674 -0
  20. package/dist/generated/kaykit-city-builder.json +863 -0
  21. package/dist/generated/kaykit-dungeon.json +4265 -0
  22. package/dist/generated/kaykit-furniture.json +1115 -0
  23. package/dist/generated/kaykit-skeletons.json +410 -0
  24. package/dist/generated/kaykit-space-base.json +1199 -0
  25. package/dist/generated/quaternius-medieval-village.json +3698 -0
  26. package/dist/generated/quaternius-modular-scifi.json +4013 -0
  27. package/dist/generated/quaternius-stylized-nature.json +1430 -0
  28. package/dist/generated-sprites/index.d.ts +5 -0
  29. package/dist/generated-sprites/index.js +4 -0
  30. package/dist/index.d.ts +9 -2
  31. package/dist/index.js +9 -2
  32. package/dist/indexGen.js +7 -5
  33. package/dist/manifest.d.ts +12 -1
  34. package/dist/materials.d.ts +61 -0
  35. package/dist/materials.js +98 -0
  36. package/dist/packs/starter.d.ts +46 -0
  37. package/dist/packs/starter.js +248 -0
  38. package/dist/provisioning.d.ts +88 -0
  39. package/dist/provisioning.js +74 -0
  40. package/dist/registry-catalog.json +40 -0
  41. package/dist/snippet.d.ts +4 -0
  42. package/dist/snippet.js +27 -0
  43. package/dist/sources/ambientcg.d.ts +5 -0
  44. package/dist/sources/ambientcg.js +64 -0
  45. package/dist/sources/gameicons.d.ts +10 -0
  46. package/dist/sources/gameicons.js +21 -0
  47. package/dist/sources/index.d.ts +9 -2
  48. package/dist/sources/index.js +11 -3
  49. package/dist/sources/kaykit.js +55 -6
  50. package/dist/sources/quaternius.js +60 -9
  51. package/dist/spriteIndexGen.d.ts +17 -0
  52. package/dist/spriteIndexGen.js +108 -0
  53. package/dist/verify.d.ts +1 -0
  54. package/dist/verify.js +38 -1
  55. package/package.json +4 -4
  56. package/skills/jgengine/SKILL.md +51 -0
  57. package/skills/jgengine/api.md +434 -0
  58. package/skills/jgengine/capabilities.md +29 -0
  59. package/skills/jgengine/reference-cartridge.md +72 -0
  60. package/skills/jgengine-assets/SKILL.md +41 -0
  61. package/skills/jgengine-assets/api.md +273 -0
  62. package/skills/jgengine-assets/capabilities.md +13 -0
  63. package/dist/generated/kenney-blaster.json +0 -362
  64. package/dist/generated/kenney-nature.json +0 -3292
  65. package/dist/generated/kenney-space.json +0 -1532
  66. package/dist/sources/kenney.d.ts +0 -2
  67. package/dist/sources/kenney.js +0 -36
  68. package/llms.txt +0 -997
package/CHANGELOG.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # Changelog
2
2
 
3
- All eight `@jgengine/*` packages are versioned in lockstep, so this one file
4
- covers every release. Format follows [Keep a Changelog](https://keepachangelog.com);
3
+ The lockstep game SDK set is `@jgengine/{core,react,ws,node,sql,convex,shell,editor,assets}`
4
+ (this file’s 0.x.y releases). The CLI package `jgengine` and `@jgengine/github` use
5
+ **separate** version lines and may lag. Format follows [Keep a Changelog](https://keepachangelog.com);
5
6
  each release **leads with a Migrate block** — the concrete steps to move a game
6
7
  from the previous version onto the new APIs — because the point of a bump is to
7
8
  let consumers pick up the better stuff, not just to list what moved.
@@ -11,7 +12,37 @@ Agents building on the published SDK can also read this programmatically:
11
12
  same data as typed values, so an updater can diff its installed version against
12
13
  the latest and surface the migration steps.
13
14
 
14
- ## Unreleased
15
+ ## 0.11.0
16
+
17
+ ### Migrate
18
+
19
+ - **Nothing required** — classic `loop.onTick` fan-out still runs after systems. Prefer moving per-frame work into `defineGame({ systems: [...] })` with `defineSystem` (`@jgengine/core/game/defineSystem`); install a system with `feature: "quest"` (etc.) instead of a redundant `features` flag. See skill `jgengine` → [reference-systems.md](.claude/skills/jgengine/reference-systems.md).
20
+
21
+ ### Added
22
+
23
+ - **Composable game systems** (#842) — `defineSystem` / `compileSystemSchedule` / `composeGameLoop` / `installSystems`. `defineGame({ systems })` is the single authoring path: fixed / frame / interval / event-only / manual ticks, multi-subscribe channels, deterministic stage order + optional `before`/`after`/`dependsOn`, system-owned save / replicate / reset / dispose. Installing a system activates its `feature` without a separate flag. `GameLoop` gains optional `onReset` / `onDispose`. `ctx.game.registerSave` / `registerReplicate` for system modules. Adopters: tower-guard, claudecraft (runtime without a giant tick fan-out).
24
+
25
+ ## 0.10.0
26
+
27
+ ### Migrate
28
+
29
+ - **Nothing to change for existing games** — the shared mobile-composition system (viewport allocation, region collision, `--jg-*` viewport vars) is wired automatically by the shell for every game; `HudPanel`s inside a `HudCanvas` register themselves. Legacy `orientation: "landscape" | "portrait"` keeps its advisory dismissible-hint behavior unchanged.
30
+ - **Opt a driving/landscape game into the strict gate**: replace the advisory `orientation` with the contract form — `orientation: { mobile: "landscape-required" }` (rules: `any` · `portrait`/`landscape` advisory · `portrait-required`/`landscape-required` · `unsupported`). In portrait the engine now shows a polished `RotateDeviceScreen`, suppresses input, freezes the sim, and unmounts the HUD/controls until the device is landscape.
31
+ - **Declare HUD intent** where useful: `HudPanel` gains `priority` (`critical`/`secondary`/`tertiary`), `mobileBehavior` (`hidden` unmounts on phones, `transient` softens collision, …), `allowOverlapWith`, and `collisionGroup`. Optional — omitting them keeps prior behavior.
32
+ - **Mobile validation now also fails on overlap**: `bun run shoot <game> --device mobile|mobile-landscape|both` exits non-zero on forbidden inter-element collisions (not just viewport overflow), naming both regions.
33
+
34
+ ### Added
35
+
36
+ - Shared mobile layout composition — `@jgengine/core/ui/gameLayout` (pure geometry: `resolveLayoutMode`, `intersects`, `overlapArea`, `detectLayoutCollisions`, `computeGameplayRect`, `LayoutRegion`, `GameViewportLayout`) and `@jgengine/core/ui/orientation` (`resolveOrientationRequirement`, `orientationGateActive`, `MobileOrientationRule`). `@jgengine/react` adds `GameViewportProvider` (mounted by the shell), hooks (`useGameViewportLayout`, `useGameLayoutMode`, `useGameOrientation`, `useReservedControlZones`, `useLayoutCollisions`, `useRegisterLayoutRegion`), and a headless `RotateDeviceScreen`. The provider tracks `window.visualViewport`, publishes `--jg-viewport-*` / `--jg-visual-viewport-*` / `--jg-safe-*` CSS vars, resolves the explicit layout mode, and detects forbidden region overlaps (dev `data-jg-layout-collision` + console diagnostic).
37
+ - Mandatory orientation contract — `PlayableGame.orientation` accepts `{ mobile: <rule> }`; `landscape-required`/`portrait-required` render an engine-owned rotate gate that blocks gameplay (input suppressed, simulation frozen, HUD/controls unmounted), self-correcting when the device is turned. Reduced-motion and safe-area respected; `visualViewport`-sized for mobile Safari/PWA.
38
+ - Reserved touch-control zones — the touch dock registers its joystick / action-cluster / utility rectangles (runtime-measured) as `control` regions, so HUD placement and collision validation know exactly where controls sit.
39
+ - HUD placement metadata — `HudPanel` gains `priority`, `mobileBehavior`, `allowOverlapWith`, `collisionGroup`, and registers as a collision-tracked layout region.
40
+ - Mobile-landscape shoot device — `bun run shoot --device mobile-landscape` (844×390) plus collision-gate reads that fail mobile validation on forbidden overlaps.
41
+ - Canyon Chase migrated as the reference landscape-required game — declares `orientation: { mobile: "landscape-required" }`, composes its HUD through coordinated `HudPanel` regions (pursuit distance critical, border secondary, survey map hidden on mobile, radio transient) instead of independent fixed corners.
42
+
43
+ ### Changed
44
+
45
+ - First-person projectile tracers now originate from the weapon muzzle instead of the camera/eye centerline. Hit detection is unchanged (still crosshair-accurate) — only the drawn tracer's start point moved to the viewmodel muzzle, which tracks full aim yaw and pitch. Non-viewmodel games and enemy tracers are unaffected.
15
46
 
16
47
  ## 0.9.0
17
48
 
package/README.md CHANGED
@@ -1,37 +1,45 @@
1
1
  # @jgengine/assets
2
2
 
3
- A self-generating, license-verified index of thousands of CC0 3D models — hosted at **zero cost**. No GLB bytes ship in the npm tarball; every byte comes from infrastructure you don't pay for:
3
+ A self-generating, license-verified index of thousands of CC0 3D models, PBR materials, **and 2D sprite/icon packs** G�� hosted at **zero cost**. No GLB, texture, or sprite bytes ship in the npm tarball; every byte comes from infrastructure you don't pay for:
4
4
 
5
5
  | What | Where it lives | Whose bandwidth |
6
6
  |------|----------------|-----------------|
7
- | Pack GLBs (Kenney, Quaternius, KayKit…) | Fetched at `pull` time from the provider's CDN | Provider's |
7
+ | Pack GLBs (Quaternius, KayKitGǪ) | Fetched at `pull` time from the provider's CDN | Provider's |
8
+ | PBR materials (ambientCG) | Fetched at `pull` time (provider or the `packs` release mirror) | Provider's / GitHub's |
9
+ | Sprite/icon packs (game-icons.net) | Fetched at `pull` time (provider or the `packs` release mirror) | Provider's / GitHub's |
8
10
  | The generated index (JSON) | Inside this npm package | npm (KB) |
9
11
  | Your one-off models | `packages/assets/local/`, served via jsDelivr-over-GitHub | GitHub + jsDelivr |
10
- | A consumer's downloaded bytes | Their `public/models/` (gitignored) | The consumer's |
12
+ | A consumer's downloaded bytes | Their `public/models/` + `public/materials/` + `public/sprites/` (gitignored) | The consumer's |
11
13
 
12
14
  ## Layers
13
15
 
14
- 1. **Sources** (`src/sources/*.ts`) — one entry per downloadable pack. Every entry carries required `license` + `author` and a `download` that is either a pinned `{ url, sha256? }` or a `{ scrape }` marker.
15
- 2. **Generated index** (`src/generated/*.json`) — machine-produced from the real `.glb` filenames after a pack is extracted. Never hand-typed. Committed JSON; bytes are not.
16
- 3. **Aliases** (`src/aliases.ts`) — hand-authored semantic keys (`nature/tree_pine → kenney-nature/tree_pineDefaultA`).
17
- 4. **Singles** (`src/singles.json`) — long-tail one-offs (per-model `url` + `license`).
16
+ 1. **Sources** (`src/sources/*.ts`) G�� one entry per downloadable pack. Every entry carries required `license` + `author` and a `download` that is either a pinned `{ url, sha256? }` or a `{ scrape }` marker.
17
+ 2. **Generated index** (`src/generated/*.json`) G�� machine-produced from the real `.glb` filenames after a pack is extracted. Never hand-typed. Committed JSON; bytes are not.
18
+ 3. **Aliases** (`src/aliases.ts`) G�� hand-authored semantic keys (`nature/tree_pine G�� quaternius-stylized-nature/Pine_1`).
19
+ 4. **Singles** (`src/singles.json`) G�� long-tail one-offs (per-model `url` + `license`).
20
+ 5. **Materials** (`src/sources/ambientcg.ts` + `src/materials.ts`) G�� `kind: "material"` sources, one CC0 PBR material each (hundreds of ambientCG materials: grass, rock, wood, brick, metal, fabricGǪ). `pull` normalizes the maps to fixed filenames, so `buildMaterialCatalog({ basePath })` resolves ids and `material/GǪ` aliases to map URLs with no generated index at all.
21
+ 6. **Sprites** (`src/sources/gameicons.ts` + `src/spriteIndexGen.ts`) G�� `kind: "sprite"` sources, packs of individual SVG/PNG icon and UI files (game-icons.net's ~4,000 CC BY 3.0 icons as one mirrored repo). Same shape as models G�� `reindex-sprites` discovers real files after a pull and writes `src/generated-sprites/*.json`, resolved through `buildSpriteCatalog({ basePath })`.
18
22
 
19
- Everything collapses into the core `AssetCatalog` via `buildCatalog({ basePath })`.
23
+ Model entries collapse into the core `AssetCatalog` via `buildCatalog({ basePath })`; materials resolve through `buildMaterialCatalog({ basePath })`; sprites/icons resolve through `buildSpriteCatalog({ basePath })`.
20
24
 
21
- ## `add` — one command for anything
25
+ ## `add` G�� one command for anything
22
26
 
23
- `assets add <query>` is the front door. It fuzzy-searches **every** catalog at once — 3D models, whole packs, HUD components (the shadcn registry), and `game-icon` glyphs — then does the fetch and prints the exact copy-paste wiring. One mental model instead of four.
27
+ `assets add <query>` is the front door. It fuzzy-searches **every** catalog at once G�� 3D models, whole packs, PBR materials, sprite/icon packs, HUD components (the shadcn registry), and `game-icon` glyphs G�� then does the fetch and prints the exact copy-paste wiring. One mental model instead of six.
24
28
 
25
29
  ```bash
26
- assets add astronaut --dir ../../apps/dev/public # model → pull + reindex + print the assets.ts snippet
27
- assets add nature --dir ../../apps/dev/public # whole pack → pull + reindex + how to wire an id
28
- assets add "mana bar" # HUD component → the `npx shadcn add` cmd + <VitalBar/> usage
29
- assets add sword # icon → the game-icon name to drop in a slot
30
+ assets add astronaut --dir ../../apps/dev/public # model G�� pull + reindex + print the assets.ts snippet
31
+ assets add nature --dir ../../apps/dev/public # whole pack G�� pull + reindex + how to wire an id
32
+ assets add grass --kind material --dir ../../apps/dev/public # PBR material G�� pull maps + resolve snippet
33
+ assets add "game icons" --dir ../../apps/dev/public # sprite pack G�� pull + reindex-sprites + resolve snippet
34
+ assets add "mana bar" # HUD component G�� the `npx shadcn add` cmd + <VitalBar/> usage
35
+ assets add sword # icon G�� the game-icon name to drop in a slot
30
36
  ```
31
37
 
32
38
  - A **model / pack** match is fully automated: if the pack isn't already in `<dir>/models/`, it's pulled and extracted, then `reindex` runs so the id is addressable, then the `buildCatalog` + model-seam snippet is printed.
33
- - A **component / icon** match prints the one-liner to run and the import + usage — no bytes to pull.
34
- - Ambiguous query? `add` lists the top matches across kinds; narrow with `--kind model|pack|component|icon` or a more specific term. `--json` emits the ranked matches for scripting.
39
+ - A **material** match pulls the maps into `<dir>/materials/<id>/` with normalized names (`color.jpg`, `normal.jpg`, `roughness.jpg`, `ao.jpg`, `displacement.jpg`) and prints the `buildMaterialCatalog` resolve snippet G�� no reindex needed, the material catalog is fully static.
40
+ - A **sprite / spritePack** match works like model/pack: if the pack isn't already in `<dir>/sprites/`, it's pulled and extracted, then `reindex-sprites` runs so individual SVG/PNG ids are addressable, then the `buildSpriteCatalog` snippet is printed.
41
+ - A **component / icon** match prints the one-liner to run and the import + usage G�� no bytes to pull.
42
+ - Ambiguous query? `add` lists the top matches across kinds; narrow with `--kind model|pack|material|component|icon|sprite|spritePack` or a more specific term. `--json` emits the ranked matches for scripting.
35
43
 
36
44
  The same ranking is available programmatically: `import { findAssets } from "@jgengine/assets"`.
37
45
 
@@ -39,98 +47,143 @@ The same ranking is available programmatically: `import { findAssets } from "@jg
39
47
 
40
48
  ```
41
49
  assets add <query> [--kind <k>] [--dir <dir>] [--mirror <baseUrl>] [--json]
42
- # unified import: models, packs, components, icons
43
- assets list [--category <c>] [--source <s>] # browse the generated index
44
- assets search <term> # grep the index
50
+ # unified import: models, packs, materials, sprites, components, icons
51
+ assets list [--category <c>] [--source <s>] [--kind material|sprite|spritePack]
52
+ # browse the generated model/sprite index, or the material/sprite-pack catalog
53
+ assets search <term> # grep the model index
45
54
  assets pull <source-id> [--dir public] [--mirror <baseUrl>] [--offline]
46
- # download + extract a whole pack's GLBs into <dir>/models/<source>/
55
+ # download + extract a pack into <dir>/models|materials|sprites/<source>/
47
56
  assets register <path|url> --category <c> --license <l> [--author <a>]
48
57
  # register a one-off single into the shipped index
49
- assets reindex [public/models] # regenerate generated/*.json from pulled packs
58
+ assets reindex [public/models] # regenerate generated/*.json from pulled model packs
59
+ assets reindex-sprites [public/sprites] # regenerate generated-sprites/*.json from pulled sprite packs
50
60
  assets verify # license + alias-integrity gate
61
+ assets provenance <id> [<id>...] # resolve each id's owner (committed/provisioned/dangling); non-zero on any dangling
51
62
  ```
52
63
 
53
- Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> …`. (`add <path|url> --license …` still works as an alias for `register`.)
64
+ ### Reference integrity
65
+
66
+ Every logical asset id resolves to a declared **owner** through the provisioning contract (`resolveProvenance`): `committed` (a single's shipped URL — resolves on a clean clone with no pull), `provisioned` (a pack index entry whose GLB `assets pull <source>` fetches into the served dir), or `dangling` (nothing owns it, so it can never resolve). `validateAssetReferences(refs)` turns a set of `{ consumer, id }` references into a clean-clone gate: it fails on any dangling reference — naming the consumer, the logical id, and the null resolved path — and returns the exact `assets pull <source>` steps the checkout still needs so provisioning stays deterministic and `ensure-ready`-driven. It resolves references structurally against the catalog rather than grepping source text.
67
+
68
+ At runtime the shell validates a model fetch *before* parsing (`classifyAssetResponse` in `@jgengine/core/scene/assetDiagnostics`): it checks HTTP status, content type, and the GLB/glTF signature, then reports `missing`, `html` (a dev-server fallback page served in place of the file), `corrupt`, or `unsupported` with the URL — replacing the opaque `Unexpected token <` GLTF parse error the old path produced when a model was absent.
69
+
70
+ Run in-repo with `bun run --cwd packages/assets src/cli/pull.ts <verb> GǪ`. (`add <path|url> --license GǪ` still works as an alias for `register`.)
54
71
 
55
72
  ### Mirror fallback and offline pulls
56
73
 
57
74
  `pull` never has just one path to the bytes. For a given `source-id` it tries, in order, until one succeeds:
58
75
 
59
- 1. **Mirror base override** — `--mirror <baseUrl>` (or the `JGENGINE_ASSETS_MIRROR` env var if `--mirror` is not passed) — the archive is expected at `<baseUrl>/<provider>/<source-id>.zip`, e.g. `https://my-mirror.example.com/kenney/kenney-nature.zip`.
60
- 2. **The default GitHub-release mirror** — `https://github.com/Noisemaker111/jgengine/releases/download/packs/<provider>-<source-id>.zip`, on this repo's own rolling `packs` release (no separate assets repo). github.com is reachable from every cloud sandbox with no network-policy change, so zero-setup sessions still pull. `.github/workflows/mirror-assets.yml` (weekly cron + manual dispatch) keeps the release in sync with `src/sources/*.ts` automatically — adding a catalog entry is the whole publishing step, no manual upload. Skip this hop with `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`.
61
- 3. **The primary provider path** — the source's pinned `{ url, sha256? }` or, for providers that rotate URLs (Kenney), a scrape of the asset page.
62
- 4. **The pack's own `mirror`** — an optional direct archive URL set on the `AssetSource` entry itself (`src/sources/*.ts`), tried as a last resort.
76
+ 1. **Mirror base override** G�� `--mirror <baseUrl>` (or the `JGENGINE_ASSETS_MIRROR` env var if `--mirror` is not passed) G�� the archive is expected at `<baseUrl>/<provider>/<source-id>.zip`, e.g. `https://my-mirror.example.com/quaternius/quaternius-stylized-nature.zip`.
77
+ 2. **The default GitHub-release mirror** G�� `https://github.com/Noisemaker111/jgengine/releases/download/packs/<provider>-<source-id>.zip`, on this repo's own rolling `packs` release (no separate assets repo). github.com is reachable from every cloud sandbox with no network-policy change, so zero-setup sessions still pull. `.github/workflows/mirror-assets.yml` (weekly cron + manual dispatch) keeps the release in sync with `src/sources/*.ts` automatically G�� adding a catalog entry is the whole publishing step, no manual upload. Skip this hop with `JGENGINE_ASSETS_NO_DEFAULT_MIRROR=1`. Model/sprite pack failures fail the job; individual ambientCG material 404s are soft (logged, non-fatal).
78
+ 3. **The primary provider path** G�� pinned `{ url, sha256? }` when stable: **KayKit** G�� `github.com/KayKit-Game-Assets/*/archive/GǪ/main.zip`; **Quaternius** free Standard packs G�� OpenGameArt direct zips (site pages JS-gate). Else scrape. `extractGlbs` packs co-located `.gltf`+`.bin` into `.glb`.
79
+ 4. **The pack's own `mirror`** G�� an optional direct archive URL set on the `AssetSource` entry itself (`src/sources/*.ts`), tried as a last resort.
63
80
 
64
- If every attempt fails, `pull` throws one aggregated error naming every URL it tried and why each one failed. Whenever the source's `download` is pinned with a `sha256`, the downloaded bytes are hashed and checked against it **no matter which path supplied them** — a mirror serving stale or tampered bytes is rejected and the next source in the chain is tried instead.
81
+ If every attempt fails, `pull` throws one aggregated error naming every URL it tried and why each one failed. Whenever the source's `download` is pinned with a `sha256`, the downloaded bytes are hashed and checked against it **no matter which path supplied them** G�� a mirror serving stale or tampered bytes is rejected and the next source in the chain is tried instead.
65
82
 
66
- `--offline` skips the network entirely: it succeeds immediately if `<dir>/models/<source-id>/` already has files in it, and otherwise fails fast with a message telling you to pull once on a connected machine or point `--mirror`/`JGENGINE_ASSETS_MIRROR` at a reachable archive — useful for CI scripts that should error in seconds instead of hanging on a blocked fetch.
83
+ `--offline` skips the network entirely: it succeeds immediately if `<dir>/models/<source-id>/` already has files in it, and otherwise fails fast with a message telling you to pull once on a connected machine or point `--mirror`/`JGENGINE_ASSETS_MIRROR` at a reachable archive G�� useful for CI scripts that should error in seconds instead of hanging on a blocked fetch.
67
84
 
68
- **Network-restricted environments** — a `403` on `CONNECT` from an outbound proxy means the environment's network policy blocks that provider host (common in sandboxed cloud sessions); it is a policy decision, not a transient failure, so retrying never helps. Either allowlist the host in the environment settings, or pull once on a machine that can reach the providers, then:
85
+ **Network-restricted environments** G�� a `403` on `CONNECT` from an outbound proxy means the environment's network policy blocks that provider host (common in sandboxed cloud sessions); it is a policy decision, not a transient failure, so retrying never helps. Either allowlist the host in the environment settings, or pull once on a machine that can reach the providers, then:
69
86
 
70
87
  - commit (or otherwise host) the resulting `public/models/<source-id>/` directory so restricted environments read it straight off disk and use `assets pull --offline` as a fast, honest no-op check, or
71
88
  - host your own mirror of the zip archives at `<baseUrl>/<provider>/<source-id>.zip` and set `JGENGINE_ASSETS_MIRROR=<baseUrl>` (or pass `--mirror <baseUrl>`) so `pull` fetches from it instead of the original provider.
72
89
 
73
90
  ## Adding assets
74
91
 
75
- **A whole new pack** — add one entry to the matching `src/sources/*.ts` (this is the layer contributors PR). No filenames are hand-typed; `reindex` reads the real `.glb` names out of the extracted pack, so entries can't silently 404.
92
+ **A whole new pack** G�� add one entry to the matching `src/sources/*.ts` (this is the layer contributors PR). No filenames are hand-typed; `reindex` reads the real `.glb` names out of the extracted pack, so entries can't silently 404.
76
93
 
77
94
  ```ts
78
- // src/sources/kenney.ts → KENNEY_PACKS
79
- { id: "kenney-food", slug: "food-kit", title: "Food Kit", categories: ["food", "prop"] },
95
+ // src/sources/quaternius.ts G�� QUATERNIUS_PACKS
96
+ { id: "quaternius-fantasy-props", slug: "fantasypropsmegakit", title: "Fantasy Props MegaKit", categories: ["fantasy", "prop"] },
80
97
  ```
81
98
 
82
99
  ```bash
83
- bun src/cli/pull.ts pull kenney-food --dir ../../apps/dev/public # fetch + extract GLBs
84
- bun src/cli/pull.ts reindex ../../apps/dev/public/models # regenerate generated/*.json + barrel
85
- bun src/cli/pull.ts verify # license + alias gate
100
+ bun src/cli/pull.ts pull quaternius-fantasy-props --dir ../../apps/dev/public # fetch + extract GLBs
101
+ bun src/cli/pull.ts reindex ../../apps/dev/public/models # regenerate generated/*.json + barrel
102
+ bun src/cli/pull.ts verify # license + alias gate
86
103
  ```
87
104
 
88
- A **new provider** is just a new `src/sources/<provider>.ts` added to the `sources` array in `src/sources/index.ts`. Pinned providers use `download: { url, sha256? }`; providers that rotate URLs (Kenney) use `download: { scrape: <page> }`. Any entry can also carry an optional top-level `mirror: <archiveUrl>` — a direct URL `pull` falls back to if both a `--mirror`/`JGENGINE_ASSETS_MIRROR` override and the primary path fail (see "Mirror fallback and offline pulls" above).
105
+ A **new provider** is just a new `src/sources/<provider>.ts` added to the `sources` array in `src/sources/index.ts`. Pinned providers use `download: { url, sha256? }`; providers whose pages JS-gate the download and can't be pinned use `download: { scrape: <page> }`. Any entry can also carry an optional top-level `mirror: <archiveUrl>` G�� a direct URL `pull` falls back to if both a `--mirror`/`JGENGINE_ASSETS_MIRROR` override and the primary path fail (see "Mirror fallback and offline pulls" above). Kenney.nl is not an eligible provider for this repo G�� see CLAUDE.md "Never Kenney".
106
+
107
+ **A whole new sprite/icon pack** G�� same shape as a model pack, just `kind: "sprite"` and `reindex-sprites` instead of `reindex`; files are matched by `.svg`/`.png` extension, deduped by basename regardless of archive nesting, so no filenames are hand-typed here either. `src/sources/gameicons.ts` is the current example (`gameicons-icons`); a new sprite provider follows the same shape in its own `src/sources/<provider>.ts`.
108
+
109
+ ```bash
110
+ bun src/cli/pull.ts pull gameicons-icons --dir ../../apps/dev/public # fetch + extract SVG/PNG
111
+ bun src/cli/pull.ts reindex-sprites ../../apps/dev/public/sprites # regenerate generated-sprites/*.json + barrel
112
+ ```
89
113
 
90
- **A single one-off** — no code edit, zero bytes stored (URL) or copied into `local/` (path):
114
+ **A single one-off** G�� no code edit, zero bytes stored (URL) or copied into `local/` (path):
91
115
 
92
116
  ```bash
93
- assets add "https://poly.pizza/…/model.glb" --category prop --license CC0-1.0 --author "Some Author"
117
+ assets add "https://poly.pizza/GǪ/model.glb" --category prop --license CC0-1.0 --author "Some Author"
94
118
  ```
95
119
 
96
120
  ## Importing in code
97
121
 
98
- Build the catalog once, then address ids (or aliases) — resolve returns `{ url }`:
122
+ Build the catalog once, then address ids (or aliases) G�� resolve returns `{ url }`:
99
123
 
100
124
  ```ts
101
125
  import { buildCatalog } from "@jgengine/assets";
102
126
 
103
127
  const catalog = buildCatalog({ basePath: "/models" });
104
128
 
105
- catalog.resolve("kenney-nature/tree_pineDefaultA"); // { url: "/models/kenney-nature/tree_pineDefaultA.glb" }
106
- catalog.resolve("nature/tree_pine"); // alias → same url
129
+ catalog.resolve("quaternius-stylized-nature/Pine_1"); // { url: "/models/quaternius-stylized-nature/Pine_1.glb" }
130
+ catalog.resolve("nature/tree_pine"); // alias G�� same url
107
131
 
108
132
  // wire ids straight into a game's model seams:
109
133
  export const game: PlayableGame = {
110
- // …
134
+ // GǪ
111
135
  objectModels: {
112
- "kenney-nature/tree_pineDefaultA": { url: catalog.resolve("kenney-nature/tree_pineDefaultA")!.url, scale: 1.4 },
136
+ "quaternius-stylized-nature/Pine_1": { url: catalog.resolve("quaternius-stylized-nature/Pine_1")!.url, scale: 1.4 },
113
137
  },
114
138
  entityModels: {
115
- hero: { url: catalog.resolve("kenney-space/astronautA")!.url, scale: 1.1 },
139
+ hero: { url: catalog.resolve("kaykit-adventurers/Barbarian")!.url, scale: 1.1 },
116
140
  },
117
141
  };
118
142
  ```
119
143
 
120
- `buildCatalog({ sources: ["kenney-nature"] })` restricts to chosen packs; `includeAliases` / `includeSingles` default true. Discover ids with `assets add <query>` (or `assets search <term>` / `assets list --category <c>`) instead of memorizing them.
144
+ `buildCatalog({ sources: ["quaternius-stylized-nature"] })` restricts to chosen packs; `includeAliases` / `includeSingles` default true. Discover ids with `assets add <query>` (or `assets search <term>` / `assets list --category <c>`) instead of memorizing them.
145
+
146
+ Materials resolve the same way G�� ids (or `material/GǪ` aliases) to normalized PBR map URLs:
147
+
148
+ ```ts
149
+ import { buildMaterialCatalog } from "@jgengine/assets";
150
+
151
+ const materials = buildMaterialCatalog({ basePath: "/materials" });
152
+
153
+ materials.resolve("ambientcg-grass001")!.maps.color; // "/materials/ambientcg-grass001/color.jpg"
154
+ materials.resolve("material/grass")!.maps.normal; // alias G�� "/materials/ambientcg-grass001/normal.jpg"
155
+ ```
156
+
157
+ Sprite/icon packs resolve the same way once pulled + reindexed G�� individual files, not whole packs:
158
+
159
+ ```ts
160
+ import { buildSpriteCatalog } from "@jgengine/assets";
161
+
162
+ const sprites = buildSpriteCatalog({ basePath: "/sprites" });
163
+
164
+ sprites.resolve("gameicons-icons/sword")!.url; // "/sprites/gameicons-icons/sword.svg"
165
+ ```
121
166
 
122
167
  ### Serving the bytes
123
168
 
124
- A resolved id only yields a **URL** — the GLB must be somewhere your app serves. For a game, `pull` the packs into your app's `public/`, and line `basePath` up with it:
169
+ A resolved id only yields a **URL** G�� the GLB must be somewhere your app serves. For a game, `pull` the packs into your app's `public/`, and line `basePath` up with it:
125
170
 
126
171
  ```bash
127
- bun src/cli/pull.ts pull kenney-nature --dir ../../apps/dev/public # → apps/dev/public/models/kenney-nature/*.glb
172
+ bun src/cli/pull.ts pull quaternius-stylized-nature --dir ../../apps/dev/public # G�� apps/dev/public/models/quaternius-stylized-nature/*.glb
128
173
  ```
129
174
 
130
- `buildCatalog({ basePath: "/models" })` then resolves to `/models/kenney-nature/…`, which the dev server serves from `public/models/` (gitignored — the bytes are the consumer's, fetched once from the provider's CDN).
175
+ `buildCatalog({ basePath: "/models" })` then resolves to `/models/quaternius-stylized-nature/GǪ`, which the dev server serves from `public/models/` (gitignored G�� the bytes are the consumer's, fetched once from the provider's CDN).
176
+
177
+ Sprite/icon packs pull the same way, into `public/sprites/`:
178
+
179
+ ```bash
180
+ bun src/cli/pull.ts pull gameicons-icons --dir ../../apps/dev/public # G�� apps/dev/public/sprites/gameicons-icons/*.svg
181
+ bun src/cli/pull.ts reindex-sprites ../../apps/dev/public/sprites # G�� src/generated-sprites/gameicons-icons.json
182
+ ```
131
183
 
132
184
  ## Notes
133
185
 
134
- - Kenney rotates download URLs, so its sources `scrape` the asset page at pull time.
135
- - Quaternius / KayKit pages gate downloads behind JS; automated `pull` falls back to a clear error when no archive link is found — download those manually into the staging dir, then `reindex`.
186
+ - Quaternius / KayKit pages gate downloads behind JS; sources without a pinned OpenGameArt/GitHub zip `scrape` the pack page as a fallback, and automated `pull` falls back to a clear error when no archive link is found G�� download those manually into the staging dir, then `reindex`.
136
187
  - `pull`'s mirror fallback and `--offline` guard exist for network-restricted environments (CI, sandboxes without provider access); see "Mirror fallback and offline pulls" above.
188
+ - `gameicons-icons` is **CC BY 3.0**, not CC0 G�� a game using it needs one credit line (source, "game-icons.net contributors", license) per the engine's asset-credit rule; the repo's own per-author `license.txt` files carry the individual credits behind this pack's single collective `author` field.
189
+ - `src/generated-sprites/` starts empty for a brand-new sprite source G�� same as any brand-new model pack, the committed per-file index only exists after someone runs `pull` + `reindex-sprites` with real network access; until then the source is fully declared (and the weekly mirror job will fetch it) but has no individually-addressable ids yet.
package/dist/aliases.d.ts CHANGED
@@ -1,2 +1,7 @@
1
1
  import type { AssetAlias } from "./manifest.js";
2
+ /**
3
+ * Semantic keys → live catalog ids. Prefer these in games so re-homes only
4
+ * touch this table. Never point at kenney-*. Curated starter theme ids
5
+ * (`asset:person_casual`, `nature_tree`, …) come from {@link starterAliases}.
6
+ */
2
7
  export declare const aliases: readonly AssetAlias[];
package/dist/aliases.js CHANGED
@@ -1,11 +1,30 @@
1
+ import { starterAliases } from "./packs/starter.js";
2
+ /**
3
+ * Semantic keys → live catalog ids. Prefer these in games so re-homes only
4
+ * touch this table. Never point at kenney-*. Curated starter theme ids
5
+ * (`asset:person_casual`, `nature_tree`, …) come from {@link starterAliases}.
6
+ */
1
7
  export const aliases = [
2
- { key: "nature/tree_pine", target: "kenney-nature/tree_pineDefaultA" },
3
- { key: "nature/tree", target: "kenney-nature/tree_blocks" },
4
- { key: "nature/rock", target: "kenney-nature/cliff_block_rock" },
5
- { key: "nature/mushroom", target: "kenney-nature/mushroom_red" },
6
- { key: "nature/flower", target: "kenney-nature/flower_purpleA" },
7
- { key: "weapon/blaster", target: "kenney-blaster/blaster-a" },
8
- { key: "space/astronaut", target: "kenney-space/astronautA" },
9
- { key: "space/ship", target: "kenney-space/craft_racer" },
10
- { key: "space/rocket", target: "kenney-space/rocket_baseA" },
8
+ { key: "nature/tree_pine", target: "quaternius-stylized-nature/Pine_1" },
9
+ { key: "nature/tree_common", target: "quaternius-stylized-nature/CommonTree_1" },
10
+ { key: "nature/tree_dead", target: "quaternius-stylized-nature/DeadTree_1" },
11
+ { key: "nature/bush", target: "quaternius-stylized-nature/Bush_Common" },
12
+ { key: "nature/rock", target: "quaternius-stylized-nature/Rock_Medium_1" },
13
+ { key: "character/rogue", target: "kaykit-adventurers/Rogue" },
14
+ { key: "character/knight", target: "kaykit-adventurers/Knight" },
15
+ { key: "character/mage", target: "kaykit-adventurers/Mage" },
16
+ { key: "character/barbarian", target: "kaykit-adventurers/Barbarian" },
17
+ { key: "scifi/alien", target: "quaternius-modular-scifi/Alien_Cyclop" },
18
+ { key: "scifi/chest", target: "quaternius-modular-scifi/Prop_Chest" },
19
+ { key: "scifi/crate", target: "quaternius-modular-scifi/Prop_Crate3" },
20
+ { key: "scifi/computer", target: "quaternius-modular-scifi/Prop_Computer" },
21
+ { key: "dungeon/chest", target: "kaykit-dungeon/chest" },
22
+ { key: "furniture/bed", target: "kaykit-furniture/bed_double_A" },
23
+ { key: "furniture/couch", target: "kaykit-furniture/couch" },
24
+ { key: "furniture/table", target: "kaykit-furniture/table_medium" },
25
+ { key: "city/car_sedan", target: "kaykit-city-builder/car_sedan" },
26
+ { key: "city/car_police", target: "kaykit-city-builder/car_police" },
27
+ { key: "city/building_a", target: "kaykit-city-builder/building_A" },
28
+ { key: "city/streetlight", target: "kaykit-city-builder/streetlight" },
29
+ ...starterAliases(),
11
30
  ];
@@ -2,6 +2,7 @@ import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
2
2
  import { aliases } from "../aliases.js";
3
3
  import { generatedIndex } from "../generated/index.js";
4
4
  import { singles } from "../singles.js";
5
+ import { sources as declaredSources } from "../sources/index.js";
5
6
  export function entryUrl(basePath, entry) {
6
7
  return `${basePath.replace(/\/+$/, "")}/${entry.source}/${entry.file}`;
7
8
  }
@@ -10,6 +11,14 @@ export function buildCatalog(options = {}) {
10
11
  const includeAliases = options.includeAliases ?? true;
11
12
  const includeSingles = options.includeSingles ?? true;
12
13
  const sourceFilter = options.sources === undefined ? null : new Set(options.sources);
14
+ if (sourceFilter !== null) {
15
+ const knownSourceIds = new Set(declaredSources.map((source) => source.id));
16
+ for (const id of sourceFilter) {
17
+ if (!knownSourceIds.has(id)) {
18
+ throw new Error(`buildCatalog: unknown source id "${id}" — not declared in packages/assets/src/sources/*.ts`);
19
+ }
20
+ }
21
+ }
13
22
  const catalog = createAssetCatalog();
14
23
  for (const entry of generatedIndex) {
15
24
  if (sourceFilter !== null && !sourceFilter.has(entry.source))
@@ -0,0 +1,10 @@
1
+ import { type AssetCatalog } from "@jgengine/core/scene/assetCatalog";
2
+ /** Options for `buildSpriteCatalog`. */
3
+ export interface BuildSpriteCatalogOptions {
4
+ /** URL prefix where pulled sprite/icon files live (consumer's `public/sprites`). */
5
+ basePath?: string;
6
+ /** Restrict to these source ids; omit for all. */
7
+ sources?: readonly string[];
8
+ }
9
+ /** Resolves individual pulled sprite/icon ids (e.g. `gameicons-icons/sword`) to `{ url }`. */
10
+ export declare function buildSpriteCatalog(options?: BuildSpriteCatalogOptions): AssetCatalog;
@@ -0,0 +1,15 @@
1
+ import { createAssetCatalog } from "@jgengine/core/scene/assetCatalog";
2
+ import { generatedSpriteIndex } from "../generated-sprites/index.js";
3
+ import { entryUrl } from "./build.js";
4
+ /** Resolves individual pulled sprite/icon ids (e.g. `gameicons-icons/sword`) to `{ url }`. */
5
+ export function buildSpriteCatalog(options = {}) {
6
+ const basePath = options.basePath ?? "/sprites";
7
+ const sourceFilter = options.sources === undefined ? null : new Set(options.sources);
8
+ const catalog = createAssetCatalog();
9
+ for (const entry of generatedSpriteIndex) {
10
+ if (sourceFilter !== null && !sourceFilter.has(entry.source))
11
+ continue;
12
+ catalog.register(entry.id, { url: entryUrl(basePath, entry) });
13
+ }
14
+ return catalog;
15
+ }
@@ -1,2 +1,7 @@
1
1
  import { type BuildCatalogOptions } from "./build.js";
2
+ /**
3
+ * Catalog restricted to the curated starter packs (people/props/nature/urban).
4
+ * Prefer this for scaffolds and probes so `asset:person_casual` etc. resolve
5
+ * without pulling the whole library. Full index: {@link buildCatalog}.
6
+ */
2
7
  export declare function createStarterCatalog(options?: BuildCatalogOptions): import("@jgengine/core/scene/assetCatalog").AssetCatalog<import("@jgengine/core/scene/assetCatalog").ModelAssetRef>;
@@ -1,4 +1,13 @@
1
+ import { STARTER_SOURCE_PACKS } from "../packs/starter.js";
1
2
  import { buildCatalog } from "./build.js";
3
+ /**
4
+ * Catalog restricted to the curated starter packs (people/props/nature/urban).
5
+ * Prefer this for scaffolds and probes so `asset:person_casual` etc. resolve
6
+ * without pulling the whole library. Full index: {@link buildCatalog}.
7
+ */
2
8
  export function createStarterCatalog(options = {}) {
3
- return buildCatalog(options);
9
+ return buildCatalog({
10
+ ...options,
11
+ sources: options.sources ?? STARTER_SOURCE_PACKS,
12
+ });
4
13
  }
@@ -1,4 +1,12 @@
1
- /** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
1
+ /** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import.
2
+ * @internal
3
+ */
2
4
  export declare function resolveGeneratedDir(cliDir: string): string;
5
+ /** Sprite-pack counterpart of `resolveGeneratedDir` — a separate generated tree, same sibling layout.
6
+ * @internal
7
+ */
8
+ export declare function resolveGeneratedSpritesDir(cliDir: string): string;
9
+ /** @internal */
3
10
  export declare function resolvePackageTreeRoot(cliDir: string): string;
11
+ /** @internal */
4
12
  export declare function resolvePackageRoot(cliDir: string): string;
package/dist/cli/paths.js CHANGED
@@ -1,11 +1,21 @@
1
1
  import { join, resolve } from "node:path";
2
- /** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
2
+ /** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import.
3
+ * @internal
4
+ */
3
5
  export function resolveGeneratedDir(cliDir) {
4
6
  return join(resolve(cliDir, ".."), "generated");
5
7
  }
8
+ /** Sprite-pack counterpart of `resolveGeneratedDir` — a separate generated tree, same sibling layout.
9
+ * @internal
10
+ */
11
+ export function resolveGeneratedSpritesDir(cliDir) {
12
+ return join(resolve(cliDir, ".."), "generated-sprites");
13
+ }
14
+ /** @internal */
6
15
  export function resolvePackageTreeRoot(cliDir) {
7
16
  return resolve(cliDir, "..");
8
17
  }
18
+ /** @internal */
9
19
  export function resolvePackageRoot(cliDir) {
10
20
  return resolve(cliDir, "..", "..");
11
21
  }
@@ -1,8 +1,20 @@
1
1
  #!/usr/bin/env node
2
2
  /** Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import. */
3
3
  export declare const generatedDir: string;
4
+ /** Sprite-pack counterpart of `generatedDir`. */
5
+ export declare const generatedSpritesDir: string;
4
6
  export { resolveGeneratedDir } from "./paths.js";
7
+ /** @internal */
5
8
  export declare function flag(argv: string[], name: string): string | undefined;
9
+ /**
10
+ * The runtime's own fetch, with a curl fallback for hosts it cannot reach —
11
+ * some proxied sandboxes tear down bun's TLS stream to GitHub's release-asset
12
+ * host while curl (which honors SSL_CERT_FILE) gets through fine.
13
+ */
14
+ export declare const cliFetch: typeof fetch;
15
+ /** @internal */
6
16
  export declare function describeNetworkFailure(error: unknown): string;
17
+ /** @internal */
7
18
  export declare function isPopulated(dir: string): boolean;
19
+ /** @internal */
8
20
  export declare function cmdPull(argv: string[]): Promise<void>;