@jgengine/shell 0.21.0 → 0.23.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 +114 -0
- package/dist/GamePlayerShell.js +18 -3
- package/dist/GameUiPreview.js +2 -1
- package/dist/Shell3dPresentation.js +13 -8
- package/dist/ShellChrome.d.ts +1 -1
- package/dist/ShellChrome.js +4 -2
- package/dist/ShellHudPresentation.js +2 -1
- package/dist/ShellNotices.d.ts +26 -0
- package/dist/ShellNotices.js +51 -0
- package/dist/audio/AudioComponents.d.ts +3 -1
- package/dist/audio/AudioComponents.js +5 -2
- package/dist/audio/listenerAnchor.d.ts +15 -0
- package/dist/audio/listenerAnchor.js +17 -0
- package/dist/audio/playableAudio.d.ts +9 -0
- package/dist/audio/playableAudio.js +11 -0
- package/dist/commandSink.d.ts +1 -1
- package/dist/commandSink.js +8 -3
- package/dist/defineGame.js +4 -2
- package/dist/devtools/DevtoolsOverlay.js +1 -0
- package/dist/devtools/agentBridge.d.ts +21 -0
- package/dist/devtools/agentBridge.js +95 -3
- package/dist/devtools/devtoolsReports.d.ts +32 -0
- package/dist/devtools/devtoolsReports.js +26 -0
- package/dist/devtools/simStepper.d.ts +21 -0
- package/dist/devtools/simStepper.js +37 -0
- package/dist/drivers/FrameDriver.js +26 -14
- package/dist/drivers/HudOnlyDriver.js +16 -5
- package/dist/environment/EnvironmentScene.js +24 -9
- package/dist/environment/RoadJunctions.d.ts +3 -1
- package/dist/environment/RoadJunctions.js +9 -2
- package/dist/environment/RoadRibbons.d.ts +3 -1
- package/dist/environment/RoadRibbons.js +16 -3
- package/dist/environment/terrainGroundColor.d.ts +5 -4
- package/dist/environment/terrainGroundColor.js +20 -9
- package/dist/materialOverride.js +4 -2
- package/dist/playSurfaceFocus.d.ts +16 -0
- package/dist/playSurfaceFocus.js +42 -0
- package/dist/postfx/PostProcessing.d.ts +3 -1
- package/dist/postfx/PostProcessing.js +14 -6
- package/dist/registry.d.ts +5 -1
- package/dist/render/CascadedShadows.d.ts +2 -0
- package/dist/render/CascadedShadows.js +4 -0
- package/dist/render/EntityPreview.js +1 -1
- package/dist/render/EnvironmentLighting.d.ts +26 -7
- package/dist/render/EnvironmentLighting.js +40 -27
- package/dist/render/SceneLighting.js +2 -2
- package/dist/render/modelLoad.d.ts +7 -0
- package/dist/render/modelLoad.js +13 -0
- package/dist/render/modelRender.d.ts +1 -0
- package/dist/render/modelRender.js +3 -0
- package/dist/render/modelSurface.d.ts +39 -0
- package/dist/render/modelSurface.js +206 -0
- package/dist/render/useModelInstance.d.ts +1 -1
- package/dist/render/useModelInstance.js +2 -2
- package/dist/scatter/scatterModels.js +3 -1
- package/dist/scene/AuthoredScene.d.ts +11 -3
- package/dist/scene/AuthoredScene.js +17 -9
- package/dist/scene/CityRenderer.js +86 -24
- package/dist/scene/GrassFieldRenderer.js +20 -2
- package/dist/scene/cityJunctionSurface.d.ts +7 -0
- package/dist/scene/cityJunctionSurface.js +34 -0
- package/dist/settings/settingsController.js +1 -1
- package/dist/terrain/CarvedTerrain.d.ts +2 -1
- package/dist/terrain/CarvedTerrain.js +2 -2
- package/dist/terrain/GrassField.d.ts +2 -1
- package/dist/terrain/GrassField.js +3 -2
- package/dist/terrain/grassGeometry.d.ts +3 -0
- package/dist/terrain/grassGeometry.js +2 -1
- package/dist/terrain/index.d.ts +5 -0
- package/dist/terrain/index.js +5 -0
- package/dist/terrain/surfaceRibbonMaterial.d.ts +62 -0
- package/dist/terrain/surfaceRibbonMaterial.js +227 -0
- package/dist/terrain/terrainDetailMaterial.d.ts +33 -10
- package/dist/terrain/terrainDetailMaterial.js +149 -63
- package/dist/terrain/terrainMath.d.ts +2 -0
- package/dist/terrain/terrainMath.js +2 -0
- package/dist/terrain/terrainPaintColor.d.ts +20 -0
- package/dist/terrain/terrainPaintColor.js +92 -0
- package/dist/terrain/terrainQuality.d.ts +5 -0
- package/dist/terrain/terrainQuality.js +7 -0
- package/dist/terrain/terrainSurfaceShader.d.ts +16 -0
- package/dist/terrain/terrainSurfaceShader.js +266 -0
- package/dist/terrain/terrainSurfaces.d.ts +59 -0
- package/dist/terrain/terrainSurfaces.js +199 -0
- package/dist/world/StaticObjectModels.d.ts +1 -1
- package/dist/world/StaticObjectModels.js +1 -1
- package/dist/world/WorldScene.js +1 -1
- package/dist/world/staticObjectBatches.d.ts +5 -1
- package/dist/world/staticObjectBatches.js +9 -5
- package/dist/world/worldBarSamples.d.ts +13 -1
- package/dist/world/worldBarSamples.js +40 -8
- package/package.json +5 -5
- package/skills/game-design/SKILL.md +2 -0
- package/skills/jgengine/SKILL.md +1 -0
- package/skills/jgengine/api.md +18 -18
- package/skills/jgengine-ui/SKILL.md +4 -0
- package/skills/jgengine-ui/api.md +242 -34
- package/skills/jgengine-ui/capabilities.md +56 -0
- package/skills/jgengine-ui/reference-primitives.md +4 -4
- package/skills/jgengine-world/api.md +28 -15
- package/skills/jgengine-world/capabilities.md +4 -0
- package/skills/jgengine-world/reference.md +30 -0
|
@@ -32,6 +32,18 @@ function projectAnchor(entity, height, camera, project) {
|
|
|
32
32
|
return null;
|
|
33
33
|
return anchor;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Screen pixels per world metre at `anchor`: project a point one metre along the camera's right
|
|
37
|
+
* axis and measure the gap. Call after reading the anchor's own projection — `project` is reused.
|
|
38
|
+
*/
|
|
39
|
+
function pixelsPerMeter(anchor, camera, project, viewport, x, y) {
|
|
40
|
+
const m = camera.matrixWorld.elements;
|
|
41
|
+
project.set(anchor[0] + m[0], anchor[1] + m[1], anchor[2] + m[2]).project(camera);
|
|
42
|
+
const px = (project.x * 0.5 + 0.5) * viewport.width;
|
|
43
|
+
const py = (-project.y * 0.5 + 0.5) * viewport.height;
|
|
44
|
+
const scale = Math.hypot(px - x, py - y);
|
|
45
|
+
return Number.isFinite(scale) ? scale : 0;
|
|
46
|
+
}
|
|
35
47
|
function healthFraction(stat) {
|
|
36
48
|
const range = stat.max - stat.min;
|
|
37
49
|
return range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
|
|
@@ -72,11 +84,14 @@ export function collectWorldBarSamples(ctx, statId, height, roles, resolveRole,
|
|
|
72
84
|
break;
|
|
73
85
|
if (occlude && worldBarOccluded(ctx, origin, anchor))
|
|
74
86
|
continue;
|
|
87
|
+
const x = (project.x * 0.5 + 0.5) * viewport.width;
|
|
88
|
+
const y = (-project.y * 0.5 + 0.5) * viewport.height;
|
|
75
89
|
into.push({
|
|
76
90
|
entityId: id,
|
|
77
|
-
x
|
|
78
|
-
y
|
|
91
|
+
x,
|
|
92
|
+
y,
|
|
79
93
|
percent: healthFraction(stat),
|
|
94
|
+
pxPerMeter: pixelsPerMeter(anchor, camera, project, viewport, x, y),
|
|
80
95
|
});
|
|
81
96
|
}
|
|
82
97
|
return into.length;
|
|
@@ -91,10 +106,12 @@ export function refreshWorldBarSamples(ctx, height, camera, viewport, samples, p
|
|
|
91
106
|
const entity = eligibleEntity(ctx, sample.entityId, options);
|
|
92
107
|
if (entity === null || Math.hypot(entity.position[0] - origin[0], entity.position[1] - origin[1], entity.position[2] - origin[2]) > maxDistance)
|
|
93
108
|
continue;
|
|
94
|
-
|
|
109
|
+
const anchor = projectAnchor(entity, height, camera, project);
|
|
110
|
+
if (anchor === null)
|
|
95
111
|
continue;
|
|
96
112
|
sample.x = (project.x * 0.5 + 0.5) * viewport.width;
|
|
97
113
|
sample.y = (-project.y * 0.5 + 0.5) * viewport.height;
|
|
114
|
+
sample.pxPerMeter = pixelsPerMeter(anchor, camera, project, viewport, sample.x, sample.y);
|
|
98
115
|
samples[kept++] = sample;
|
|
99
116
|
}
|
|
100
117
|
samples.length = kept;
|
|
@@ -138,19 +155,34 @@ export function collectNameplateSamples(ctx, statId, height, roles, resolveRole,
|
|
|
138
155
|
}
|
|
139
156
|
return into.length;
|
|
140
157
|
}
|
|
158
|
+
/** World width a health bar spans over its entity, in metres. */
|
|
159
|
+
const WORLD_BAR_METERS = 1.4;
|
|
160
|
+
const MIN_BAR_WIDTH_PX = 28;
|
|
161
|
+
/**
|
|
162
|
+
* Painted bar size for a sample: about {@link WORLD_BAR_METERS} wide at the anchor's on-screen
|
|
163
|
+
* scale, clamped between a still-readable minimum and the configured maximum, with height in
|
|
164
|
+
* proportion. Samples without a scale (caller-painted screen samples) keep the maximum.
|
|
165
|
+
* @internal
|
|
166
|
+
*/
|
|
167
|
+
export function worldBarSize(pxPerMeter, maxWidthPx = 112, maxHeightPx = 10) {
|
|
168
|
+
if (pxPerMeter === undefined || !(pxPerMeter > 0))
|
|
169
|
+
return { width: maxWidthPx, height: maxHeightPx };
|
|
170
|
+
const width = Math.max(Math.min(MIN_BAR_WIDTH_PX, maxWidthPx), Math.min(maxWidthPx, pxPerMeter * WORLD_BAR_METERS));
|
|
171
|
+
const height = Math.max(4, Math.min(maxHeightPx, Math.round(width / 11)));
|
|
172
|
+
return { width: Math.round(width), height };
|
|
173
|
+
}
|
|
141
174
|
/** @internal */
|
|
142
|
-
export function paintWorldBarSamples(canvas, samples, dpr,
|
|
175
|
+
export function paintWorldBarSamples(canvas, samples, dpr, maxBarWidthPx = 112, maxBarHeightPx = 10) {
|
|
143
176
|
const g = canvas.getContext("2d");
|
|
144
177
|
if (g === null)
|
|
145
178
|
return;
|
|
146
179
|
g.setTransform(1, 0, 0, 1, 0, 0);
|
|
147
180
|
g.clearRect(0, 0, canvas.width, canvas.height);
|
|
148
181
|
g.setTransform(dpr, 0, 0, dpr, 0, 0);
|
|
149
|
-
const halfW = barWidthPx / 2;
|
|
150
|
-
const halfH = barHeightPx / 2;
|
|
151
182
|
for (const sample of samples) {
|
|
152
|
-
const
|
|
153
|
-
const
|
|
183
|
+
const { width: barWidthPx, height: barHeightPx } = worldBarSize(sample.pxPerMeter, maxBarWidthPx, maxBarHeightPx);
|
|
184
|
+
const left = sample.x - barWidthPx / 2;
|
|
185
|
+
const top = sample.y - barHeightPx / 2;
|
|
154
186
|
g.fillStyle = "rgba(0,0,0,0.7)";
|
|
155
187
|
g.strokeStyle = "rgba(0,0,0,0.7)";
|
|
156
188
|
g.lineWidth = 1;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.23.0",
|
|
4
4
|
"description": "Game player shell for JGengine: React Three Fiber canvas, orbit camera, input tracking, HUD mounting, GameUiPreview, and a demo game. Consumers supply a GameRegistry.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -31,13 +31,13 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsgo -p tsconfig.build.json && bun ../../scripts/fix-extensions.ts dist",
|
|
34
|
-
"check-types": "
|
|
34
|
+
"check-types": "tsgo --noEmit -p tsconfig.json",
|
|
35
35
|
"test": "bun test src"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@jgengine/core": "^0.
|
|
39
|
-
"@jgengine/react": "^0.
|
|
40
|
-
"@jgengine/ws": "^0.
|
|
38
|
+
"@jgengine/core": "^0.23.0",
|
|
39
|
+
"@jgengine/react": "^0.23.0",
|
|
40
|
+
"@jgengine/ws": "^0.23.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|
|
@@ -11,6 +11,8 @@ Own the intended player experience and the rules that produce it: fantasy, pilla
|
|
|
11
11
|
|
|
12
12
|
Use `level-design` for spatial realization; route implementation to `jgengine-gameplay`, `jgengine-world`, `jgengine-combat`, `jgengine-ui`, `jgengine-multiplayer` once intent is coherent.
|
|
13
13
|
|
|
14
|
+
For a substantial existing-game quality pass spanning concept, depth, world and presentation, use `game-improvement` to sequence these owners and keep a per-game implementation plan and acceptance evidence.
|
|
15
|
+
|
|
14
16
|
For a greenfield build, read [references/application-playbook.md](references/application-playbook.md) and produce its greenfield contract (promise, pillars, anti-pillars, core decision, and falsifier) before writing code. Audits may read only the playbook's audit sections. Read [references/field-guide.md](references/field-guide.md) when the task needs deeper loop, economy, progression, genre, accessibility, or research guidance.
|
|
15
17
|
|
|
16
18
|
## Choose the mode
|
package/skills/jgengine/SKILL.md
CHANGED
|
@@ -53,6 +53,7 @@ Load only rows the target needs. Do not preload every domain.
|
|
|
53
53
|
| Need | Skill | Owner |
|
|
54
54
|
| --- | --- | --- |
|
|
55
55
|
| player promise, pillars, verbs, loops, economy, progression, balance | `game-design` | experience and rule intent |
|
|
56
|
+
| substantial existing-game improvement across depth, world and presentation | `game-improvement` | ordered improvement plan and per-game completion |
|
|
56
57
|
| metrics, topology, encounters, navigation, spatial pacing | `level-design` | playable space intent |
|
|
57
58
|
| scene placement, terrain painting, paths, asset authoring | `jgengine-editor` | editor document and authoring tools |
|
|
58
59
|
| runtime world, movement, input, interaction, AI, navigation, audio | `jgengine-world` | spatial simulation and world runtime |
|
package/skills/jgengine/api.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
8
8
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
9
|
-
- `VERSION` (const): const VERSION: "0.
|
|
9
|
+
- `VERSION` (const): const VERSION: "0.23.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
- `CommandDecoder` (type): type CommandDecoder<TInput> = (input: unknown) => CommandDecodeResult<TInput> — Parses raw `unknown` transport input into `TInput`, rejecting anything that doesn't match the command's declared shape. Runs before `validate`/`apply`, so a malformed payload never reaches game logic.
|
|
66
66
|
- `CommandDefinition` (interface): interface CommandDefinition<TState, TInput> { decode?: CommandDecoder<TInput>; validate?(state: TState, input: TInput): CommandRejection | null; apply(state: TState, input: TInput): TState | void } — ⚠ undocumented
|
|
67
67
|
- `CommandRegistry` (interface): interface CommandRegistry<TState> { define<TInput>(name: string, definition: CommandDefinition<TState, TInput>): void; has(name: string): boolean; names(): string[]; run(state: TState, name: string, input: unknown): CommandResult<TState> } — ⚠ undocumented
|
|
68
|
-
- `CommandRejection` (interface): interface CommandRejection
|
|
69
|
-
- `CommandResult` (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string } | { status: "unknown-command" } — ⚠ undocumented
|
|
68
|
+
- `CommandRejection` (interface): interface CommandRejection — Why a command refused: `reason` is a stable machine code; `message`, when set, is shown to the player.
|
|
69
|
+
- `CommandResult` (type): type CommandResult<TState> = | { status: "applied"; state: TState } | { status: "rejected"; reason: string; message?: string } | { status: "unknown-command" } — ⚠ undocumented
|
|
70
70
|
- `createCommandRegistry` (function): function createCommandRegistry<TState>(): CommandRegistry<TState> — ⚠ undocumented
|
|
71
71
|
|
|
72
72
|
## @jgengine/core/devtools/controls
|
|
@@ -232,7 +232,7 @@
|
|
|
232
232
|
|
|
233
233
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
234
234
|
- `ChangelogEntry` (interface): interface ChangelogEntry — One release's migrate steps plus added/changed/removed notes (typed mirror of CHANGELOG.md).
|
|
235
|
-
- `VERSION` (const): const VERSION: "0.
|
|
235
|
+
- `VERSION` (const): const VERSION: "0.23.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
236
236
|
|
|
237
237
|
## @jgengine/core/runtime/adapter
|
|
238
238
|
|
|
@@ -292,9 +292,9 @@
|
|
|
292
292
|
- `CommandDef` (type): type CommandDef<TInput = unknown> = { /** * `"server"` when `input` carries facts only the host may decide (unlock state, prices, multipliers): * public transports then refuse it and only trusted host calls such as `helpers.runCommand` run it. * Defaults to `"client"`, so every field of `input` must… — ⚠ undocumented · used by `defineCommand`: Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type.
|
|
293
293
|
- `CommandScope` (type): type CommandScope = { /** Member ids to hydrate; omit for the whole roster. */ players?: readonly string[]; /** Chunk keys to hydrate; omit for every chunk of the server, `[]` for none. */ chunkKeys?: readonly string[]; } — Which slice of a server has to be hydrated before some work runs against it. Both fields default to "everything", which costs a read per member plus one per chunk — the cost that makes a large shared world unaffordable per mutation. Narrow them when the caller knows what it will touch.
|
|
294
294
|
- `CommandScopeDefinition` (type): type CommandScopeDefinition = Omit<CommandScope, "players"> & { players?: readonly string[] | "actor" } — Declarative read scope; actor is resolved before host hydration.
|
|
295
|
-
- `CommandValidationError` (type): type CommandValidationError = { reason: string } —
|
|
295
|
+
- `CommandValidationError` (type): type CommandValidationError = { reason: string; message?: string } — `reason` is a stable machine code; `message`, when set, is player-facing text the client shows on refusal.
|
|
296
296
|
- `MALFORMED_COMMAND_INPUT_REASON` (const): const MALFORMED_COMMAND_INPUT_REASON: "Malformed command input" — Reason every host returns when a command's `parse` rejects its input.
|
|
297
|
-
- `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string } — ⚠ undocumented
|
|
297
|
+
- `RunCommandResult` (type): type RunCommandResult = | { ok: true; snapshot: GameRuntimeSnapshot } | { ok: false; reason: string; message?: string } — ⚠ undocumented
|
|
298
298
|
- `SERVER_ONLY_COMMAND_REASON` (const): const SERVER_ONLY_COMMAND_REASON: "Command is server-only" — Reason a public transport returns for a {@link CommandDef} marked `access: "server"`.
|
|
299
299
|
- `defineCommand` (function): function defineCommand<TInput>(definition: CommandDef<TInput> & { parse: (input: unknown) => TInput | null }): CommandDef — Declare a command with a typed input: `parse` narrows the wire value once, and `scope`, `validate` and `apply` receive the parsed type. Returns the untyped {@link CommandDef} a runtime's command table holds.
|
|
300
300
|
|
|
@@ -321,7 +321,7 @@
|
|
|
321
321
|
- `GameContextItemUse` (interface): interface GameContextItemUse { register(handlers: Record<string, ItemUseHandler<GameContext>>): void; registered(): string[]; can(input: ItemUseInput): ItemUseRejection | null; use(input: ItemUseInput): ItemUseResult<GameContext> } — ⚠ undocumented
|
|
322
322
|
- `GameContextLoot` (interface): interface GameContextLoot { register(def: LootTableDef): void; has(id: string): boolean; roll(id: string, rng?: () => number): Drop[]; grantToPlayer(userId: string, drops: Drop[], source?: string): void } — ⚠ undocumented
|
|
323
323
|
- `GameContextModels` (interface): interface GameContextModels — Per-kind render-model lookup for {@link GameContextOptions.models}; a resolved `ModelConfig` satisfies {@link ModelBodySource} structurally.
|
|
324
|
-
- `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number]
|
|
324
|
+
- `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number]; scenery?: boo… — ⚠ undocumented
|
|
325
325
|
- `GameContextOptions` (interface): interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> { definition: GameDefinition<TAssetRef, TMultiplayer>; content: GameContextContent; player: {userId: string; isNew: boolean}; territory?: Territory; now?: () => number; occluder?: (from: EntityPosition, to: EntityPosition) => boolean; seed?… — ⚠ undocumented
|
|
326
326
|
- `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
|
|
327
327
|
- `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
|
|
@@ -352,7 +352,7 @@
|
|
|
352
352
|
- `GameContextItemUse` (interface): interface GameContextItemUse { register(handlers: Record<string, ItemUseHandler<GameContext>>): void; registered(): string[]; can(input: ItemUseInput): ItemUseRejection | null; use(input: ItemUseInput): ItemUseResult<GameContext> } — ⚠ undocumented
|
|
353
353
|
- `GameContextLoot` (interface): interface GameContextLoot { register(def: LootTableDef): void; has(id: string): boolean; roll(id: string, rng?: () => number): Drop[]; grantToPlayer(userId: string, drops: Drop[], source?: string): void } — ⚠ undocumented
|
|
354
354
|
- `GameContextModels` (interface): interface GameContextModels — Per-kind render-model lookup for {@link GameContextOptions.models}; a resolved `ModelConfig` satisfies {@link ModelBodySource} structurally.
|
|
355
|
-
- `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number]
|
|
355
|
+
- `GameContextObjectEntry` (interface): interface GameContextObjectEntry { proximityPrompt?: ProximityPrompt; breakable?: false | {baseBreakTime: number}; slotInventory?: InventoryLayout; verbs?: readonly ContextVerb[]; colliders?: EntityColliderSet; halfExtents?: readonly [number, number, number]; scenery?: boo… — ⚠ undocumented
|
|
356
356
|
- `GameContextOptions` (interface): interface GameContextOptions<TAssetRef extends ModelAssetRef = ModelAssetRef, TMultiplayer = unknown> { definition: GameDefinition<TAssetRef, TMultiplayer>; content: GameContextContent; player: {userId: string; isNew: boolean}; territory?: Territory; now?: () => number; occluder?: (from: EntityPosition, to: EntityPosition) => boolean; seed?… — ⚠ undocumented
|
|
357
357
|
- `GameContextRace` (interface): interface GameContextRace { state(id: string, config?: RaceStateConfig): RaceState } — ⚠ undocumented
|
|
358
358
|
- `GameContextTurn` (interface): interface GameContextTurn { loop(id: string, config?: TurnLoopConfig): TurnLoop } — ⚠ undocumented
|
|
@@ -593,7 +593,7 @@
|
|
|
593
593
|
- `PresenceSync` (type): type PresenceSync = { subscribe: (serverId: string, onChange: (rows: PresencePoseRow[]) => void) => FeedUnsubscribe; syncPose: (serverId: string, pose: PlayerPose) => void; } — ⚠ undocumented
|
|
594
594
|
- `ResumeTicket` (type): type ResumeTicket = { userId: string; serverId: string; token: string; } — A short-lived credential allowing a disconnected client to reclaim one server membership.
|
|
595
595
|
- `RunCommandArgs` (type): type RunCommandArgs = { serverId: string; command: string; input: unknown; } — ⚠ undocumented
|
|
596
|
-
- `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } | { ok: false; reason: string } — ⚠ undocumented
|
|
596
|
+
- `TransportRunCommandResult` (type): type TransportRunCommandResult = | { ok: true } /** `message` is the command's player-facing refusal text, when its `validate` gave one. */ | { ok: false; reason: string; message?: string } — ⚠ undocumented
|
|
597
597
|
- `WorldSyncFrame` (type): type WorldSyncFrame = | { kind: "baseline"; revision: number; snapshot: WorldSnapshot } | { kind: "diff"; revision: number; diff: WorldDiff } — A revision-stamped world baseline or compact diff sent to a server-authoritative client.
|
|
598
598
|
- `createTransportSessionId` (function): function createTransportSessionId(): string — Fresh id for one client session's join/leave pair (see {@link GameRuntimeTransport.joinServer}).
|
|
599
599
|
|
|
@@ -661,8 +661,8 @@
|
|
|
661
661
|
|
|
662
662
|
## @jgengine/shell/gameKit
|
|
663
663
|
|
|
664
|
-
- `Clock` (function): function Clock({ format, showDay, controls, style, className, }: { format?: "24h" | "12h"; showDay?: boolean; controls?: boolean; style?: CSSProperties; className?: string; }): JSX.Element — A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so a game opts into letting the player scrub time.
|
|
665
|
-
- `Coins` (function): function Coins({ currencyId, icon, style, className, }: { currencyId: string; icon?: ReactNode; style?: CSSProperties; className?: string; }): JSX.Element — A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
|
|
664
|
+
- `Clock` (function): function Clock({ format = "24h", showDay = true, controls = false, style, className, }: { format?: "24h" | "12h"; showDay?: boolean; controls?: boolean; style?: CSSProperties; className?: string; }): React.JSX.Element — A time-of-day clock reading the sim calendar — `Day N · HH:MM`, 24h or 12h. `controls` adds pause + the game's speed multipliers as clickable pills (the "fast-forward" bar), off by default so a game opts into letting the player scrub time.
|
|
665
|
+
- `Coins` (function): function Coins({ currencyId, icon = "🪙", style, className, }: { currencyId: string; icon?: ReactNode; style?: CSSProperties; className?: string; }): React.JSX.Element — A currency counter — an icon (emoji/char, default a coin) plus the live amount for `currencyId`. *
|
|
666
666
|
- `DEFAULT_WALK_CODES` (const): const DEFAULT_WALK_CODES: { readonly moveForward: readonly ["KeyW"]; readonly moveBack: readonly ["KeyS"]; readonly moveLeft: readonly ["KeyA"]; readonly moveRight: readonly ["KeyD"]; readonly jump: readonly ["Space"]; } — Default keyboard codes for the shell walk controller (WASD + Space). Use as the base of `defineGame({ input: { ...DEFAULT_WALK_CODES, interact: ["KeyE"] } })` instead of re-typing the same five bindings in every game.
|
|
667
667
|
- `EditorCatalogDefinition` (interface): interface EditorCatalogDefinition — Game-exported catalog definition: a `ParamSchema` plus default entries. Schemas stay in code; entry values merge into `document.catalogs` and are what the editor/RPC edits and saves.
|
|
668
668
|
- `EditorDocument` (interface): interface EditorDocument — The full authored scene: every marker, volume, path, note, and sculpted terrain for a game.
|
|
@@ -673,21 +673,21 @@
|
|
|
673
673
|
- `GameHost` (function): function GameHost({ playable, gameId, wsUrl, multiplayer, resolveMultiplayer, editor, creator, creatorOpen = false, onCreatorOpenChange }: GameHostProps): React.JSX.Element — The one documented mount: resolves multiplayer for the playable and renders the shell. With the `editor` loader prop it also owns the whole editor summon (F2+E, `?mode=editor`, dev save endpoint).
|
|
674
674
|
- `GameHostProps` (interface): interface GameHostProps — Props for {@link GameHost}: the playable, optional multiplayer overrides, and the optional editor loader that enables the engine-owned F2+E summon.
|
|
675
675
|
- `GameLoop` (interface): interface GameLoop<TContext = unknown> — Lifecycle hooks a game implements to drive init, per-tick simulation, and player join/leave.
|
|
676
|
-
- `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?:
|
|
676
|
+
- `Hotbar` (function): function Hotbar({ inventoryId, activeSlot, keys, slotSize = 46, itemIcon, style, className, }: { inventoryId: string; activeSlot?: number; keys?: readonly string[]; slotSize?: number; /** Caller-supplied item id → icon registry; return null/undefined to fall back to the default glyph. */ itemIcon?: … — A numbered hotbar bound to an inventory — painted iconed slots (a `GameIcon` glyph over a school-keyed gradient with a count badge, #1035), an active-slot highlight, and a keycap per slot. Place it and pass the `inventoryId`; `activeSlot` highlights the equipped one. Supply `itemIcon` to map your item ids to your own glyphs — the default resolves a `GameIcon` from the item id.
|
|
677
677
|
- `HudCanvas` (function): function HudCanvas({ layout, editChord, compactScale, showDuring, className, style, children, }: { layout: HudLayoutStore; editChord?: HudEditChord | false; /** Zoom applied to the whole HUD on compact displays. Default 0.85. */ compactScale?: number; /** Opt-in play-phase gate: render the HUD only … — Full-viewport HUD surface. Panels declared with `HudPanel` flow into nine anchor regions and stack automatically with a gap — no per-panel pixel offsets, no manual clearance for sibling panels, the touch-control dock (`--jg-hud-dock-clearance`), or device safe areas. On compact displays the whole surface scales down and each panel applies its `compact` behavior.
|
|
678
|
-
- `HudPanel` (function): function HudPanel({ id, anchor, order, compact: compactMode, chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack
|
|
679
|
-
- `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
|
|
680
|
-
- `StatBar` (function): function StatBar({ statId, entityId, tone, label, showValue, width, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }
|
|
678
|
+
- `HudPanel` (function): function HudPanel({ id, anchor = "top-left", order, compact: compactMode = "keep", chip, interactive, inset, locked, showDuring, priority, mobileBehavior, allowOverlapWith, collisionGroup, region = true, width, height, type, className, style, children, }: { id: string; anchor?: HudAnchor; /** Stack … — A HUD block that lives in one of the nine anchor regions. Panels sharing a region stack outward from the screen edge in ascending `order`. On fine pointers panels stay draggable through the edit chord; a dragged panel leaves the flow and keeps its custom placement. On compact displays custom placements are ignored and the `compact` behavior applies.
|
|
679
|
+
- `PlayableGame` (type): type PlayableGame = EnginePlayableGame< ComponentType, ComponentType, RenderEntity, RenderObject, ComponentType<ViewmodelProps>, ComponentType<WorldOverlayProps> > & { /** HUD theme the shell applies at the `GameUI` root. Unset uses `defaultHudTheme`; `false` opts out of the default theme and the en… — The concrete React-bound playable a shell `defineGame` returns and `GameHost` mounts — the one PlayableGame games handle.
|
|
680
|
+
- `StatBar` (function): function StatBar({ statId = "health", entityId, tone = "health", label, showValue = true, width = 200, icon, style, className, }: { statId?: string; entityId?: string; tone?: StatTone; label?: string; showValue?: boolean; width?: number; icon?: ReactNode; style?: CSSProperties; className?: string; }… — A polished stat bar (health/mana/stamina/shield/xp) — a rounded, glassy meter with a tone-colored fill and an optional value readout. Reads `statId` off `entityId` (defaults to the local player). Renders nothing until the stat exists.
|
|
681
681
|
- `StoreHandle` (interface): interface StoreHandle<T> — A typed, cast-free handle onto one slot of the reactive game store (`ctx.game.store`). The single type parameter is fixed at definition; `read`/`write`/`update` never widen to `unknown`, so no call site repeats `store.get(key) as T`. Writes flow through the engine store, so they bump `ctx.version()`, serialize into a {@link WorldSnapshot}, and replay under host authority — the same guarantees a hand-rolled `store.get`/`store.set` pair gives up the moment it forks run state into a module-level singleton.
|
|
682
682
|
- `SystemDefinition` (interface): interface SystemDefinition — A reusable game capability — lifecycle, timing, events, and optional save / replication / reset / disposal. Pass instances via `defineGame({ systems })`. Prefer one system per meaningful capability (`combat`, `quests`), not per micro-tick.
|
|
683
|
-
- `authoredSpawnPosition` (function): function authoredSpawnPosition(document: AuthoredSpawnDocumentLike, options
|
|
683
|
+
- `authoredSpawnPosition` (function): function authoredSpawnPosition(document: AuthoredSpawnDocumentLike, options: AuthoredSpawnOptions = {}): [number, number, number] | null — Position of the authored spawn marker as a spawn-ready `[x, y, z]` tuple, or null when the document has none. Reads the first `player_spawn` marker by default, so dragging the marker in the editor moves where players spawn — no coordinates copied into game code.
|
|
684
684
|
- `collectAuthoredTriggers` (function): function collectAuthoredTriggers(document: SceneDocumentLike): AuthoredTrigger[] — Collect every authored trigger on a document's markers and volumes. Pure — no runtime state. Action params use the live {@link registerTriggerAction} registry when present.
|
|
685
685
|
- `defineGame` (function): function defineGame<TAssetRef extends ModelAssetRef = ModelAssetRef>(config: GameConfig<TAssetRef>): PlayableGame — The one public authoring entry point: compose engine fields (systems, world, physics, input) and presentation fields (camera, HUD, audio, authored scene) into a `PlayableGame` ready for `GameHost`. Defaults to solo/offline multiplayer; `editorLayers` auto-mounts the authored scene document.
|
|
686
686
|
- `defineKeyedStore` (function): function defineKeyedStore<T>(keyFor: (id: string) => string, initial: T | (() => T)): KeyedStoreHandle<T> — Define a typed per-owner keyed family on the game store: game code expresses `read(ctx, userId)`/`write(ctx, userId, v)`/`update(...)` with the value type it means, and both the `` `prefix:${id}` `` key composition and the `unknown → T` cast live once, here, behind the boundary. Reach for this over N separate `defineStore` calls whenever the owning id varies at runtime (per-user class, per-instance auras) — `defineStore` stays the right call for a single fixed slot.
|
|
687
687
|
- `defineStore` (function): function defineStore<T>(key: string, initial: T | (() => T)): StoreHandle<T> — Define a typed slot on the game store: game code expresses `read(ctx)`/`write(ctx, v)`/`update(...)` with the value type it means, and the `unknown → T` cast lives once, here, behind the boundary. Pass a factory for `initial` when the fallback is a fresh mutable object; the factory runs at most once and its result is reused, so an unwritten slot keeps a stable identity across reads (no per-read churn for a React selector, no allocation on a hot path). A value that carries functions (a closure-based run store, an input handle) is runtime-only: snapshots, saves and replication skip it and hydration keeps it in place. Hold such objects with `perContext` when they need no store notifications.
|
|
688
688
|
- `defineSystem` (function): function defineSystem(definition: SystemDefinition): SystemDefinition — Declare a composable game system. Pure data + hooks — the engine compiles the schedule and installs lifecycle when the game boots.
|
|
689
|
-
- `environment` (function): function environment(config
|
|
690
|
-
- `environmentContentFromDocument` (function): function environmentContentFromDocument(doc: EditorDocument, options
|
|
689
|
+
- `environment` (function): function environment(config: EnvironmentWorldConfig = {}): EnvironmentWorldFeature — Composes an `environment()` feature from terrain, sky, weather, vegetation, water, structures, roads, and pads.
|
|
690
|
+
- `environmentContentFromDocument` (function): function environmentContentFromDocument(doc: EditorDocument, options: EnvironmentContentOptions = {}): EnvironmentContent — Derives the coordinate/placement content of an `environment()` world from its scene document: terrain footprint (via {@link terrainBoundsFromDocument}), ground clearings under authored spawns/POIs (via `clearanceZonesFrom`), and the document's sculpt snapshot. This is the seam that lets a game author its world footprint and flatten regions in the editor instead of hardcoding them — the `world.ts` that consumes it carries only engine tuning.
|
|
691
691
|
- `grounded` (function): function grounded(ctx: Pick<GameContext, "world">, x: number, z: number): readonly [number, number, number] — Ground an XZ point onto the live world surface as `[x, groundY, z]`.
|
|
692
692
|
- `offline` (function): function offline(): MultiplayerAdapterConfig — Explicit single-player adapter. Solo games never need this — omitting `multiplayer` in the shell `defineGame` already defaults to offline; pass it only where an adapter value is structurally required.
|
|
693
693
|
- `seededRng` (function): function seededRng(seed: string | number): SeededRng — Deterministic pseudo-random generator seeded from a string or number — same seed, same sequence.
|
|
@@ -32,6 +32,8 @@ Existing React games keep their entity store and use the focused
|
|
|
32
32
|
- **Settings is game-owned and reachable from a sensible place.** Every game ships settings as *its* settings — skinned and placed inline with its UI (main menu and pause), never the engine's stock face floated into a corner. Credits are a reachable in-game screen, not attribution buried in a build artifact.
|
|
33
33
|
- **Route the front-end with `useMenuRouter`, and generate the credits.** `useMenuRouter(root)` is the screen stack behind title → save select → settings → credits: `open`/`back`/`replace`/`reset`, `path` for depth, and Escape-to-back bound by default (`escapeToBack: false` when the game owns Escape). `menuStackReducer` is the same transition as a pure function. For credits, `creditsForSourceIds(ids)` (`@jgengine/assets/credits`) turns the pack ids a game declares to `buildCatalog` into licensed, attributed sections, `mergeCredits` folds them into hand-authored lines, and `CreditsScreen` renders the result HudTheme-skinned.
|
|
34
34
|
- **Skin the shared chrome with one `HudTheme` token object.** `hudThemeVars(theme)` emits the whole `--jg-*` vocabulary — bar fills and troughs, `HudFrame variation="themed"`, painted slots, the minimap ring, plus surfaces, edges, text, status colors, fonts, and rarity — so one theme spread on any HUD ancestor reskins the `@jgengine/react` primitives and the `registry/jgengine` blocks together. Author your own with `deriveHudTheme({ accent, surface })` or `hudTheme({ ... })`; `HUD_THEME_PRESETS` / `resolveHudTheme("<preset>")` are demo/scaffold starting points, never a game's identity. A component that needs a token no theme emits fails `check-hud-tokens` — add it to `HudTheme` rather than inventing a second vocabulary. Caller `style`/`renderItem` overrides still win.
|
|
35
|
+
- **The shell applies `defaultHudTheme` at the GameUI root.** `HudThemeRoot` wraps every `GameUI` in a `display: contents` `[data-jg-hud]` element carrying the theme vars and injects the engine stylesheet (`jg-*` keyframes, `[data-jg-*]` interaction states, reduced motion). Set `defineGame({ hudTheme })` to swap the root theme, `hudTheme: false` to opt out; a theme spread inside `GameUI` still wins. Games outside the shell add `@import "@jgengine/react/styles.css";` to `src/index.css` (the `jgengine create` template does). The theme also carries a type scale with a 12px floor, spacing, motion, elevation, and hover/press/focus/disabled tokens; read them in inline styles through `hudType`, `hudSpace`, `hudTransition`, `hudElevation`, and `HUD_FOCUS_RING` (`@jgengine/react/hudTokens`) — never hardcode text under 12px.
|
|
36
|
+
- **Build interactive chrome from `HudButton`, `HudPanel`, and `TweenNumber`.** `HudButton` gives hover lift, press scale, focus ring, and disabled from tokens; `HudPanel` gives elevation, header slots, and an `open` enter/exit transition; `TweenNumber` / `useTweenedValue` roll counters with an up/down delta flash. Each takes `cue` (`data-jg-cue`) and `onInteract`; `HudInteractProvider` routes every primitive's hover/press/activate/open/close to one listener; the shell mounts one that plays the matching UI cue, so engine primitives sound with no wiring (`cue="confirm"` picks the activate sound).
|
|
35
37
|
- **Paint slot icons with `IconTreatment`, never render raw `itemId` strings.** It draws a `GameIcon` glyph over a school/element-keyed gradient with gloss, vignette, and optional count/keycap badges, reading the `--jg-slot-*` / `--jg-accent` tokens. `treatedItemIcon(itemId)` / `treatedActionIcon(action)` are ready adapters for hotbars and ability bars.
|
|
36
38
|
- **Player death gets a visible screen and respawn feedback — never a silent teleport.** Build the moment `game-design` requires: an authored death/downed overlay that reads the lethal event, communicates the consequence, and surfaces the way back (respawn/revive/restart). Drive it from `createDownedState` (`@jgengine/core/combat`) rather than a hand-rolled flag, and mount it inside `HudCanvas`.
|
|
37
39
|
- **Browser interruption listeners are shared.** Use `useBrowserSuspension` from `@jgengine/react/browserLifecycle` for blur, hidden-page and pointer-lock loss. Keep the decision to pause, release controls or preserve multiplayer simulation in the callback; use options to disable unwanted triggers.
|
|
@@ -41,6 +43,8 @@ Existing React games keep their entity store and use the focused
|
|
|
41
43
|
|
|
42
44
|
## Shared-world interaction rules
|
|
43
45
|
|
|
46
|
+
- **Refusals and notices are engine feedback, not per-game toasts.** Return `{ reason: "insufficient-gold", message: "Needs 40 more gold" }` from a command's `validate`: the shell shows `message` in the shared notice feed with the deny cue and stacks repeats. Push other player messages with `ctx.game.notices.push({ text, tone })` (info/success/warn/deny) from game logic instead of a hand-built toast list. Tune or hide the stock feed with `notices: { placement, hidden }` (`hidden` keeps cues and lets the game render `useNoticeList(ctx.game.notices)` itself).
|
|
47
|
+
- **UI sounds come from `useUiCue()`.** Engine primitives already sound through the shell; for game-owned markup call `cue("click" | "hover" | "open" | "close" | "confirm" | "deny" | "notify")` from buttons and panels; replace a default by declaring `audio.sounds["ui.click"]`, or silence with `audio.uiCues: { hover: false }` / `false`.
|
|
44
48
|
- Every rejected verb displays its reason on the triggering surface. Use `useServerSession(transport, gameId)` for join lifecycle. Join failure blocks play with `JoinGate` and retry; returning `{ ok: false }` without feedback is incomplete.
|
|
45
49
|
- Placement previews display claim cost and affordability inline with `TerritoryOverlay`.
|
|
46
50
|
- `ChatPanel` owns collapsed/expanded state, own-message styling, Enter focus and Escape blur; standalone hosts pass `messages`, `userId`, and `onSend` (typed `{ ok, reason }`), while context-backed games use channel props. Use native inputs so shell typing-target suspension applies.
|