@jgengine/shell 0.24.0 → 0.26.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 +92 -0
- package/dist/Shell3dPresentation.js +29 -11
- package/dist/ShellChrome.js +1 -1
- package/dist/ShellNotices.d.ts +4 -4
- package/dist/ShellNotices.js +9 -6
- package/dist/devtools/FrameTelemetry.d.ts +12 -0
- package/dist/devtools/FrameTelemetry.js +68 -0
- package/dist/devtools/agentBridge.js +2 -0
- package/dist/devtools/devtoolsReports.d.ts +2 -0
- package/dist/devtools/devtoolsReports.js +3 -0
- package/dist/devtools/frameSampler.d.ts +127 -0
- package/dist/devtools/frameSampler.js +345 -0
- package/dist/environment/AuthoredDaylight.d.ts +28 -0
- package/dist/environment/AuthoredDaylight.js +42 -0
- package/dist/environment/Daylight.d.ts +55 -7
- package/dist/environment/Daylight.js +126 -29
- package/dist/environment/daylightCycle.d.ts +26 -4
- package/dist/environment/daylightCycle.js +36 -5
- package/dist/postfx/PostProcessing.d.ts +7 -1
- package/dist/postfx/PostProcessing.js +28 -2
- package/dist/postfx/autoExposure.d.ts +49 -0
- package/dist/postfx/autoExposure.js +189 -0
- package/dist/postfx/gradeShader.js +43 -4
- package/dist/presentationRecovery3d.js +2 -1
- package/dist/render/CascadedShadows.d.ts +3 -1
- package/dist/render/CascadedShadows.js +9 -1
- package/dist/render/EnvironmentLighting.d.ts +10 -1
- package/dist/render/EnvironmentLighting.js +11 -3
- package/dist/render/IrradianceVolumeLighting.d.ts +18 -0
- package/dist/render/IrradianceVolumeLighting.js +35 -0
- package/dist/render/SceneLighting.d.ts +14 -1
- package/dist/render/SceneLighting.js +29 -8
- package/dist/render/authoredGrade.d.ts +11 -0
- package/dist/render/authoredGrade.js +26 -0
- package/dist/render/presentationLook.d.ts +15 -0
- package/dist/render/presentationLook.js +18 -0
- package/dist/render/stableTextureSet.d.ts +9 -0
- package/dist/render/stableTextureSet.js +24 -0
- package/dist/render/triplanarMaterial.d.ts +52 -0
- package/dist/render/triplanarMaterial.js +169 -0
- package/dist/settings/appliedSettings.js +16 -13
- package/package.json +4 -4
- package/skills/jgengine/api.md +59 -6
- package/skills/jgengine/capabilities.md +4 -0
- package/skills/jgengine-ui/SKILL.md +3 -3
- package/skills/jgengine-ui/api.md +98 -31
- package/skills/jgengine-ui/capabilities.md +15 -1
- package/skills/jgengine-ui/reference-primitives.md +5 -3
- package/skills/jgengine-world/SKILL.md +3 -1
- package/skills/jgengine-world/api.md +107 -3
- package/skills/jgengine-world/capabilities.md +22 -2
- package/skills/jgengine-world/recipes/npc-senses.md +8 -2
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useRef } from "react";
|
|
2
|
+
/** Whether two texture sets hold the same roles and texture instances. @internal */
|
|
3
|
+
export function sameTextureSet(a, b) {
|
|
4
|
+
if (a === b)
|
|
5
|
+
return true;
|
|
6
|
+
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
|
|
7
|
+
return false;
|
|
8
|
+
const keys = Object.keys(a);
|
|
9
|
+
if (keys.length !== Object.keys(b).length)
|
|
10
|
+
return false;
|
|
11
|
+
return keys.every((key) => a[key] === b[key]);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Keep one texture-set identity while its roles and texture instances are unchanged. The drei
|
|
15
|
+
* loader hands back a fresh keyed object per render, and materials built from it rebuilt and
|
|
16
|
+
* recompiled their shaders every frame.
|
|
17
|
+
* @internal
|
|
18
|
+
*/
|
|
19
|
+
export function useStableTextureSet(textures) {
|
|
20
|
+
const stable = useRef(textures);
|
|
21
|
+
if (!sameTextureSet(stable.current, textures))
|
|
22
|
+
stable.current = textures;
|
|
23
|
+
return stable.current;
|
|
24
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { MaterialAsset } from "@jgengine/core/material/materialAsset";
|
|
2
|
+
import * as THREE from "three";
|
|
3
|
+
/** Inputs for {@link applyTriplanarProjection}. */
|
|
4
|
+
export interface TriplanarProjectionOptions {
|
|
5
|
+
/** Metres one texture tile covers in world space. Must be finite and positive. */
|
|
6
|
+
tileMetres: number;
|
|
7
|
+
/** Blend sharpness between the three projections; 4 keeps corners soft, 8 is decisive. */
|
|
8
|
+
sharpness?: number;
|
|
9
|
+
/** Normal strength applied to each projection's tangent-space perturbation. */
|
|
10
|
+
normalScale?: number;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The GLSL the triplanar path injects, split by chunk. Exposed so a test can prove the patch
|
|
14
|
+
* samples by world position instead of `vMapUv`, and so a reader can audit the projection.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export declare function triplanarChunks(): {
|
|
18
|
+
vertexPars: string;
|
|
19
|
+
vertexNormal: string;
|
|
20
|
+
vertexPosition: string;
|
|
21
|
+
fragmentPars: string;
|
|
22
|
+
color: string;
|
|
23
|
+
roughness: string;
|
|
24
|
+
normal: string;
|
|
25
|
+
ao: string;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Projects a standard material's maps from world position instead of mesh UVs, so world geometry
|
|
29
|
+
* without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their
|
|
30
|
+
* instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is
|
|
31
|
+
* close enough for the static world surfaces this path exists for.
|
|
32
|
+
*/
|
|
33
|
+
export declare function applyTriplanarProjection(material: THREE.MeshStandardMaterial, options: TriplanarProjectionOptions): THREE.MeshStandardMaterial;
|
|
34
|
+
/**
|
|
35
|
+
* Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the
|
|
36
|
+
* editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size
|
|
37
|
+
* defaults to the asset's own `physicalSize`, which the shared library sets per surface.
|
|
38
|
+
* @capability material-triplanar render a material asset on world geometry without UVs
|
|
39
|
+
*/
|
|
40
|
+
export declare function useTriplanarAssetMaterial(asset: MaterialAsset, options?: {
|
|
41
|
+
tileMetres?: number;
|
|
42
|
+
sharpness?: number;
|
|
43
|
+
normalScale?: number;
|
|
44
|
+
}): THREE.MeshStandardMaterial;
|
|
45
|
+
/** Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal. */
|
|
46
|
+
export declare function TriplanarAssetMaterial({ asset, ...options }: {
|
|
47
|
+
asset: MaterialAsset;
|
|
48
|
+
} & {
|
|
49
|
+
tileMetres?: number;
|
|
50
|
+
sharpness?: number;
|
|
51
|
+
normalScale?: number;
|
|
52
|
+
}): import("react").JSX.Element;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import * as THREE from "three";
|
|
4
|
+
import { useMaterialAssetMaterial } from "./materialAsset.js";
|
|
5
|
+
const VERTEX_PARS = `varying vec3 vJgTriPos;
|
|
6
|
+
varying vec3 vJgTriNormal;`;
|
|
7
|
+
const VERTEX_WORLD_NORMAL = `vJgTriNormal = normalize(mat3(modelMatrix) * objectNormal);`;
|
|
8
|
+
const VERTEX_WORLD_POSITION = `vec4 jgTriWorld = vec4(transformed, 1.0);
|
|
9
|
+
#ifdef USE_INSTANCING
|
|
10
|
+
jgTriWorld = instanceMatrix * jgTriWorld;
|
|
11
|
+
#endif
|
|
12
|
+
vJgTriPos = (modelMatrix * jgTriWorld).xyz;`;
|
|
13
|
+
const FRAGMENT_PARS = `varying vec3 vJgTriPos;
|
|
14
|
+
varying vec3 vJgTriNormal;
|
|
15
|
+
uniform float uJgTriTile;
|
|
16
|
+
uniform float uJgTriSharp;
|
|
17
|
+
uniform float uJgTriNormalScale;
|
|
18
|
+
vec3 jgTriWeights() {
|
|
19
|
+
vec3 w = pow(abs(normalize(vJgTriNormal)), vec3(uJgTriSharp));
|
|
20
|
+
return w / max(dot(w, vec3(1.0)), 1e-4);
|
|
21
|
+
}
|
|
22
|
+
vec3 jgTriSample(sampler2D tex, vec3 w) {
|
|
23
|
+
vec3 p = vJgTriPos / uJgTriTile;
|
|
24
|
+
return texture2D(tex, p.zy).rgb * w.x + texture2D(tex, p.xz).rgb * w.y + texture2D(tex, p.xy).rgb * w.z;
|
|
25
|
+
}`;
|
|
26
|
+
const FRAGMENT_COLOR = `#ifdef USE_MAP
|
|
27
|
+
{
|
|
28
|
+
vec3 jgW = jgTriWeights();
|
|
29
|
+
vec3 jgP = vJgTriPos / uJgTriTile;
|
|
30
|
+
diffuseColor *= texture2D(map, jgP.zy) * jgW.x + texture2D(map, jgP.xz) * jgW.y + texture2D(map, jgP.xy) * jgW.z;
|
|
31
|
+
}
|
|
32
|
+
#endif`;
|
|
33
|
+
const FRAGMENT_ROUGHNESS = `float roughnessFactor = roughness;
|
|
34
|
+
#ifdef USE_ROUGHNESSMAP
|
|
35
|
+
roughnessFactor *= jgTriSample(roughnessMap, jgTriWeights()).g;
|
|
36
|
+
#endif`;
|
|
37
|
+
// Each projection perturbs the world normal along its own tangent frame: an X-facing projection
|
|
38
|
+
// uses +Z as U and +Y as V, a Y-facing one +X/+Z, a Z-facing one +X/+Y.
|
|
39
|
+
const FRAGMENT_NORMAL = `#ifdef USE_NORMALMAP
|
|
40
|
+
{
|
|
41
|
+
vec3 jgW = jgTriWeights();
|
|
42
|
+
vec3 jgP = vJgTriPos / uJgTriTile;
|
|
43
|
+
vec3 jgNx = texture2D(normalMap, jgP.zy).xyz * 2.0 - 1.0;
|
|
44
|
+
vec3 jgNy = texture2D(normalMap, jgP.xz).xyz * 2.0 - 1.0;
|
|
45
|
+
vec3 jgNz = texture2D(normalMap, jgP.xy).xyz * 2.0 - 1.0;
|
|
46
|
+
jgNx.xy *= uJgTriNormalScale;
|
|
47
|
+
jgNy.xy *= uJgTriNormalScale;
|
|
48
|
+
jgNz.xy *= uJgTriNormalScale;
|
|
49
|
+
vec3 jgPerturb = vec3(jgNx.z, jgNx.y, jgNx.x) * jgW.x
|
|
50
|
+
+ vec3(jgNy.x, jgNy.z, jgNy.y) * jgW.y
|
|
51
|
+
+ vec3(jgNz.x, jgNz.y, jgNz.z) * jgW.z;
|
|
52
|
+
normal = normalize((viewMatrix * vec4(normalize(vJgTriNormal + jgPerturb), 0.0)).xyz);
|
|
53
|
+
}
|
|
54
|
+
#endif`;
|
|
55
|
+
const FRAGMENT_AO = `#ifdef USE_AOMAP
|
|
56
|
+
{
|
|
57
|
+
float jgAo = jgTriSample(aoMap, jgTriWeights()).r;
|
|
58
|
+
float ambientOcclusion = ( jgAo - 1.0 ) * aoMapIntensity + 1.0;
|
|
59
|
+
reflectedLight.indirectDiffuse *= ambientOcclusion;
|
|
60
|
+
#if defined( USE_CLEARCOAT )
|
|
61
|
+
clearcoatSpecularIndirect *= ambientOcclusion;
|
|
62
|
+
#endif
|
|
63
|
+
#if defined( USE_SHEEN )
|
|
64
|
+
sheenSpecularIndirect *= ambientOcclusion;
|
|
65
|
+
#endif
|
|
66
|
+
#if defined( USE_ENVMAP ) && defined( STANDARD )
|
|
67
|
+
float dotNV = saturate( dot( geometryNormal, geometryViewDir ) );
|
|
68
|
+
reflectedLight.indirectSpecular *= computeSpecularOcclusion( dotNV, ambientOcclusion, material.roughness );
|
|
69
|
+
#endif
|
|
70
|
+
}
|
|
71
|
+
#endif`;
|
|
72
|
+
/**
|
|
73
|
+
* The GLSL the triplanar path injects, split by chunk. Exposed so a test can prove the patch
|
|
74
|
+
* samples by world position instead of `vMapUv`, and so a reader can audit the projection.
|
|
75
|
+
* @internal
|
|
76
|
+
*/
|
|
77
|
+
export function triplanarChunks() {
|
|
78
|
+
return {
|
|
79
|
+
vertexPars: VERTEX_PARS,
|
|
80
|
+
vertexNormal: VERTEX_WORLD_NORMAL,
|
|
81
|
+
vertexPosition: VERTEX_WORLD_POSITION,
|
|
82
|
+
fragmentPars: FRAGMENT_PARS,
|
|
83
|
+
color: FRAGMENT_COLOR,
|
|
84
|
+
roughness: FRAGMENT_ROUGHNESS,
|
|
85
|
+
normal: FRAGMENT_NORMAL,
|
|
86
|
+
ao: FRAGMENT_AO,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function assertPatchable(material) {
|
|
90
|
+
const vertex = THREE.ShaderLib.physical.vertexShader;
|
|
91
|
+
const fragment = THREE.ShaderLib.physical.fragmentShader;
|
|
92
|
+
const required = [
|
|
93
|
+
[vertex, "#include <beginnormal_vertex>"],
|
|
94
|
+
[vertex, "#include <worldpos_vertex>"],
|
|
95
|
+
[fragment, "#include <map_fragment>"],
|
|
96
|
+
[fragment, "#include <roughnessmap_fragment>"],
|
|
97
|
+
[fragment, "#include <normal_fragment_maps>"],
|
|
98
|
+
[fragment, "#include <aomap_fragment>"],
|
|
99
|
+
];
|
|
100
|
+
for (const [source, marker] of required) {
|
|
101
|
+
if (!source.includes(marker))
|
|
102
|
+
throw new Error(`Three.js r${THREE.REVISION} physical shader lacks ${marker} for the triplanar world projection`);
|
|
103
|
+
}
|
|
104
|
+
if (!material.isMeshStandardMaterial)
|
|
105
|
+
throw new Error("Triplanar projection requires a standard or physical material");
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Projects a standard material's maps from world position instead of mesh UVs, so world geometry
|
|
109
|
+
* without a `uv` attribute still renders a textured PBR surface. Instanced meshes project in their
|
|
110
|
+
* instanced world space. Skinned meshes keep the bind-pose normal for the blend weights, which is
|
|
111
|
+
* close enough for the static world surfaces this path exists for.
|
|
112
|
+
*/
|
|
113
|
+
export function applyTriplanarProjection(material, options) {
|
|
114
|
+
const tile = options.tileMetres;
|
|
115
|
+
if (!Number.isFinite(tile) || tile <= 0)
|
|
116
|
+
throw new Error("Triplanar tileMetres must be finite and positive");
|
|
117
|
+
const sharpness = options.sharpness ?? 8;
|
|
118
|
+
const normalScale = options.normalScale ?? 1;
|
|
119
|
+
if (!Number.isFinite(sharpness) || sharpness <= 0)
|
|
120
|
+
throw new Error("Triplanar sharpness must be finite and positive");
|
|
121
|
+
if (!Number.isFinite(normalScale) || normalScale < 0)
|
|
122
|
+
throw new Error("Triplanar normalScale must be finite and non-negative");
|
|
123
|
+
if (material.userData["jgTriplanar"] !== undefined)
|
|
124
|
+
return material;
|
|
125
|
+
assertPatchable(material);
|
|
126
|
+
const uniforms = {
|
|
127
|
+
uJgTriTile: { value: tile },
|
|
128
|
+
uJgTriSharp: { value: sharpness },
|
|
129
|
+
uJgTriNormalScale: { value: normalScale },
|
|
130
|
+
};
|
|
131
|
+
material.userData["jgTriplanar"] = uniforms;
|
|
132
|
+
const previousCompile = material.onBeforeCompile;
|
|
133
|
+
const previousKey = material.customProgramCacheKey;
|
|
134
|
+
material.onBeforeCompile = (shader, renderer) => {
|
|
135
|
+
previousCompile.call(material, shader, renderer);
|
|
136
|
+
Object.assign(shader.uniforms, uniforms);
|
|
137
|
+
shader.vertexShader = shader.vertexShader
|
|
138
|
+
.replace("#include <common>", `#include <common>\n${VERTEX_PARS}`)
|
|
139
|
+
.replace("#include <beginnormal_vertex>", `#include <beginnormal_vertex>\n${VERTEX_WORLD_NORMAL}`)
|
|
140
|
+
.replace("#include <worldpos_vertex>", `#include <worldpos_vertex>\n${VERTEX_WORLD_POSITION}`);
|
|
141
|
+
shader.fragmentShader = shader.fragmentShader
|
|
142
|
+
.replace("#include <common>", `#include <common>\n${FRAGMENT_PARS}`)
|
|
143
|
+
.replace("#include <map_fragment>", FRAGMENT_COLOR)
|
|
144
|
+
.replace("#include <roughnessmap_fragment>", FRAGMENT_ROUGHNESS)
|
|
145
|
+
.replace("#include <normal_fragment_maps>", FRAGMENT_NORMAL)
|
|
146
|
+
.replace("#include <aomap_fragment>", FRAGMENT_AO);
|
|
147
|
+
};
|
|
148
|
+
material.customProgramCacheKey = () => `${previousKey.call(material)}|jg-triplanar:v1`;
|
|
149
|
+
material.needsUpdate = true;
|
|
150
|
+
return material;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Render a serializable material asset — one built by `libraryMaterialAsset`, or authored in the
|
|
154
|
+
* editor — with world-space triplanar projection, so world geometry needs no UVs. Tile size
|
|
155
|
+
* defaults to the asset's own `physicalSize`, which the shared library sets per surface.
|
|
156
|
+
* @capability material-triplanar render a material asset on world geometry without UVs
|
|
157
|
+
*/
|
|
158
|
+
export function useTriplanarAssetMaterial(asset, options = {}) {
|
|
159
|
+
const material = useMaterialAssetMaterial(asset);
|
|
160
|
+
const tile = options.tileMetres ?? asset.textures?.color?.physicalSize?.[0] ?? asset.textures?.normal?.physicalSize?.[0] ?? 1;
|
|
161
|
+
const sharpness = options.sharpness ?? 8;
|
|
162
|
+
const normalScale = options.normalScale ?? 1;
|
|
163
|
+
return useMemo(() => applyTriplanarProjection(material, { tileMetres: tile, sharpness, normalScale }), [material, tile, sharpness, normalScale]);
|
|
164
|
+
}
|
|
165
|
+
/** Mounts {@link useTriplanarAssetMaterial} on a mesh; the R3F renderer owns disposal. */
|
|
166
|
+
export function TriplanarAssetMaterial({ asset, ...options }) {
|
|
167
|
+
const material = useTriplanarAssetMaterial(asset, options);
|
|
168
|
+
return _jsx("primitive", { object: material, attach: "material" });
|
|
169
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo, useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { busVolumeSettingId, DEFAULT_MASTER_VOLUME, DEFAULT_UI_SCALE, SETTING_IDS, UI_SCALE_MAX, UI_SCALE_MIN, } from "@jgengine/core/settings/settingsModel";
|
|
3
3
|
import { readGraphicsSettings } from "@jgengine/core/settings/graphicsSettings";
|
|
4
4
|
import { readFrameRateLimit } from "@jgengine/core/settings/frameRateLimit";
|
|
@@ -29,18 +29,21 @@ export function useSettingsRevision(store) {
|
|
|
29
29
|
return useSyncExternalStore(subscribe, getVersion, getVersion);
|
|
30
30
|
}
|
|
31
31
|
export function useGraphicsSettings(store, shadowsDefault, overrides) {
|
|
32
|
-
useSettingsRevision(store);
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
const rev = useSettingsRevision(store);
|
|
33
|
+
// Effects rebuild on identity change; a fresh object each render recompiled the post chain every frame.
|
|
34
|
+
return useMemo(() => {
|
|
35
|
+
const { quality, profile } = readGraphicsSettings(store, overrides);
|
|
36
|
+
const rawUiScale = store.get(SETTING_IDS.graphicsUiScale, DEFAULT_UI_SCALE);
|
|
37
|
+
return {
|
|
38
|
+
shadows: store.get(SETTING_IDS.graphicsShadows, shadowsDefault),
|
|
39
|
+
dpr: profile.renderScale,
|
|
40
|
+
uiScale: Math.min(UI_SCALE_MAX, Math.max(UI_SCALE_MIN, rawUiScale)),
|
|
41
|
+
quality,
|
|
42
|
+
profile,
|
|
43
|
+
frameRateLimit: readFrameRateLimit(store),
|
|
44
|
+
showFps: store.get(SETTING_IDS.graphicsShowFps, false),
|
|
45
|
+
};
|
|
46
|
+
}, [store, rev, shadowsDefault, overrides]);
|
|
44
47
|
}
|
|
45
48
|
export function AudioSettingsBridge({ store, engine, buses, }) {
|
|
46
49
|
const rev = useSettingsRevision(store);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.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",
|
|
@@ -35,9 +35,9 @@
|
|
|
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.26.0",
|
|
39
|
+
"@jgengine/react": "^0.26.0",
|
|
40
|
+
"@jgengine/ws": "^0.26.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|
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.26.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
10
10
|
|
|
11
11
|
## @jgengine/core/authoring
|
|
12
12
|
|
|
@@ -82,25 +82,42 @@
|
|
|
82
82
|
- `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
|
|
83
83
|
- `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
|
|
84
84
|
- `DevtoolsControlKind` (type): type DevtoolsControlKind = | "slider" | "toggle" | "color" | "select" | "text" | "vec2" | "vec3" | "vec4" | "interval" | "angle" | "enum" — ⚠ undocumented
|
|
85
|
-
- `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, and formatted message.
|
|
85
|
+
- `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
|
|
86
86
|
- `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
|
|
87
|
+
- `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
|
|
87
88
|
- `DevtoolsOverrides` (interface): interface DevtoolsOverrides { version: number; enabled: string[]; values: Record<string, unknown>; schemas?: Record<string, {kind: DevtoolsControlKind; schemaVersion?: number}> } — ⚠ undocumented
|
|
88
89
|
- `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
|
|
89
90
|
- `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
|
|
90
91
|
- `DiscoverySkip` (interface): interface DiscoverySkip { readonly path: string; readonly reason: string } — ⚠ undocumented
|
|
91
92
|
- `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
|
|
92
93
|
- `FrameStats` (interface): interface FrameStats — Aggregated frame-timing statistics over the recent sampling window, including per-phase breakdown.
|
|
93
|
-
- `
|
|
94
|
+
- `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
|
|
95
|
+
- `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
|
|
96
|
+
- `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
|
|
94
97
|
- `LatencyStats` (interface): interface LatencyStats — Aggregated latency statistics (last/avg/min/max) over recorded latency samples.
|
|
98
|
+
- `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
|
|
99
|
+
- `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
|
|
100
|
+
- `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
|
|
95
101
|
- `LongFrameEvent` (interface): interface LongFrameEvent — Recorded spike event for a frame that exceeded the long-frame threshold, with culprit attribution.
|
|
96
102
|
- `NormalizedColor` (interface): interface NormalizedColor { readonly hex: string; readonly rgb: string; readonly alpha: number; readonly hasAlpha: boolean } — ⚠ undocumented
|
|
97
103
|
- `OVERRIDES_FORMAT_VERSION` (const): const OVERRIDES_FORMAT_VERSION: 1 — ⚠ undocumented
|
|
98
104
|
- `OverrideApplyDiagnostic` (interface): interface OverrideApplyDiagnostic { readonly id: string; readonly reason: string } — ⚠ undocumented
|
|
99
105
|
- `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
|
|
106
|
+
- `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
|
|
107
|
+
- `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
|
|
100
108
|
- `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
|
|
109
|
+
- `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
|
|
110
|
+
- `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
|
|
111
|
+
- `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
|
|
101
112
|
- `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
|
|
113
|
+
- `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
|
|
114
|
+
- `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
|
|
115
|
+
- `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
|
|
102
116
|
- `ScanFieldMeta` (interface): interface ScanFieldMeta extends TunableOptions { kind?: DevtoolsControlKind } — ⚠ undocumented
|
|
103
117
|
- `ScanMeta` (type): type ScanMeta = Readonly<Record<string, ScanFieldMeta>> — ⚠ undocumented
|
|
118
|
+
- `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
|
|
119
|
+
- `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
|
|
120
|
+
- `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
|
|
104
121
|
- `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
|
|
105
122
|
- `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
|
|
106
123
|
- `TunableChoice` (interface): interface TunableChoice<T = unknown> { readonly value: T; readonly label?: string } — ⚠ undocumented
|
|
@@ -139,9 +156,20 @@
|
|
|
139
156
|
## @jgengine/core/devtools/frame
|
|
140
157
|
|
|
141
158
|
- `FrameModule` (interface): interface FrameModule — Frame subsystem exposing the frame-timing, profiling, and render-sample facades.
|
|
142
|
-
- `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — Frame duration in milliseconds above which a frame is recorded as a long-frame spike (~30fps budget).
|
|
143
159
|
- `createFrameModule` (const): const createFrameModule: (deps: { readProbes: () => Record<string, unknown>; }) => FrameModule — Create the frame subsystem that records frame/sim/phase timings, computes stats, and captures long-frame events.
|
|
144
160
|
|
|
161
|
+
## @jgengine/core/devtools/logBus
|
|
162
|
+
|
|
163
|
+
- `LOG_BUS_CAPACITY` (const): const LOG_BUS_CAPACITY: 200 — Default measured window for the log ring; older lines are evicted, never grown.
|
|
164
|
+
- `LOG_BUS_MESSAGE_MAX` (const): const LOG_BUS_MESSAGE_MAX: 400 — Longest kept message; a longer one is cut once, at write time.
|
|
165
|
+
- `LOG_BUS_SEVERE_CAPACITY` (const): const LOG_BUS_SEVERE_CAPACITY: 64 — Warnings and errors also land in their own ring, so an error is never pushed out by info traffic.
|
|
166
|
+
- `LogBus` (interface): interface LogBus — A fixed-capacity ring of log lines tagged by owning system. `write` reuses a preallocated slot — no array growth, no splice, no per-write object — so a hot path can call it and only the message string (already built by the caller) costs an allocation.
|
|
167
|
+
- `LogBusCounts` (interface): interface LogBusCounts — Lifetime totals per level, plus how many lines the ring dropped.
|
|
168
|
+
- `LogBusEntry` (interface): interface LogBusEntry — One captured line: when, how bad, which system said it, and what it said.
|
|
169
|
+
- `LogBusOptions` (interface): interface LogBusOptions — Tunables for the log ring: capacity, the severity ring's capacity, and the clock.
|
|
170
|
+
- `LogBusSnapshot` (interface): interface LogBusSnapshot — Read-out of the log bus: newest-window entries, the warning/error ring, totals, and evictions.
|
|
171
|
+
- `createLogBus` (function): function createLogBus(options: LogBusOptions = {}): LogBus — Create the bounded log bus the devtools log facade writes through.
|
|
172
|
+
|
|
145
173
|
## @jgengine/core/devtools/logs
|
|
146
174
|
|
|
147
175
|
- `LogsModule` (interface): interface LogsModule — Logs subsystem exposing the log-capture and latency-tracking facades.
|
|
@@ -153,6 +181,27 @@
|
|
|
153
181
|
- `OverridesModule` (interface): interface OverridesModule — Overrides subsystem exposing the export/apply facade for persisting and restoring control values.
|
|
154
182
|
- `createOverridesModule` (const): const createOverridesModule: (deps: { signal: ChangeSignal; controlRecords: Map<string, ControlRecord>; writeControl: (record: ControlRecord, raw: unknown) => boolean; discoveredRecords: Map<string, DiscoveredRecord>; enableDiscovered: (id: string) => void; }) => OverridesModule — Create the overrides subsystem that serializes changed control values and re-applies a saved overrides payload.
|
|
155
183
|
|
|
184
|
+
## @jgengine/core/devtools/report
|
|
185
|
+
|
|
186
|
+
- `FrameWindow` (interface): interface FrameWindow — Frame timings over the measured window, as the player sees them.
|
|
187
|
+
- `GlReport` (interface): interface GlReport — GPU-object creation counts. `total` is since the page loaded, the named fields since warmup.
|
|
188
|
+
- `LONG_FRAME_MS` (const): const LONG_FRAME_MS: 33.4 — The one perf record shape. `bun run perf` (CDP, out of process) and the in-page telemetry report (`window.__JG.report()`, the dev-server route, `bun run debug`) both produce exactly this object, so a budget check and a diff read either one.
|
|
189
|
+
- `PerfQuality` (type): type PerfQuality = "full" | "draft" — Graphics tier the record was measured at.
|
|
190
|
+
- `PerfReport` (interface): interface PerfReport — One measured window of one game: the record `bun run perf`, `window.__JG.report()` and `bun run debug` print.
|
|
191
|
+
- `ProfileEntry` (interface): interface ProfileEntry — One sampled stack frame reduced to a readable key and its self time.
|
|
192
|
+
- `ProfileReport` (interface): interface ProfileReport — CPU profile summary; only the CDP harness can produce one.
|
|
193
|
+
- `RenderReport` (interface): interface RenderReport — Renderer-side counts for the measured window; `null` where the source could not read them.
|
|
194
|
+
- `ReportLogLine` (interface): interface ReportLogLine — One log line as it appears in the report.
|
|
195
|
+
- `ReportLogs` (interface): interface ReportLogs — Bounded log read-out carried by the report: warn/error lines survive even a flood of info logs.
|
|
196
|
+
- `ReportTelemetry` (interface): interface ReportTelemetry — Who produced the report, and how much producing it cost.
|
|
197
|
+
- `SceneNodeSource` (type): type SceneNodeSource = "scene-graph" | "heap-object3d" | "unavailable" — Where the scene-node count came from: the live scene graph, a heap walk, or nowhere.
|
|
198
|
+
|
|
199
|
+
## @jgengine/core/devtools/reportEndpoint
|
|
200
|
+
|
|
201
|
+
- `REPORT_ENDPOINT_PATH` (const): const REPORT_ENDPOINT_PATH: "/__jgengine/report" — The dev-only route a running game pushes its in-page report to, and that `bun run debug <game>` reads back. One process: the page gathers, the dev server holds the newest record per game, the CLI fetches it. Nothing here ships in a production build — the Vite plugin mounts it under `apply: "serve"`.
|
|
202
|
+
- `ReportPushBody` (interface): interface ReportPushBody — Body the page POSTs: the game it measured and the record itself.
|
|
203
|
+
- `ReportRouteError` (interface): interface ReportRouteError — Body sent when there is nothing to return yet.
|
|
204
|
+
|
|
156
205
|
## @jgengine/core/devtools/rewriteTunables
|
|
157
206
|
|
|
158
207
|
- `TunableDelta` (interface): interface TunableDelta — One changed tunable to write back into game source: which table, key path, and new value.
|
|
@@ -203,8 +252,10 @@
|
|
|
203
252
|
|
|
204
253
|
- `Devtools` (interface): interface Devtools — Top-level devtools facade aggregating frame, profile, render, logs, latency, controls, discover, overrides, and probes.
|
|
205
254
|
- `DevtoolsControl` (interface): interface DevtoolsControl — Registered tunable control with its resolved schema metadata and read/write/reset accessors.
|
|
206
|
-
- `
|
|
255
|
+
- `DevtoolsLatency` (interface): interface DevtoolsLatency — Latency samples recorded by instrumented transport methods.
|
|
256
|
+
- `DevtoolsLogEntry` (interface): interface DevtoolsLogEntry — A single captured log line with timestamp, level, owning system tag, and formatted message.
|
|
207
257
|
- `DevtoolsLogLevel` (type): type DevtoolsLogLevel = "log" | "info" | "warn" | "error" — Severity level for a captured devtools log entry.
|
|
258
|
+
- `DevtoolsLogs` (interface): interface DevtoolsLogs — Bounded log ring with per-system tags, a warning/error ring, and lifetime counts.
|
|
208
259
|
- `DevtoolsSnapshot` (interface): interface DevtoolsSnapshot — Serializable point-in-time snapshot of devtools state: frame, render, latency, logs, controls, and discovered fields.
|
|
209
260
|
- `DiscoveredEntry` (interface): interface DiscoveredEntry — A control auto-discovered by scanning an object/table, with its binding metadata and current reader.
|
|
210
261
|
- `FrameRecordSample` (interface): interface FrameRecordSample — Per-frame timing input recorded by callers: total frame time, sim time, and optional named phase durations.
|
|
@@ -214,6 +265,8 @@
|
|
|
214
265
|
- `OverrideApplyResult` (interface): interface OverrideApplyResult — Outcome of applying a devtools overrides payload: counts of applied and skipped entries plus diagnostics.
|
|
215
266
|
- `PhaseStats` (interface): interface PhaseStats — Timing summary for a single named profiling phase across the sampling window.
|
|
216
267
|
- `RenderSample` (interface): interface RenderSample — Snapshot of renderer resource counts (draw calls, triangles, geometries, textures) for a frame.
|
|
268
|
+
- `SystemProbe` (interface): interface SystemProbe — What one system reports about itself: totals it keeps anyway, named timings, and one state line.
|
|
269
|
+
- `SystemProbeReading` (interface): interface SystemProbeReading — One system's probe read out on demand; missing sections stay empty rather than absent.
|
|
217
270
|
- `Tunable` (interface): interface Tunable<T> — Typed handle to a registered control, exposing its live value and subscribe/set/reset operations.
|
|
218
271
|
- `TunableAccessor` (interface): interface TunableAccessor — Get/set accessor pair plus initial value used to bind a discovered field to a control.
|
|
219
272
|
|
|
@@ -232,7 +285,7 @@
|
|
|
232
285
|
|
|
233
286
|
- `CHANGELOG` (const): const CHANGELOG: Record<string, ChangelogEntry> — Per-version engine changelog keyed by semver string (e.g. `"0.10.0"`).
|
|
234
287
|
- `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.
|
|
288
|
+
- `VERSION` (const): const VERSION: "0.26.0" — Installed `@jgengine/core` semver — compare against {@link CHANGELOG} keys when migrating.
|
|
236
289
|
|
|
237
290
|
## @jgengine/core/runtime/adapter
|
|
238
291
|
|
|
@@ -48,6 +48,10 @@ Reach for these before hand-rolling. Each row is *the thing you need* → *the p
|
|
|
48
48
|
|
|
49
49
|
- `defineSystem` (function) · `import { defineSystem } from "@jgengine/shell/gameKit"`
|
|
50
50
|
|
|
51
|
+
## devtools-log-bus — a bounded, per-system-tagged log ring a system writes to and a report reads
|
|
52
|
+
|
|
53
|
+
- `LogBus` (interface) · `import { LogBus } from "@jgengine/core/devtools/logBus"`
|
|
54
|
+
|
|
51
55
|
## editor-catalogs — Export typed gameplay catalogs for in-editor tuning.
|
|
52
56
|
|
|
53
57
|
- `EditorCatalogDefinition` (interface) · `import { EditorCatalogDefinition } from "@jgengine/shell/gameKit"`
|
|
@@ -32,8 +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`, `
|
|
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). Every engine rule sits in the `components.jgengine` cascade layer, so plain game CSS and Tailwind utilities override engine buttons and panels without `!important`. 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`, `HudSurface`, and `TweenNumber`.** `HudButton` gives hover lift, press scale, focus ring, and disabled from tokens, and wraps long labels; `HudSurface` (`@jgengine/react/hudSurface`; `HudPanel` from `@jgengine/react/hudPanel` is its deprecated old name, not the `HudCanvas` slot) gives elevation, header slots, an `as` landmark element, padding from `--jg-panel-padding`, 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). `audio.hudCues: false` silences those control sounds; notice sounds are separate (`notices.cues`).
|
|
37
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.
|
|
38
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`.
|
|
39
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.
|
|
@@ -43,7 +43,7 @@ Existing React games keep their entity store and use the focused
|
|
|
43
43
|
|
|
44
44
|
## Shared-world interaction rules
|
|
45
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
|
|
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. The stack renders inside the HUD theme root, so it takes the game's `hudTheme` colours and font; a top or bottom stack moves clear of `HudCanvas` panels and touch controls in its column and sits above `ModalHost` (z 85). Tune or hide it with `notices: { placement, offset, avoidHud, zIndex, cues, hidden }` (`hidden` keeps cues and lets the game render `useNoticeList(ctx.game.notices)` itself); `push({ text, silent: true })` skips one notice's sound.
|
|
47
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`.
|
|
48
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.
|
|
49
49
|
- Placement previews display claim cost and affordability inline with `TerritoryOverlay`.
|