@jgengine/assets 0.11.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 +26 -0
- package/README.md +1 -1
- package/dist/catalogs/build.d.ts +27 -0
- package/dist/catalogs/build.js +19 -2
- package/dist/cli/pull.js +7 -11
- 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 +26 -10
- package/dist/download.js +140 -43
- package/dist/generated/kaykit-adventurers.json +64 -32
- package/dist/generated/kaykit-city-builder.json +82 -41
- package/dist/generated/kaykit-dungeon.json +940 -203
- package/dist/generated/kaykit-furniture.json +106 -53
- package/dist/generated/quaternius-stylized-nature.json +136 -68
- package/dist/indexGen.d.ts +2 -2
- package/dist/indexGen.js +27 -5
- package/dist/manifest.d.ts +4 -0
- package/dist/registry-catalog.json +0 -5
- package/package.json +2 -2
- package/skills/jgengine/SKILL.md +7 -4
- package/skills/jgengine/api.md +172 -79
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-assets/SKILL.md +4 -0
- package/skills/jgengine-assets/api.md +13 -4
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -4,18 +4,100 @@
|
|
|
4
4
|
|
|
5
5
|
Reach for these before hand-rolling. Each row is *the thing you need* → *the primitive that already does it*.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## authored-spawn — read spawn points and markers from the editor document
|
|
8
8
|
|
|
9
|
-
- `
|
|
9
|
+
- `authoredSpawnPosition` (function) · `import { authoredSpawnPosition } from "@jgengine/shell/gameKit"`
|
|
10
|
+
|
|
11
|
+
## authored-triggers — schema'd on/action vocabulary on volumes and markers with runtime dispatch
|
|
12
|
+
|
|
13
|
+
- `collectAuthoredTriggers` (function) · `import { collectAuthoredTriggers } from "@jgengine/shell/gameKit"`
|
|
14
|
+
|
|
15
|
+
## collider-autofit — hitboxes and physical bodies fit each kind's rendered model bounds automatically — no hand-tuned sizes
|
|
16
|
+
|
|
17
|
+
- `GameContextModels` (interface) · `import { GameContextModels } from "@jgengine/core/runtime/gameContext"`
|
|
18
|
+
|
|
19
|
+
## define-game — single public game-authoring path — compose systems, world, presentation, and authored scene in one definition
|
|
20
|
+
|
|
21
|
+
- `defineGame` (function) · `import { defineGame } from "@jgengine/shell/gameKit"`
|
|
22
|
+
|
|
23
|
+
## define-system — composable game behavior — pure data + hooks the engine schedules; pass via `defineGame({ systems })`
|
|
24
|
+
|
|
25
|
+
- `defineSystem` (function) · `import { defineSystem } from "@jgengine/shell/gameKit"`
|
|
26
|
+
|
|
27
|
+
## editor-catalogs — Export typed gameplay catalogs for in-editor tuning.
|
|
28
|
+
|
|
29
|
+
- `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/shell/gameKit"`
|
|
30
|
+
|
|
31
|
+
## game-system — declare a composable capability with its own schedule and lifecycle
|
|
32
|
+
|
|
33
|
+
- `SystemDefinition` (interface) · `import { SystemDefinition } from "@jgengine/shell/gameKit"`
|
|
10
34
|
|
|
11
35
|
## headless-runner — play a real game loop with no renderer — tick, feed input, read the world snapshot
|
|
12
36
|
|
|
13
37
|
- `HeadlessRunner` (interface) · `import { HeadlessRunner } from "@jgengine/core/runtime/headlessRunner"`
|
|
14
38
|
|
|
39
|
+
## host-join-code-gate — membership-or-code gate for private hosted servers
|
|
40
|
+
|
|
41
|
+
- `canJoinPrivateServer` (function) · `import { canJoinPrivateServer } from "@jgengine/core/runtime/hostPolicy"`
|
|
42
|
+
|
|
43
|
+
## host-listing-filter — exclude private sessions from public browse results
|
|
44
|
+
|
|
45
|
+
- `isListablePublicly` (function) · `import { isListablePublicly } from "@jgengine/core/runtime/hostPolicy"`
|
|
46
|
+
|
|
47
|
+
## hud-canvas — the HUD root surface — design-resolution scaling plus live panel-placement editing (F2+C)
|
|
48
|
+
|
|
49
|
+
- `HudCanvas` (function) · `import { HudCanvas } from "@jgengine/shell/gameKit"`
|
|
50
|
+
|
|
51
|
+
## hud-clock — in-game time-of-day readout bound to the sim clock
|
|
52
|
+
|
|
53
|
+
- `Clock` (function) · `import { Clock } from "@jgengine/shell/gameKit"`
|
|
54
|
+
|
|
55
|
+
## hud-coins — currency counter widget bound to a currency id
|
|
56
|
+
|
|
57
|
+
- `Coins` (function) · `import { Coins } from "@jgengine/shell/gameKit"`
|
|
58
|
+
|
|
59
|
+
## hud-hotbar — optional inventory hotbar building block — games own slot chrome
|
|
60
|
+
|
|
61
|
+
- `Hotbar` (function) · `import { Hotbar } from "@jgengine/shell/gameKit"`
|
|
62
|
+
|
|
63
|
+
## hud-layout — persistent, live-editable HUD panel layout state for `HudCanvas`
|
|
64
|
+
|
|
65
|
+
- `useHudLayout` (function) · `import { useHudLayout } from "@jgengine/shell/gameKit"`
|
|
66
|
+
|
|
67
|
+
## hud-panel — anchored HUD slot — drop any widget in a named, player-movable panel
|
|
68
|
+
|
|
69
|
+
- `HudPanel` (function) · `import { HudPanel } from "@jgengine/shell/gameKit"`
|
|
70
|
+
|
|
71
|
+
## hud-stat-bar — optional stat-bound bar building block (health, mana, stamina) — prefer game-owned chrome
|
|
72
|
+
|
|
73
|
+
- `StatBar` (function) · `import { StatBar } from "@jgengine/shell/gameKit"`
|
|
74
|
+
|
|
15
75
|
## loot-table — validate a loot table definition for use with the registry
|
|
16
76
|
|
|
17
77
|
- `lootTable` (function) · `import { lootTable } from "@jgengine/core/authoring"`
|
|
18
78
|
|
|
79
|
+
## mount-game — mount a defined game in the browser — `<GameHost playable={game} editor={() => import("@jgengine/editor")} />`
|
|
80
|
+
|
|
81
|
+
- `GameHost` (function) · `import { GameHost } from "@jgengine/shell/gameKit"`
|
|
82
|
+
|
|
83
|
+
## multiplayer-offline — explicit solo adapter — omit `multiplayer` instead; offline is the shell default
|
|
84
|
+
|
|
85
|
+
- `offline` (function) · `import { offline } from "@jgengine/shell/gameKit"`
|
|
86
|
+
|
|
87
|
+
## multiplayer-p2p — serverless co-op over WebRTC — `multiplayer: p2p({ room })`
|
|
88
|
+
|
|
89
|
+
- `p2p` (function) · `import { p2p } from "@jgengine/core/runtime/adapter"`
|
|
90
|
+
|
|
91
|
+
## multiplayer-ws — server-hosted shared world over WebSocket — `multiplayer: ws({ authority: "server" })`
|
|
92
|
+
|
|
93
|
+
- `ws` (function) · `import { ws } from "@jgengine/core/runtime/adapter"`
|
|
94
|
+
|
|
95
|
+
## render-fallback-diagnostics — Enable/disable the dev-only placeholder-fallback probe.
|
|
96
|
+
|
|
97
|
+
- `armFallbackSeams` (function) · `import { armFallbackSeams } from "@jgengine/core/devtools/fallbackSeams"`
|
|
98
|
+
- `fallbackSeamsSnapshot` (function) · `import { fallbackSeamsSnapshot } from "@jgengine/core/devtools/fallbackSeams"`
|
|
99
|
+
- `reportFallbackSeam` (function) · `import { reportFallbackSeam } from "@jgengine/core/devtools/fallbackSeams"`
|
|
100
|
+
|
|
19
101
|
## runtime-save — save/load the whole game world through a pluggable backend, autosave or save points
|
|
20
102
|
|
|
21
103
|
- `createRuntimeSave` (function) · `import { createRuntimeSave } from "@jgengine/core/runtime/runtimeSave"`
|
|
@@ -27,3 +109,23 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
27
109
|
- `collectOwnershipDiagnostics` (function) · `import { collectOwnershipDiagnostics } from "@jgengine/core/authoring"`
|
|
28
110
|
- `isSceneOwnershipManifest` (function) · `import { isSceneOwnershipManifest } from "@jgengine/core/authoring"`
|
|
29
111
|
- `ownershipKey` (function) · `import { ownershipKey } from "@jgengine/core/authoring"`
|
|
112
|
+
|
|
113
|
+
## seeded-random — injected deterministic randomness for game logic — never `Math.random` in a simulation
|
|
114
|
+
|
|
115
|
+
- `seededRng` (function) · `import { seededRng } from "@jgengine/shell/gameKit"`
|
|
116
|
+
|
|
117
|
+
## typed-keyed-store — a cast-free typed handle onto a per-owner keyed family of reactive game-store slots, replayable and host-authoritative
|
|
118
|
+
|
|
119
|
+
- `defineKeyedStore` (function) · `import { defineKeyedStore } from "@jgengine/shell/gameKit"`
|
|
120
|
+
|
|
121
|
+
## typed-store — a cast-free typed handle onto one reactive game-store slot, replayable and host-authoritative
|
|
122
|
+
|
|
123
|
+
- `defineStore` (function) · `import { defineStore } from "@jgengine/shell/gameKit"`
|
|
124
|
+
|
|
125
|
+
## world-convergence — derive environment coordinate content from the scene document
|
|
126
|
+
|
|
127
|
+
- `environmentContentFromDocument` (function) · `import { environmentContentFromDocument } from "@jgengine/shell/gameKit"`
|
|
128
|
+
|
|
129
|
+
## world-environment — compose editor/preset-derived outdoor dressing — terrain, weather, water, structures — into a legacy environment feature
|
|
130
|
+
|
|
131
|
+
- `environment` (function) · `import { environment } from "@jgengine/shell/gameKit"`
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Recipe — minimal game (scaffold → authored scene → win)
|
|
2
|
+
|
|
3
|
+
**What this wires:** the default end-to-end path from nothing to a complete, genre-free
|
|
4
|
+
walk/interact/win game: a scaffold, one `defineGame` config off the happy-path kit, an
|
|
5
|
+
authored scene edited in the browser, one system, one store slot, an empty-canvas HUD,
|
|
6
|
+
and a win condition. Every new game starts here; add combat, multiplayer, or deeper
|
|
7
|
+
world features only after this loop plays.
|
|
8
|
+
|
|
9
|
+
## 1. Scaffold
|
|
10
|
+
|
|
11
|
+
`npx jgengine create <name>` (inside the engine monorepo: `bun run new:game "<id-or-title>"`).
|
|
12
|
+
The file set is thin and already runs:
|
|
13
|
+
|
|
14
|
+
- `src/main.tsx` — mounts `GameHost` with `editor={() => import("@jgengine/editor")}`; the
|
|
15
|
+
host owns the F2+E / `?mode=editor` editor summon and the dev save endpoint.
|
|
16
|
+
- `src/game.config.ts` — the whole game definition (below).
|
|
17
|
+
- `src/editor.scene.json` + `src/editorLayers.ts` — the authored scene document, normalized
|
|
18
|
+
with `normalizeEditorLayers`. All placement lives here, never as coordinates in code.
|
|
19
|
+
- `src/loop.ts` and `src/game/ui/GameUI.tsx` — systems and HUD.
|
|
20
|
+
|
|
21
|
+
## 2. Define the game — one import surface
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
// game.config.ts — everything below comes from the kit; deeper paths are for outgrowing it.
|
|
25
|
+
import { defineGame } from "@jgengine/shell/gameKit";
|
|
26
|
+
|
|
27
|
+
import { editorLayers } from "./editorLayers";
|
|
28
|
+
import { GameUI } from "./game/ui/GameUI";
|
|
29
|
+
import { onNewPlayer, systems } from "./loop";
|
|
30
|
+
|
|
31
|
+
export const game = defineGame({
|
|
32
|
+
name: "my-game",
|
|
33
|
+
// Binding movement actions makes the shell drive the walk controller — the game walks day one.
|
|
34
|
+
input: { moveForward: ["KeyW"], moveBack: ["KeyS"], moveLeft: ["KeyA"], moveRight: ["KeyD"], interact: ["KeyE"] },
|
|
35
|
+
systems,
|
|
36
|
+
loop: { onNewPlayer },
|
|
37
|
+
GameUI,
|
|
38
|
+
editorLayers, // the authored scene auto-mounts; the player spawns at its player_spawn marker
|
|
39
|
+
});
|
|
40
|
+
// Solo game: omit `multiplayer` entirely — offline is the shell default.
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**World is optional and thin.** Omitting `world` is valid — a pure UI/rules game is not a
|
|
44
|
+
spatial place. When the game *is* a place, declare only substrate + laws with `world()` from
|
|
45
|
+
`@jgengine/core/world/place` (e.g. `{ id, ground: { mode: "flat", size: { x: Infinity, z: Infinity } }, physics: { gravity: -24 } }`,
|
|
46
|
+
or `mode: "board"` for 2D). Never seed, sky, or vegetation in `world` — the engine renders its
|
|
47
|
+
default sky until you author one in the editor, and foliage is editor scatter over the ground.
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
// loop.ts — one store slot for run state, one system for the rules tick.
|
|
52
|
+
import { defineStore, defineSystem, authoredSpawnPosition, type GameContext } from "@jgengine/shell/gameKit";
|
|
53
|
+
|
|
54
|
+
import { editorLayers } from "./editorLayers";
|
|
55
|
+
|
|
56
|
+
export const run = defineStore("run", { collected: 0, won: false });
|
|
57
|
+
|
|
58
|
+
export const systems = [
|
|
59
|
+
defineSystem({
|
|
60
|
+
id: "rules",
|
|
61
|
+
tick: { type: "fixed" },
|
|
62
|
+
update(ctx: GameContext) {
|
|
63
|
+
// System-check win alternative: read/update `run` here (e.g. win at collected >= 3).
|
|
64
|
+
if (run.read(ctx).collected >= 3) run.update(ctx, (s) => ({ ...s, won: true }));
|
|
65
|
+
},
|
|
66
|
+
}),
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
export function onNewPlayer(ctx: GameContext): void {
|
|
70
|
+
ctx.scene.entity.spawn("player", { id: ctx.player.userId, position: authoredSpawnPosition(editorLayers) ?? [0, 0, 0] });
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
`GameUI` starts as an empty `HudCanvas` (`useHudLayout` + `HudCanvas` from the kit). That is
|
|
75
|
+
intentional: **every game owns its UI**. Write a short UI art direction, then build custom
|
|
76
|
+
panels for this pitch — do not ship stock drop-in widgets (`StatBar`, `Hotbar`, `Coins`, glass
|
|
77
|
+
frames) as the product face. Reach for engine pieces only as headless data, layout, or
|
|
78
|
+
interaction models under game-owned chrome (see `jgengine-ui`).
|
|
79
|
+
|
|
80
|
+
## 3. Author, play, win
|
|
81
|
+
|
|
82
|
+
1. `bun dev`, then F2+E opens the editor on `editor.scene.json` — move the spawn, place
|
|
83
|
+
props and trigger markers, Ctrl+S saves back to the document.
|
|
84
|
+
2. The starter scene ships a goal marker with `{ on: "enter", action: "win" }` — walking
|
|
85
|
+
into it wins with zero game code. Author more rules the same way (triggers in the
|
|
86
|
+
editor), or read volumes yourself via the kit's `collectAuthoredTriggers(editorLayers)`.
|
|
87
|
+
3. Prove it: `bun test` asserts the document (spawn, goal), and `jgengine-verify` covers
|
|
88
|
+
screenshot and gameplay evidence before any "it works" claim.
|
|
@@ -24,8 +24,12 @@ Search [capabilities.md](capabilities.md) before adding a resolver or catalog pa
|
|
|
24
24
|
|
|
25
25
|
Every logical asset id has a declared owner: `committed` (a single's shipped URL, resolves on a clean clone), `provisioned` (a pack index entry whose GLB is fetched by `assets pull <source>`), or `dangling` (nothing owns it — a broken contract). Resolve one id with `resolveProvenance(id)`; gate a set of references with `validateAssetReferences(refs)`, which fails on any dangling reference and reports the exact `assets pull <source>` steps a clean checkout still needs. `assets provenance <id>...` exposes the same contract on the CLI and exits non-zero on a dangling id. This resolves references structurally against the catalog — never grep source text as the long-term contract.
|
|
26
26
|
|
|
27
|
+
A game's catalog is `buildCatalog(...)` over the generated pack index, singles, and aliases; `extras` is its durable escape hatch for assets that aren't in a pack. An editor asset import into a **promoted** game project writes into that `extras` array in `src/game/assets.ts` (bytes land in `public/<basePath>/imported/`), registered after packs (last-writer-wins) and before aliases (so an alias can target one — see `jgengine-editor`). At runtime an extra resolves to exactly its `{ url }`; the `label` is human-facing only and never enters the catalog.
|
|
28
|
+
|
|
27
29
|
At load time the shell classifies a model fetch before parsing via `classifyAssetResponse` (`@jgengine/core/scene/assetDiagnostics`), so a missing file, a dev-server HTML fallback, corrupt bytes, or a non-model format surface as an actionable diagnostic naming the URL — not an opaque GLTF parse error.
|
|
28
30
|
|
|
31
|
+
Reindex measures every GLB's `dims`; assets listed in `COLLISION_MESH_ASSET_IDS` (`collisionMeshAssets.ts`) additionally ship a compact quantized triangle mesh (`collisionMesh` on the index entry) that collider auto-fit raycasts instead of the fitted box — opt in only concave models whose box lies (archways, rings, frames), because each entry adds real index bytes and per-ray BVH cost.
|
|
32
|
+
|
|
29
33
|
## Source rules
|
|
30
34
|
|
|
31
35
|
- Never use or reference Kenney.nl assets.
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
- `BuildCatalogOptions` (interface): interface BuildCatalogOptions — ⚠ undocumented
|
|
19
19
|
- `BuildMaterialCatalogOptions` (interface): interface BuildMaterialCatalogOptions — Options for `buildMaterialCatalog`.
|
|
20
20
|
- `BuildSpriteCatalogOptions` (interface): interface BuildSpriteCatalogOptions — Options for `buildSpriteCatalog`.
|
|
21
|
+
- `CollisionMeshData` (interface): interface CollisionMeshData — Compact, serializable triangle collision mesh measured at asset reindex — the renderer-free source of mesh-accurate hitboxes. Positions are welded onto a 16-bit grid spanning `min`..`max` (model space) and stored base64-encoded, so an opted-in catalog asset ships its collision triangles inside the generated index without the runtime ever touching the rendered scene graph.
|
|
21
22
|
- `ExtractedMaterialMap` (interface): interface ExtractedMaterialMap — One normalized map pulled out of a material archive by `extractMaterialMaps`.
|
|
22
23
|
- `ExtractedSpriteFile` (interface): interface ExtractedSpriteFile — One SVG/PNG file pulled out of a sprite/icon-pack archive by `extractSpriteFiles`.
|
|
23
24
|
- `FindOptions` (interface): interface FindOptions — ⚠ undocumented
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
- `VerifyResult` (interface): interface VerifyResult — ⚠ undocumented
|
|
50
51
|
- `aliases` (const): const aliases: readonly AssetAlias[] — Semantic keys → live catalog ids. Prefer these in games so re-homes only touch this table. Never point at kenney-*. Curated starter theme ids (`asset:person_casual`, `nature_tree`, …) come from {@link starterAliases}.
|
|
51
52
|
- `ambientcgSources` (const): const ambientcgSources: readonly AssetSource[] — Every ambientCG material source, generated per family (`ambientcg-grass001` … ).
|
|
52
|
-
- `buildCatalog` (function): function buildCatalog(options: BuildCatalogOptions = {}): AssetCatalog —
|
|
53
|
+
- `buildCatalog` (function): function buildCatalog(options: BuildCatalogOptions = {}): AssetCatalog — Builds a game's asset catalog from the generated CC0 pack index plus singles, extras, and aliases. Registration order is packs → singles → {@link BuildCatalogOptions.extras | extras} → aliases: packs and singles come first, extras override them last-writer-wins, and aliases resolve last so they can target an extra. See {@link BuildCatalogOptions} for filtering and opt-outs.
|
|
53
54
|
- `buildMaterialCatalog` (function): function buildMaterialCatalog(options: BuildMaterialCatalogOptions = {}): MaterialCatalog — A resolvable catalog over every `kind: "material"` source. Ids are source ids (`ambientcg-grass001`) plus the `material/…` aliases; every resolve returns the normalized map URLs under `basePath`, matching what `assets pull` writes into `<dir>/materials/<id>/`.
|
|
54
55
|
- `buildSpriteCatalog` (function): function buildSpriteCatalog(options: BuildSpriteCatalogOptions = {}): AssetCatalog — Resolves individual pulled sprite/icon ids (e.g. `gameicons-icons/sword`) to `{ url }`.
|
|
55
56
|
- `componentInstallUrl` (function): function componentInstallUrl(name: string): string — The `shadcn add` URL for a HUD component, e.g. `https://jgengine.com/r/vital-bar.json`.
|
|
@@ -95,7 +96,8 @@
|
|
|
95
96
|
## @jgengine/assets/catalogs/build
|
|
96
97
|
|
|
97
98
|
- `BuildCatalogOptions` (interface): interface BuildCatalogOptions — ⚠ undocumented
|
|
98
|
-
- `
|
|
99
|
+
- `CatalogExtra` (interface): interface CatalogExtra — A durable, hand-or-tool authored catalog entry that is not part of the generated pack index, singles, or aliases — the shape the editor writes into a promoted game's `src/game/assets.ts` when an asset is imported. `label` is human-facing only and is intentionally not registered into the catalog (it mirrors how a single asset's author/license live only in the source literal), so at runtime an extra resolves to exactly `{ url }`.
|
|
100
|
+
- `buildCatalog` (function): function buildCatalog(options: BuildCatalogOptions = {}): AssetCatalog — Builds a game's asset catalog from the generated CC0 pack index plus singles, extras, and aliases. Registration order is packs → singles → {@link BuildCatalogOptions.extras | extras} → aliases: packs and singles come first, extras override them last-writer-wins, and aliases resolve last so they can target an extra. See {@link BuildCatalogOptions} for filtering and opt-outs.
|
|
99
101
|
- `entryUrl` (function): function entryUrl(basePath: string, entry: IndexEntry): string — ⚠ undocumented
|
|
100
102
|
|
|
101
103
|
## @jgengine/assets/catalogs/buildSprites
|
|
@@ -113,9 +115,14 @@
|
|
|
113
115
|
- `generatedDir` (const): const generatedDir: string — Sibling of `cli/` under `src/` (dev) or `dist/` (published) so reindex writes the tree consumers import.
|
|
114
116
|
- `generatedSpritesDir` (const): const generatedSpritesDir: string — Sprite-pack counterpart of `generatedDir`.
|
|
115
117
|
|
|
118
|
+
## @jgengine/assets/collisionMeshAssets
|
|
119
|
+
|
|
120
|
+
- `COLLISION_MESH_ASSET_IDS` (const): const COLLISION_MESH_ASSET_IDS: ReadonlySet<string> — Catalog assets that ship a collision mesh in the generated index. Opt-in only: each entry adds ~10-30KB of quantized triangles to the index and a BVH raycast per query, so list only concave models whose fitted box lies (archways, rings, frames).
|
|
121
|
+
|
|
116
122
|
## @jgengine/assets/dims
|
|
117
123
|
|
|
118
124
|
- `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space.
|
|
125
|
+
- `readGlbCollisionMesh` (function): function readGlbCollisionMesh(bytes: Uint8Array): CollisionMeshData | null — Extract a model-space collision triangle soup from a GLB and compress it with {@link encodeCollisionMesh}. Walks the default scene, transforming each TRIANGLES primitive's float32 VEC3 positions by the node's world matrix, and returns `null` for a JSON-only GLB or when no triangle survives quantization.
|
|
119
126
|
- `readGlbDims` (function): function readGlbDims(bytes: Uint8Array): ModelDims | null — ⚠ undocumented
|
|
120
127
|
|
|
121
128
|
## @jgengine/assets/download
|
|
@@ -124,8 +131,9 @@
|
|
|
124
131
|
- `DownloadPackOptions` (interface): interface DownloadPackOptions — ⚠ undocumented
|
|
125
132
|
- `DownloadPackResult` (interface): interface DownloadPackResult — ⚠ undocumented
|
|
126
133
|
- `ExtractedGlb` (interface): interface ExtractedGlb — ⚠ undocumented
|
|
134
|
+
- `ExtractedPack` (interface): interface ExtractedPack — What `extractGlbs` pulls out of a pack archive: the models plus the textures they reference.
|
|
135
|
+
- `ExtractedPackImage` (interface): interface ExtractedPackImage — An image a pack's models reference, shipped flat beside the `.glb` files.
|
|
127
136
|
- `ExtractedSpriteFile` (interface): interface ExtractedSpriteFile — One SVG/PNG file pulled out of a sprite/icon-pack archive by `extractSpriteFiles`.
|
|
128
|
-
- `ExtractedTexture` (interface): interface ExtractedTexture — ⚠ undocumented
|
|
129
137
|
- `FetchLike` (type): type FetchLike = typeof fetch — ⚠ undocumented
|
|
130
138
|
- `MAX_ARCHIVE_COMPRESSION_RATIO` (const): const MAX_ARCHIVE_COMPRESSION_RATIO: 100 — Max allowed originalSize/size ratio for a single archive entry — past this it's treated as a zip bomb.
|
|
131
139
|
- `MAX_ARCHIVE_DOWNLOAD_BYTES` (const): const MAX_ARCHIVE_DOWNLOAD_BYTES: number — Max size of a downloaded (still-compressed) archive, in bytes. Provider zips run tens of MB; this leaves headroom without buffering an unbounded response.
|
|
@@ -154,7 +162,7 @@
|
|
|
154
162
|
## @jgengine/assets/indexGen
|
|
155
163
|
|
|
156
164
|
- `ReindexResult` (interface): interface ReindexResult — ⚠ undocumented
|
|
157
|
-
- `entryForFile` (function): function entryForFile(source: AssetSource, file: string, dims?: ModelDims): IndexEntry — ⚠ undocumented
|
|
165
|
+
- `entryForFile` (function): function entryForFile(source: AssetSource, file: string, dims?: ModelDims, collisionMesh?: CollisionMeshData): IndexEntry — ⚠ undocumented
|
|
158
166
|
- `indexSourceDir` (function): function indexSourceDir(source: AssetSource, dir: string): IndexEntry[] — ⚠ undocumented
|
|
159
167
|
- `keyFromFile` (function): function keyFromFile(file: string): string — ⚠ undocumented
|
|
160
168
|
- `reindex` (function): function reindex(modelsDir: string, outDir: string): ReindexResult — ⚠ undocumented
|
|
@@ -167,6 +175,7 @@
|
|
|
167
175
|
- `AssetSource` (interface): interface AssetSource — ⚠ undocumented
|
|
168
176
|
- `AssetSourceKind` (type): type AssetSourceKind = "model" | "material" | "sprite" — What a source's archive contains: GLB models (default), one PBR material's texture maps, or a pack of individual 2D sprite/icon files (SVG/PNG).
|
|
169
177
|
- `AssetSpace` (interface): interface AssetSpace — Catalog-level asset-space metadata: how a model is authored relative to the engine, owned by the catalog entry rather than re-derived per game. Every field is optional so an unmeasured asset stays valid; {@link resolveFacingRotationY}/{@link assetUnitScale}/{@link resolveAnchorOffset} supply the documented defaults.
|
|
178
|
+
- `CollisionMeshData` (interface): interface CollisionMeshData — Compact, serializable triangle collision mesh measured at asset reindex — the renderer-free source of mesh-accurate hitboxes. Positions are welded onto a 16-bit grid spanning `min`..`max` (model space) and stored base64-encoded, so an opted-in catalog asset ships its collision triangles inside the generated index without the runtime ever touching the rendered scene graph.
|
|
170
179
|
- `IndexEntry` (interface): interface IndexEntry — ⚠ undocumented
|
|
171
180
|
- `ModelDims` (interface): interface ModelDims — Measured horizontal footprint, footprint center, and lowest Y of a model in model space.
|
|
172
181
|
- `PinnedDownload` (interface): interface PinnedDownload — ⚠ undocumented
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
# jgengine-api — Cartridge
|
|
2
|
-
|
|
3
|
-
Reference module for the [`jgengine`](./SKILL.md) skill. Load this when building a game as a **cartridge** — one declarative config instead of hand-wired loop/state/UI files. A cartridge game is ~75% smaller than the classic shape and nearly all of it is data; reach for `defineGame` + `defineSystem` ([reference-systems.md](./reference-systems.md)) when the simulation outgrows cartridge archetypes — same single authoring model, no rewrite cliff.
|
|
4
|
-
|
|
5
|
-
## Why
|
|
6
|
-
|
|
7
|
-
Across the classic games, 78–91% of each game's LOC was generic wiring restated per game: a run-state store with subscribe/notify, chase-and-contact enemy loops, auto-fire weapon routines, magnet pickups, draft plumbing, phase-gated HUD trees, and tests for all of it. The cartridge layer owns every one of those; the game ships only its **data** (catalogs, tuning, layout) and any genuinely bespoke rules through typed escape hatches.
|
|
8
|
-
|
|
9
|
-
## Shape
|
|
10
|
-
|
|
11
|
-
`game.config.ts` is the whole game — `cartridge({...})` from `@jgengine/shell/cartridge` compiles the config into a `PlayableGame` (validating it first — a bad reference throws at import with a list of problems). `loop.ts` and `world.ts` disappear; the spec carries the world feature and the loop is engine-owned. `main.tsx` is 5 lines via `mountGame` (`@/components/ui/mount`, registry). Sprites/SVG art stay in `src/game/assets.ts`; the game keeps one test file under `src/game/`.
|
|
12
|
-
|
|
13
|
-
```ts
|
|
14
|
-
import { cartridge, type CartridgeConfig } from "@jgengine/shell/cartridge";
|
|
15
|
-
import { standardCartridgePanels } from "@/components/ui/cartridge-panels";
|
|
16
|
-
|
|
17
|
-
export const config: CartridgeConfig = {
|
|
18
|
-
name, seed, panels: standardCartridgePanels, assets, entitySprites,
|
|
19
|
-
flow: { start: "gate", countdownSeconds: 3, restart: true }, // press-to-start → 3-2-1 → playing; "restart" command
|
|
20
|
-
player: { kind, health, walkSpeed }, // compiled into content + spawned per player
|
|
21
|
-
enemies: { id: { label, health, walkSpeed, xp, contact: { damage, intervalSeconds }, behavior: "chase" | "none" } },
|
|
22
|
-
combat: { contactRadius },
|
|
23
|
-
spawning: { director: SpawnDirectorConfig, placement: { kind: "ring", radius } },
|
|
24
|
-
weapons: { id: { kind: "projectile" | "orbit" | "pulse" | "custom", damage, cooldownMs, maxLevel, ... } },
|
|
25
|
-
progression: { xp: Curve, maxLevel, draft: { choices, upgrades } },
|
|
26
|
-
fields: { magnetRadius, damageMultiplier }, // named run-scalars upgrades mutate
|
|
27
|
-
xpGems: { collectRadius, pullSpeed, rarityThresholds, defaultRarity },
|
|
28
|
-
rules: { win: { kind: "survive", seconds }, lose: { kind: "playerDeath" } | { kind: "custom", check }, killLeaderboardStat },
|
|
29
|
-
world, physics, camera, worldItem, theme, hud, screens, // screens: start/win/lose
|
|
30
|
-
};
|
|
31
|
-
export const game = cartridge(config);
|
|
32
|
-
```
|
|
33
|
-
|
|
34
|
-
The run is a phase machine every game gets for free: `start → countdown → playing → won | lost`. `flow.start: "gate"` renders `screens.start` (TitleScreen binding) until begin; `flow.countdownSeconds` renders the big-number countdown; `flow.restart: true` registers a `restart` command that resets the run in place, clears cartridge entities, and restores player stats — no hand-rolled session store, phase union, or reset function. `run.playingSeconds` is the run clock (drives the survive rule and the timer panel), so pauses and pre-game phases never skew timing.
|
|
35
|
-
|
|
36
|
-
## Cartridge surface (`@jgengine/shell/cartridge/`)
|
|
37
|
-
|
|
38
|
-
The survivor-shaped defaults (XP gems, contact-radius melee, orbit/pulse/projectile weapons) are a genre archetype, not a core primitive — the whole cartridge implementation lives in `@jgengine/shell` (opt-in, alongside its React binding), never in `@jgengine/core`. `core` only supplies the generic pieces it composes: `spawnDirector`, `abilityKit`, `leveling`/`progression`, `runDraft`, `autoTarget`. A puzzle or tactics game imports none of this.
|
|
39
|
-
|
|
40
|
-
- `@jgengine/shell/cartridge/spec` — the `CartridgeSpec` types, `Leveled` values (`number | { base, perLevel, min?, max? } | { table }` resolved by `leveled(value, level)`), `WASD_KEYBINDS` (the default when `input` is omitted).
|
|
41
|
-
- `@jgengine/shell/cartridge/runtime` — `createCartridge(spec)` → `{ content, loop, run(ctx), weaponKit(ctx), chooseUpgrade(ctx, offerId) }`. Owns the run store (outcome/kills/fields/weapon levels, subscribe/notify), spawn director advance + ring placement, chase + contact-damage enemies (terrain-grounded), the three weapon archetypes with auto-targeting, magnet xp-gem pickups feeding `leveling()`, pause-and-draft level-ups, kill → gem drop + leaderboard, and win/lose rules.
|
|
42
|
-
- `@jgengine/shell/cartridge/validate` — `validateCartridge(spec)` → problem list: spawn waves must reference declared enemies, upgrades must reference declared weapons/fields, stacks can't exceed weapon `maxLevel`, tuning must be positive, rarity thresholds descending. The game's test asserts it returns `[]`.
|
|
43
|
-
|
|
44
|
-
Weapon archetypes: `projectile` (auto-target nearest in `range`, travel-time bolt fx), `orbit` (leveled blade count/radius sweeping `hitRadius`), `pulse` (radial AoE with linear falloff and an expanding-ring fx). Upgrade effects: `weaponLevel` (+1 level per stack), `statBonus` (+max and +current on pick), `fieldAdd` / `fieldMultiply` (recomputed from stacks). Weapon damage is multiplied by the `damageMultiplier` field when declared.
|
|
45
|
-
|
|
46
|
-
## Presentation (shell + registry)
|
|
47
|
-
|
|
48
|
-
`hud.panels` is a schema — items `vital | xp | timer | score | abilityBar | component`, anchored via the movable HUD layout; `screens.win/lose` render the results/death screens; the upgrade-draft modal appears whenever a draft is pending. The shell renders the frame and injects concrete components through the `panels` seam — `standardCartridgePanels` (registry `cartridge-panels`) binds the jgengine UI kit; swap any binding for a custom component without touching the engine. Weapon fx render engine-side from the run store's bolt/pulse feeds (`fxColor`/`fxEmissive` per weapon). `theme` is the `--jg-*` CSS var block — hand-pick it or derive from a few seed colors with `deriveJgTheme` (registry `jg-theme`).
|
|
49
|
-
|
|
50
|
-
## Escape hatches — bespoke logic without leaving the cartridge
|
|
51
|
-
|
|
52
|
-
- `weapons.<id> = { kind: "custom", fire(ctx, run, args) }` — cooldown/level/damage handled for you; the fire shape is yours.
|
|
53
|
-
- `spawning.placement = { kind: "custom", position(ctx, run) }`.
|
|
54
|
-
- `rules.win = { kind: "custom", check(ctx, run) }`.
|
|
55
|
-
- `progression.draft.upgrades[].effect = { kind: "custom", apply(ctx, run, stacks) }`.
|
|
56
|
-
- `systems: [(ctx, run, dt) => ...]` — extra per-tick simulation after the built-ins, only while playing.
|
|
57
|
-
- `hud` items `{ kind: "component", Component }` — arbitrary React panels alongside the schema ones.
|
|
58
|
-
|
|
59
|
-
A game that is *mostly* bespoke simulation (custom physics, session machines) should keep `defineGame` and write its systems — the cartridge is for games whose shape the archetypes already cover.
|
|
60
|
-
|
|
61
|
-
## Testing
|
|
62
|
-
|
|
63
|
-
Cartridge behaviors are engine-tested once (`packages/shell/src/cartridge/runtime.test.ts`); the game test is one call plus any game-specific assertions:
|
|
64
|
-
|
|
65
|
-
```ts
|
|
66
|
-
import { bootCartridge, cartridgeSmokeTest, tickCartridge } from "@jgengine/shell/cartridge/testkit";
|
|
67
|
-
cartridgeSmokeTest(config); // validate + world summary + headless run/spawn/kill/gem smoke
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
`bootCartridge`/`tickCartridge` build a headless `GameContext` and drive the loop (auto-choosing drafts) for custom assertions — see `@jgengine/shell/cartridge/testkit`. The testkit imports `bun:test`; import it from test files only.
|
|
71
|
-
|
|
72
|
-
Barrel surface tokens: createKeyValueStore createUnlocks durabilityFraction isComplete isOverdrawn memorySaveBackend partInSlot pruneToasts policyProjectsViewers projectByVisibleIds projectEntitiesForViewer projectPerUserForViewer visibleEntityIds applyDeltaToSnapshot applySurfaceDeltaToSnapshot boundaryNeighbors catenaryCurve distanceToPolygonEdge footprintObstacles hasValidAdjacency pointInPolygon polygonArea polygonBounds populateNavGridFromEnvironment readScatterPalette readScatterRules revertDeltaFromSnapshot revertSurfaceDeltaFromSnapshot slopeStepCost solveLock solveLockPath
|