@jgengine/shell 0.11.0 → 0.13.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 +52 -0
- package/dist/CombatPresentation.d.ts +5 -0
- package/dist/CombatPresentation.js +8 -0
- package/dist/GameHost.d.ts +27 -1
- package/dist/GameHost.js +83 -4
- package/dist/GamePlayer.d.ts +5 -0
- package/dist/GamePlayer.js +5 -0
- package/dist/GamePlayerShell.d.ts +5 -0
- package/dist/GamePlayerShell.js +63 -359
- package/dist/GameUiPreview.js +3 -0
- package/dist/Shell3dPresentation.d.ts +63 -0
- package/dist/Shell3dPresentation.js +302 -0
- package/dist/ShellChrome.d.ts +51 -0
- package/dist/ShellChrome.js +51 -0
- package/dist/ShellHudPresentation.d.ts +41 -0
- package/dist/ShellHudPresentation.js +22 -0
- package/dist/audio/audioEngine.d.ts +4 -0
- package/dist/audio/audioEngine.js +111 -26
- package/dist/audio/audioWire.d.ts +22 -4
- package/dist/audio/audioWire.js +46 -3
- package/dist/audio/loopParams.d.ts +20 -0
- package/dist/audio/loopParams.js +28 -0
- package/dist/camera/GameCameraRig.js +6 -1
- package/dist/camera/GameOrbitCamera.js +7 -1
- package/dist/camera/cameraCollision.d.ts +28 -0
- package/dist/camera/cameraCollision.js +34 -0
- package/dist/camera/cameraRigs.d.ts +3 -1
- package/dist/camera/cameraRigs.js +24 -10
- package/dist/camera/index.d.ts +1 -0
- package/dist/camera/index.js +1 -0
- package/dist/camera/rigMath.d.ts +27 -0
- package/dist/camera/rigMath.js +45 -0
- package/dist/defineGame.d.ts +15 -1
- package/dist/defineGame.js +26 -4
- package/dist/devtools/DevtoolsOverlay.d.ts +4 -4
- package/dist/devtools/DevtoolsOverlay.js +28 -12
- package/dist/devtools/agentBridge.js +4 -0
- package/dist/devtools/perfDiagnose.js +1 -1
- package/dist/diagnostics/RuntimeDiagnostics.d.ts +7 -0
- package/dist/diagnostics/RuntimeDiagnostics.js +23 -12
- package/dist/environment/EnvironmentScene.js +17 -3
- package/dist/environment/RoadJunctions.d.ts +12 -0
- package/dist/environment/RoadJunctions.js +69 -0
- package/dist/environment/RoadRibbons.d.ts +8 -2
- package/dist/environment/RoadRibbons.js +26 -8
- package/dist/environment/terrainGroundColor.d.ts +9 -0
- package/dist/environment/terrainGroundColor.js +22 -0
- package/dist/gameKit.d.ts +27 -0
- package/dist/gameKit.js +29 -0
- package/dist/map/MapMarkerBeacons.d.ts +3 -2
- package/dist/postfx/PostProcessing.d.ts +9 -1
- package/dist/postfx/PostProcessing.js +11 -4
- package/dist/presentationResolve.d.ts +39 -0
- package/dist/presentationResolve.js +53 -0
- package/dist/registry.d.ts +1 -0
- package/dist/render/PartMotion.d.ts +18 -0
- package/dist/render/PartMotion.js +91 -0
- package/dist/render/SceneModels.d.ts +9 -2
- package/dist/render/SceneModels.js +31 -180
- package/dist/render/assetBase.d.ts +17 -0
- package/dist/render/assetBase.js +35 -0
- package/dist/render/measureBounds.d.ts +34 -0
- package/dist/render/measureBounds.js +127 -0
- package/dist/render/modelLoad.js +4 -1
- package/dist/render/resolveModel.d.ts +15 -0
- package/dist/render/resolveModel.js +43 -2
- package/dist/render/useModelAnimation.d.ts +10 -0
- package/dist/render/useModelAnimation.js +198 -0
- package/dist/scatter/InstancedScatter.js +5 -0
- package/dist/scatter/scatterProxies.js +24 -0
- package/dist/scene/AuthoredScene.d.ts +3 -1
- package/dist/scene/AuthoredScene.js +7 -2
- package/dist/scene/CityRenderer.d.ts +2 -0
- package/dist/scene/CityRenderer.js +1031 -0
- package/dist/scene/GrassFieldRenderer.js +53 -5
- package/dist/scene/PoleLineRenderer.d.ts +2 -0
- package/dist/scene/PoleLineRenderer.js +287 -0
- package/dist/scene/SoilPatchRenderer.js +70 -7
- package/dist/scene/WaterRenderer.js +34 -10
- package/dist/scene/builtinSceneKindRenderers.js +4 -0
- package/dist/scene/cityWindowMaterial.d.ts +16 -0
- package/dist/scene/cityWindowMaterial.js +81 -0
- package/dist/scene/sceneKindRenderers.d.ts +2 -0
- package/dist/settings/appliedSettings.d.ts +2 -1
- package/dist/settings/appliedSettings.js +4 -2
- package/dist/structures/GeneratedBuilding.d.ts +4 -0
- package/dist/structures/GeneratedBuilding.js +17 -7
- package/dist/structures/TransformGizmo.d.ts +11 -1
- package/dist/structures/TransformGizmo.js +9 -5
- package/dist/terrain/GrassField.d.ts +11 -3
- package/dist/terrain/GrassField.js +16 -6
- package/dist/terrain/grassGeometry.d.ts +33 -1
- package/dist/terrain/grassGeometry.js +83 -21
- package/dist/terrain/grassMaterial.d.ts +18 -0
- package/dist/terrain/grassMaterial.js +66 -13
- package/dist/terrain/heightfieldGeometry.d.ts +34 -0
- package/dist/terrain/heightfieldGeometry.js +109 -0
- package/dist/terrain/index.d.ts +1 -0
- package/dist/terrain/index.js +1 -0
- package/dist/terrain/soilPatchMaterial.d.ts +15 -2
- package/dist/terrain/soilPatchMaterial.js +46 -8
- package/dist/terrain/terrainDetailMaterial.js +15 -2
- package/dist/water/Ocean.d.ts +7 -1
- package/dist/water/Ocean.js +16 -3
- package/dist/water/OceanConfig.d.ts +9 -0
- package/dist/water/OceanConfig.js +6 -0
- package/dist/water/OceanMaterial.d.ts +12 -0
- package/dist/water/OceanMaterial.js +9 -0
- package/dist/water/OceanShader.d.ts +2 -2
- package/dist/water/OceanShader.js +78 -10
- package/dist/world/PlaceScene.d.ts +11 -0
- package/dist/world/PlaceScene.js +27 -0
- package/dist/world/WorldHud.d.ts +3 -1
- package/dist/world/WorldHud.js +2 -2
- package/dist/world/WorldScene.js +32 -4
- package/dist/worldSky.d.ts +7 -1
- package/dist/worldSky.js +11 -1
- package/package.json +4 -4
- package/skills/game-design/SKILL.md +28 -0
- package/skills/game-design/agents/openai.yaml +4 -0
- package/skills/game-design/references/application-playbook.md +170 -0
- package/skills/game-design/references/field-guide.md +288 -0
- package/skills/jgengine/SKILL.md +8 -4
- package/skills/jgengine/api.md +182 -81
- package/skills/jgengine/capabilities.md +104 -2
- package/skills/jgengine/recipes/minimal-game.md +88 -0
- package/skills/jgengine-ui/SKILL.md +17 -10
- package/skills/jgengine-ui/api.md +316 -51
- package/skills/jgengine-ui/capabilities.md +135 -2
- package/skills/jgengine-ui/recipes/portable-minimap.md +150 -0
- package/skills/jgengine-ui/reference.md +59 -9
- package/skills/jgengine-ui/references/visual-scorecard.md +1 -1
- package/skills/jgengine-world/SKILL.md +13 -0
- package/skills/jgengine-world/api.md +453 -21
- package/skills/jgengine-world/capabilities.md +112 -2
- package/skills/jgengine-world/recipes/commanding-entities.md +128 -0
- package/skills/jgengine-world/reference.md +43 -4
- package/skills/level-design/SKILL.md +54 -0
- package/skills/level-design/agents/openai.yaml +4 -0
- package/skills/level-design/references/application-playbook.md +192 -0
- package/skills/level-design/references/field-guide.md +337 -0
- package/skills/level-design/references/live-editor-loop.md +179 -0
- package/dist/cartridge/runtime.d.ts +0 -15
- package/dist/cartridge/runtime.js +0 -556
- package/dist/cartridge/spec.d.ts +0 -188
- package/dist/cartridge/spec.js +0 -19
- package/dist/cartridge/validate.d.ts +0 -2
- package/dist/cartridge/validate.js +0 -78
- package/dist/cartridge.d.ts +0 -145
- package/dist/cartridge.js +0 -246
- package/skills/jgengine/reference-cartridge.md +0 -72
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\nvoid main() {\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float slopeTerm = horizontal * k * sine;\n tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);\n bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
|
|
2
|
-
export declare const oceanFragmentShader = "\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\n\
|
|
1
|
+
export declare const oceanVertexShader = "\nuniform float uTime;\nuniform vec2 uWaveDirections[6];\nuniform vec4 uWaveParams[6];\nuniform float uChoppiness;\nuniform float uFoamThreshold;\nuniform float uFoamSoftness;\nuniform float uFoamCoverage;\n\nattribute float aDepth;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\nvarying float vDepth;\nvarying vec2 vOceanUv;\n\nvoid main() {\n vOceanUv = uv;\n vec3 displaced = position;\n vec3 tangent = vec3(1.0, 0.0, 0.0);\n vec3 bitangent = vec3(0.0, 0.0, 1.0);\n float crest = 0.0;\n // Waves flatten as the bed shoals so swells die at the shoreline instead of clipping it.\n float depthDamp = smoothstep(0.05, 1.4, aDepth);\n\n for (int i = 0; i < 6; i++) {\n vec2 direction = normalize(uWaveDirections[i]);\n vec4 wave = uWaveParams[i];\n float k = wave.x;\n float amplitude = wave.y * depthDamp;\n float steepness = wave.z;\n float omega = wave.w;\n float phase = k * dot(direction, position.xz) - omega * uTime;\n float sine = sin(phase);\n float cosine = cos(phase);\n float horizontal = steepness * amplitude * uChoppiness;\n displaced.x += horizontal * direction.x * cosine;\n displaced.z += horizontal * direction.y * cosine;\n displaced.y += amplitude * sine;\n float slopeTerm = horizontal * k * sine;\n tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);\n bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);\n crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));\n }\n\n vNormal = normalize(cross(bitangent, tangent));\n vCrest = crest;\n vWaveHeight = displaced.y;\n vDepth = aDepth;\n vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);\n vWorldPosition = worldPosition.xyz;\n gl_Position = projectionMatrix * viewMatrix * worldPosition;\n}\n";
|
|
2
|
+
export declare const oceanFragmentShader = "\nuniform float uTime;\nuniform vec3 uShallowColor;\nuniform vec3 uDeepColor;\nuniform vec3 uCrestColor;\nuniform vec3 uFoamColor;\nuniform float uOpacity;\nuniform float uFresnelStrength;\nuniform float uHorizonBlend;\nuniform float uFoamIntensity;\nuniform float uDepthRange;\nuniform float uShoreFoamWidth;\nuniform float uSparkle;\nuniform vec3 uSunDirection;\n\nvarying vec3 vWorldPosition;\nvarying vec3 vNormal;\nvarying float vCrest;\nvarying float vWaveHeight;\nvarying float vDepth;\nvarying vec2 vOceanUv;\n\nfloat jgWaterHash(vec2 p) {\n uvec2 q = uvec2(ivec2(floor(p)));\n uint x = q.x * 1664525u + q.y * 1013904223u;\n x = (x ^ (x >> 16u)) * 2246822519u;\n x = (x ^ (x >> 13u)) * 3266489917u;\n return float(x ^ (x >> 16u)) * (1.0 / 4294967296.0);\n}\n\nfloat jgWaterNoise(vec2 p) {\n vec2 i = floor(p);\n vec2 f = fract(p);\n vec2 u = f * f * (3.0 - 2.0 * f);\n float a = jgWaterHash(i);\n float b = jgWaterHash(i + vec2(1.0, 0.0));\n float c = jgWaterHash(i + vec2(0.0, 1.0));\n float d = jgWaterHash(i + vec2(1.0, 1.0));\n return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);\n}\n\nvoid main() {\n vec3 viewDirection = normalize(cameraPosition - vWorldPosition);\n\n // Fine scrolling normal perturbation: two counter-drifting noise layers ripple the surface\n // between the big Gerstner swells.\n vec2 wp = vWorldPosition.xz;\n float ripple1 = jgWaterNoise(wp * 1.7 + vec2(uTime * 0.22, uTime * 0.15));\n float ripple2 = jgWaterNoise(wp * 3.1 - vec2(uTime * 0.17, uTime * 0.26));\n vec3 normal = normalize(normalize(vNormal) + vec3(ripple1 - 0.5, 0.0, ripple2 - 0.5) * 0.14);\n\n // Depth-based body color: warm shallows at the shore ramp falling to the deep tone.\n float depthMix = smoothstep(0.0, max(uDepthRange, 0.001), vDepth);\n vec3 waterColor = mix(uShallowColor, uDeepColor, depthMix);\n waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.22);\n\n // Restrained fresnel: a soft sky tint at grazing angles, never a white-out.\n float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 5.0);\n waterColor = mix(waterColor, vec3(0.62, 0.78, 0.85), min(fresnel * uFresnelStrength, 0.45));\n float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));\n waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend * 0.6);\n\n // Sun glints: sparkle where the perturbed normal mirrors the sun, gated by noise so it\n // twinkles in cells instead of sheening uniformly.\n vec3 halfway = normalize(viewDirection + normalize(uSunDirection));\n float glint = pow(max(dot(normal, halfway), 0.0), 150.0);\n float glintMask = smoothstep(0.68, 0.92, jgWaterNoise(wp * 2.3 + vec2(uTime * 0.4, -uTime * 0.31)));\n waterColor += vec3(1.0, 0.98, 0.9) * glint * glintMask * uSparkle;\n\n // Foam: a noise-eaten shoreline lick plus crest foam. A second, larger noise gates whole\n // stretches of shoreline so foam laps in patches instead of outlining the bank uniformly.\n float foamNoise = jgWaterNoise(wp * 2.6 + vec2(uTime * 0.28, uTime * 0.2));\n float foamStretch = smoothstep(0.3, 0.8, jgWaterNoise(wp * 0.33 + vec2(3.7, 9.2) + vec2(uTime * 0.05, 0.0)));\n float shoreFoam = (1.0 - smoothstep(0.0, max(uShoreFoamWidth, 0.001), vDepth - foamNoise * uShoreFoamWidth * 0.9))\n * smoothstep(0.25, 0.75, foamNoise) * (0.2 + 0.6 * foamStretch);\n float crestFoam = clamp(vCrest * uFoamIntensity, 0.0, 1.0) * smoothstep(0.35, 0.75, foamNoise);\n float foam = clamp(shoreFoam + crestFoam, 0.0, 1.0);\n vec3 color = mix(waterColor, uFoamColor, foam);\n\n // Soft shore: the sheet turns glass-clear over the last stretch of shoaling bed, so the\n // waterline is a blend into the terrain, not a hard tarp edge.\n float shoreAlpha = smoothstep(0.0, 0.55, vDepth);\n float alpha = uOpacity * mix(0.35, 1.0, shoreAlpha) + foam * 0.3;\n // The sheet's own border always dissolves \u2014 a bounded water plane must never end in a\n // visible straight cut, whatever the bed underneath is doing.\n float borderDist = min(min(vOceanUv.x, 1.0 - vOceanUv.x), min(vOceanUv.y, 1.0 - vOceanUv.y));\n alpha *= smoothstep(0.0, 0.045, borderDist);\n gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0));\n}\n";
|
|
@@ -7,22 +7,29 @@ uniform float uFoamThreshold;
|
|
|
7
7
|
uniform float uFoamSoftness;
|
|
8
8
|
uniform float uFoamCoverage;
|
|
9
9
|
|
|
10
|
+
attribute float aDepth;
|
|
11
|
+
|
|
10
12
|
varying vec3 vWorldPosition;
|
|
11
13
|
varying vec3 vNormal;
|
|
12
14
|
varying float vCrest;
|
|
13
15
|
varying float vWaveHeight;
|
|
16
|
+
varying float vDepth;
|
|
17
|
+
varying vec2 vOceanUv;
|
|
14
18
|
|
|
15
19
|
void main() {
|
|
20
|
+
vOceanUv = uv;
|
|
16
21
|
vec3 displaced = position;
|
|
17
22
|
vec3 tangent = vec3(1.0, 0.0, 0.0);
|
|
18
23
|
vec3 bitangent = vec3(0.0, 0.0, 1.0);
|
|
19
24
|
float crest = 0.0;
|
|
25
|
+
// Waves flatten as the bed shoals so swells die at the shoreline instead of clipping it.
|
|
26
|
+
float depthDamp = smoothstep(0.05, 1.4, aDepth);
|
|
20
27
|
|
|
21
28
|
for (int i = 0; i < 6; i++) {
|
|
22
29
|
vec2 direction = normalize(uWaveDirections[i]);
|
|
23
30
|
vec4 wave = uWaveParams[i];
|
|
24
31
|
float k = wave.x;
|
|
25
|
-
float amplitude = wave.y;
|
|
32
|
+
float amplitude = wave.y * depthDamp;
|
|
26
33
|
float steepness = wave.z;
|
|
27
34
|
float omega = wave.w;
|
|
28
35
|
float phase = k * dot(direction, position.xz) - omega * uTime;
|
|
@@ -41,12 +48,14 @@ void main() {
|
|
|
41
48
|
vNormal = normalize(cross(bitangent, tangent));
|
|
42
49
|
vCrest = crest;
|
|
43
50
|
vWaveHeight = displaced.y;
|
|
51
|
+
vDepth = aDepth;
|
|
44
52
|
vec4 worldPosition = modelMatrix * vec4(displaced, 1.0);
|
|
45
53
|
vWorldPosition = worldPosition.xyz;
|
|
46
54
|
gl_Position = projectionMatrix * viewMatrix * worldPosition;
|
|
47
55
|
}
|
|
48
56
|
`;
|
|
49
57
|
export const oceanFragmentShader = `
|
|
58
|
+
uniform float uTime;
|
|
50
59
|
uniform vec3 uShallowColor;
|
|
51
60
|
uniform vec3 uDeepColor;
|
|
52
61
|
uniform vec3 uCrestColor;
|
|
@@ -55,24 +64,83 @@ uniform float uOpacity;
|
|
|
55
64
|
uniform float uFresnelStrength;
|
|
56
65
|
uniform float uHorizonBlend;
|
|
57
66
|
uniform float uFoamIntensity;
|
|
67
|
+
uniform float uDepthRange;
|
|
68
|
+
uniform float uShoreFoamWidth;
|
|
69
|
+
uniform float uSparkle;
|
|
70
|
+
uniform vec3 uSunDirection;
|
|
58
71
|
|
|
59
72
|
varying vec3 vWorldPosition;
|
|
60
73
|
varying vec3 vNormal;
|
|
61
74
|
varying float vCrest;
|
|
62
75
|
varying float vWaveHeight;
|
|
76
|
+
varying float vDepth;
|
|
77
|
+
varying vec2 vOceanUv;
|
|
78
|
+
|
|
79
|
+
float jgWaterHash(vec2 p) {
|
|
80
|
+
uvec2 q = uvec2(ivec2(floor(p)));
|
|
81
|
+
uint x = q.x * 1664525u + q.y * 1013904223u;
|
|
82
|
+
x = (x ^ (x >> 16u)) * 2246822519u;
|
|
83
|
+
x = (x ^ (x >> 13u)) * 3266489917u;
|
|
84
|
+
return float(x ^ (x >> 16u)) * (1.0 / 4294967296.0);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
float jgWaterNoise(vec2 p) {
|
|
88
|
+
vec2 i = floor(p);
|
|
89
|
+
vec2 f = fract(p);
|
|
90
|
+
vec2 u = f * f * (3.0 - 2.0 * f);
|
|
91
|
+
float a = jgWaterHash(i);
|
|
92
|
+
float b = jgWaterHash(i + vec2(1.0, 0.0));
|
|
93
|
+
float c = jgWaterHash(i + vec2(0.0, 1.0));
|
|
94
|
+
float d = jgWaterHash(i + vec2(1.0, 1.0));
|
|
95
|
+
return mix(mix(a, b, u.x), mix(c, d, u.x), u.y);
|
|
96
|
+
}
|
|
63
97
|
|
|
64
98
|
void main() {
|
|
65
|
-
vec3 normal = normalize(vNormal);
|
|
66
99
|
vec3 viewDirection = normalize(cameraPosition - vWorldPosition);
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
100
|
+
|
|
101
|
+
// Fine scrolling normal perturbation: two counter-drifting noise layers ripple the surface
|
|
102
|
+
// between the big Gerstner swells.
|
|
103
|
+
vec2 wp = vWorldPosition.xz;
|
|
104
|
+
float ripple1 = jgWaterNoise(wp * 1.7 + vec2(uTime * 0.22, uTime * 0.15));
|
|
105
|
+
float ripple2 = jgWaterNoise(wp * 3.1 - vec2(uTime * 0.17, uTime * 0.26));
|
|
106
|
+
vec3 normal = normalize(normalize(vNormal) + vec3(ripple1 - 0.5, 0.0, ripple2 - 0.5) * 0.14);
|
|
107
|
+
|
|
108
|
+
// Depth-based body color: warm shallows at the shore ramp falling to the deep tone.
|
|
109
|
+
float depthMix = smoothstep(0.0, max(uDepthRange, 0.001), vDepth);
|
|
110
|
+
vec3 waterColor = mix(uShallowColor, uDeepColor, depthMix);
|
|
111
|
+
waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.22);
|
|
112
|
+
|
|
113
|
+
// Restrained fresnel: a soft sky tint at grazing angles, never a white-out.
|
|
114
|
+
float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 5.0);
|
|
115
|
+
waterColor = mix(waterColor, vec3(0.62, 0.78, 0.85), min(fresnel * uFresnelStrength, 0.45));
|
|
72
116
|
float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));
|
|
73
|
-
waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend);
|
|
74
|
-
|
|
117
|
+
waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend * 0.6);
|
|
118
|
+
|
|
119
|
+
// Sun glints: sparkle where the perturbed normal mirrors the sun, gated by noise so it
|
|
120
|
+
// twinkles in cells instead of sheening uniformly.
|
|
121
|
+
vec3 halfway = normalize(viewDirection + normalize(uSunDirection));
|
|
122
|
+
float glint = pow(max(dot(normal, halfway), 0.0), 150.0);
|
|
123
|
+
float glintMask = smoothstep(0.68, 0.92, jgWaterNoise(wp * 2.3 + vec2(uTime * 0.4, -uTime * 0.31)));
|
|
124
|
+
waterColor += vec3(1.0, 0.98, 0.9) * glint * glintMask * uSparkle;
|
|
125
|
+
|
|
126
|
+
// Foam: a noise-eaten shoreline lick plus crest foam. A second, larger noise gates whole
|
|
127
|
+
// stretches of shoreline so foam laps in patches instead of outlining the bank uniformly.
|
|
128
|
+
float foamNoise = jgWaterNoise(wp * 2.6 + vec2(uTime * 0.28, uTime * 0.2));
|
|
129
|
+
float foamStretch = smoothstep(0.3, 0.8, jgWaterNoise(wp * 0.33 + vec2(3.7, 9.2) + vec2(uTime * 0.05, 0.0)));
|
|
130
|
+
float shoreFoam = (1.0 - smoothstep(0.0, max(uShoreFoamWidth, 0.001), vDepth - foamNoise * uShoreFoamWidth * 0.9))
|
|
131
|
+
* smoothstep(0.25, 0.75, foamNoise) * (0.2 + 0.6 * foamStretch);
|
|
132
|
+
float crestFoam = clamp(vCrest * uFoamIntensity, 0.0, 1.0) * smoothstep(0.35, 0.75, foamNoise);
|
|
133
|
+
float foam = clamp(shoreFoam + crestFoam, 0.0, 1.0);
|
|
75
134
|
vec3 color = mix(waterColor, uFoamColor, foam);
|
|
76
|
-
|
|
135
|
+
|
|
136
|
+
// Soft shore: the sheet turns glass-clear over the last stretch of shoaling bed, so the
|
|
137
|
+
// waterline is a blend into the terrain, not a hard tarp edge.
|
|
138
|
+
float shoreAlpha = smoothstep(0.0, 0.55, vDepth);
|
|
139
|
+
float alpha = uOpacity * mix(0.35, 1.0, shoreAlpha) + foam * 0.3;
|
|
140
|
+
// The sheet's own border always dissolves — a bounded water plane must never end in a
|
|
141
|
+
// visible straight cut, whatever the bed underneath is doing.
|
|
142
|
+
float borderDist = min(min(vOceanUv.x, 1.0 - vOceanUv.x), min(vOceanUv.y, 1.0 - vOceanUv.y));
|
|
143
|
+
alpha *= smoothstep(0.0, 0.045, borderDist);
|
|
144
|
+
gl_FragColor = vec4(color, clamp(alpha, 0.0, 1.0));
|
|
77
145
|
}
|
|
78
146
|
`;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { PlaceWorldFeature } from "@jgengine/core/world/place";
|
|
2
|
+
/**
|
|
3
|
+
* The substrate of a place world, rendered plainly: a flat slab or a round planet under the engine
|
|
4
|
+
* default sky. This is deliberately not a diorama — sky look, foliage, props, and sculpt are scene
|
|
5
|
+
* content the editor authors; `voxel` grounds are realized by the game's generation systems and
|
|
6
|
+
* `board` grounds by the game's own 2D face, so both mount nothing here.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
9
|
+
export declare function PlaceScene({ feature }: {
|
|
10
|
+
feature: PlaceWorldFeature;
|
|
11
|
+
}): import("react").JSX.Element | null;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { DefaultSurface } from "../render/defaultSurface.js";
|
|
4
|
+
/** Render extent (m) standing in for an `Infinity` flat-ground axis — far past the default fog. */
|
|
5
|
+
const UNBOUNDED_RENDER_EXTENT = 2048;
|
|
6
|
+
const GROUND_COLOR = "#5c6b52";
|
|
7
|
+
function clampExtent(value) {
|
|
8
|
+
return Number.isFinite(value) ? value : UNBOUNDED_RENDER_EXTENT;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The substrate of a place world, rendered plainly: a flat slab or a round planet under the engine
|
|
12
|
+
* default sky. This is deliberately not a diorama — sky look, foliage, props, and sculpt are scene
|
|
13
|
+
* content the editor authors; `voxel` grounds are realized by the game's generation systems and
|
|
14
|
+
* `board` grounds by the game's own 2D face, so both mount nothing here.
|
|
15
|
+
* @internal
|
|
16
|
+
*/
|
|
17
|
+
export function PlaceScene({ feature }) {
|
|
18
|
+
const ground = feature.ground;
|
|
19
|
+
const size = useMemo(() => ground.mode === "flat" ? [clampExtent(ground.size.x), clampExtent(ground.size.z)] : undefined, [ground]);
|
|
20
|
+
if (ground.mode === "flat" && size !== undefined) {
|
|
21
|
+
return (_jsxs("mesh", { "rotation-x": -Math.PI / 2, receiveShadow: true, children: [_jsx("planeGeometry", { args: [size[0], size[1]] }), _jsx(DefaultSurface, { color: GROUND_COLOR, roughness: 0.95 })] }));
|
|
22
|
+
}
|
|
23
|
+
if (ground.mode === "round") {
|
|
24
|
+
return (_jsxs("mesh", { position: [0, -ground.size.radius, 0], receiveShadow: true, children: [_jsx("sphereGeometry", { args: [ground.size.radius, 64, 48] }), _jsx(DefaultSurface, { color: GROUND_COLOR, roughness: 0.95 })] }));
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
package/dist/world/WorldHud.d.ts
CHANGED
|
@@ -23,6 +23,8 @@ export interface WorldNameplatesProps {
|
|
|
23
23
|
maxDistance?: number;
|
|
24
24
|
/** Minimum ms between position/health refreshes — trades smoothness for fewer re-renders at scale. Default 120. */
|
|
25
25
|
tickMs?: number;
|
|
26
|
+
/** Draw the built-in HP bar under the name. Set `false` for a name-only plate when the game already draws its own health bar (its own HUD, or `worldHealthBars`). Default true. */
|
|
27
|
+
showHealth?: boolean;
|
|
26
28
|
className?: string;
|
|
27
29
|
nameplateClassName?: string;
|
|
28
30
|
nameClassName?: string;
|
|
@@ -39,7 +41,7 @@ export interface WorldNameplatesProps {
|
|
|
39
41
|
*
|
|
40
42
|
* @capability nameplates billboarded name + HP bar over nearby entities
|
|
41
43
|
*/
|
|
42
|
-
export declare function WorldNameplates({ statId, height, roles, resolveRole, maxDistance, tickMs, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): import("react").JSX.Element;
|
|
44
|
+
export declare function WorldNameplates({ statId, height, roles, resolveRole, maxDistance, tickMs, showHealth, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }: WorldNameplatesProps): import("react").JSX.Element;
|
|
43
45
|
/** Props for {@link WorldObjectHighlights}. */
|
|
44
46
|
export interface WorldObjectHighlightsProps {
|
|
45
47
|
/** Ring tint. Default a construction-select amber. */
|
package/dist/world/WorldHud.js
CHANGED
|
@@ -50,7 +50,7 @@ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, maxD
|
|
|
50
50
|
*
|
|
51
51
|
* @capability nameplates billboarded name + HP bar over nearby entities
|
|
52
52
|
*/
|
|
53
|
-
export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, tickMs = 120, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }) {
|
|
53
|
+
export function WorldNameplates({ statId = "health", height = 2.3, roles, resolveRole, maxDistance = 40, tickMs = 120, showHealth = true, className, nameplateClassName, nameClassName, barClassName, fillClassName, renderNameplate, }) {
|
|
54
54
|
const ctx = useGameContext();
|
|
55
55
|
const camera = useThree((state) => state.camera);
|
|
56
56
|
const size = useThree((state) => state.size);
|
|
@@ -83,7 +83,7 @@ export function WorldNameplates({ statId = "health", height = 2.3, roles, resolv
|
|
|
83
83
|
color: "#f1f5f9",
|
|
84
84
|
textShadow: "0 1px 3px rgba(0,0,0,0.9)",
|
|
85
85
|
whiteSpace: "nowrap",
|
|
86
|
-
}, children: sample.name }), sample.percent !== null ? (_jsx("span", { className: barClassName, "data-nameplate-bar": true, style: {
|
|
86
|
+
}, children: sample.name }), showHealth && sample.percent !== null ? (_jsx("span", { className: barClassName, "data-nameplate-bar": true, style: {
|
|
87
87
|
display: "block",
|
|
88
88
|
width: 78,
|
|
89
89
|
height: 6,
|
package/dist/world/WorldScene.js
CHANGED
|
@@ -4,10 +4,12 @@ import { useEffect, useMemo, useRef } from "react";
|
|
|
4
4
|
import * as THREE from "three";
|
|
5
5
|
import { objectVisualScale } from "@jgengine/core/scene/objectStore";
|
|
6
6
|
import { WORLD_ITEM_ENTITY_NAME } from "@jgengine/core/game/worldItem";
|
|
7
|
+
import { beginFallbackSeam, endFallbackPass, reportFallbackSeam, } from "@jgengine/core/devtools/fallbackSeams";
|
|
7
8
|
import { useGameContext } from "@jgengine/react/provider";
|
|
8
9
|
import { useSceneEntityIds, useSceneObjectIds, useGameStore, usePlayer, useTarget } from "@jgengine/react/hooks";
|
|
9
10
|
import { colorFromId } from "../worldSky.js";
|
|
10
11
|
import { DefaultSurface, detailMaps } from "../render/defaultSurface.js";
|
|
12
|
+
import { MeasuredBoundsGroup } from "../render/measureBounds.js";
|
|
11
13
|
import { EntitySprite, IsolatedEntityModel } from "../render/SceneModels.js";
|
|
12
14
|
import { resolveModel, resolveEntityModel, tryResolveCatalogModel } from "../render/resolveModel.js";
|
|
13
15
|
import { useRenderVisibility } from "../visibility/CullingProvider.js";
|
|
@@ -40,7 +42,7 @@ function EntityMarker({ entity, custom, model, sprite, isLocal, targeted, select
|
|
|
40
42
|
const live = ctx.scene.entity.get(entityId);
|
|
41
43
|
if (live !== null)
|
|
42
44
|
onSelect(live);
|
|
43
|
-
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: entityId, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, castShadow: true, receiveShadow: true, children: [_jsx("sphereGeometry", { args: [0.45, 24, 24] }), _jsx(DefaultSurface, { color: color })] })) : (_jsxs("group", { scale: ctx.scene.entity.visualScaleOf(entityId), children: [_jsxs("mesh", { "position-y": 0.95, castShadow: true, receiveShadow: true, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx(DefaultSurface, { color: color, roughness: 0.6 })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
|
|
45
|
+
}, children: [selected ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.02, children: [_jsx("ringGeometry", { args: [0.8, 0.95, 32] }), _jsx("meshBasicMaterial", { color: "#34d399", transparent: true, opacity: 0.9 })] })) : null, custom !== undefined && custom !== null ? (_jsx(MeasuredBoundsGroup, { target: "entity", measureKey: name, children: custom })) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: entityId, measure: { target: "entity", key: name }, fallback: sprite !== undefined ? _jsx(EntitySprite, { sprite: sprite }) : undefined })) : sprite !== undefined ? (_jsx(EntitySprite, { sprite: sprite })) : role === "prop" ? (_jsxs("mesh", { "position-y": 0.5, castShadow: true, receiveShadow: true, children: [_jsx("sphereGeometry", { args: [0.45, 24, 24] }), _jsx(DefaultSurface, { color: color })] })) : (_jsxs("group", { scale: ctx.scene.entity.visualScaleOf(entityId), children: [_jsxs("mesh", { "position-y": 0.95, castShadow: true, receiveShadow: true, children: [_jsx("capsuleGeometry", { args: [0.35, 1.1, 6, 14] }), _jsx(DefaultSurface, { color: color, roughness: 0.6 })] }), _jsxs("mesh", { position: [0, 1.35, 0.32], children: [_jsx("boxGeometry", { args: [0.16, 0.16, 0.16] }), _jsx("meshStandardMaterial", { color: "#f8fafc" })] })] })), targeted ? (_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.6, 0.75, 28] }), _jsx("meshBasicMaterial", { color: "#f87171" })] })) : null] }));
|
|
44
46
|
}
|
|
45
47
|
function ObjectMarker({ object, custom, model, style, }) {
|
|
46
48
|
const groupRef = useRef(null);
|
|
@@ -60,7 +62,7 @@ function ObjectMarker({ object, custom, model, style, }) {
|
|
|
60
62
|
writeEntityPose(group, live);
|
|
61
63
|
group.visible = visibleRef.current(instanceId);
|
|
62
64
|
});
|
|
63
|
-
return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (custom) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: instanceId })) : style?.hidden === true ? null : (_jsxs("mesh", { "position-y": 0.5 * scaleY, scale: [scaleX, scaleY, scaleZ], castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx(DefaultSurface, { color: color, transparent: opacity < 1, opacity: opacity })] })) }));
|
|
65
|
+
return (_jsx("group", { ref: groupRef, userData: { [POINTER_OBJECT_KEY]: instanceId }, children: custom !== undefined && custom !== null ? (_jsx(MeasuredBoundsGroup, { target: "object", measureKey: object.catalogId, children: custom })) : model !== undefined ? (_jsx(IsolatedEntityModel, { model: model, instanceId: instanceId, measure: { target: "object", key: object.catalogId } })) : style?.hidden === true ? null : (_jsxs("mesh", { "position-y": 0.5 * scaleY, scale: [scaleX, scaleY, scaleZ], castShadow: true, receiveShadow: true, children: [_jsx("boxGeometry", { args: [1, 1, 1] }), _jsx(DefaultSurface, { color: color, transparent: opacity < 1, opacity: opacity })] })) }));
|
|
64
66
|
}
|
|
65
67
|
function GroundPlane() {
|
|
66
68
|
const geometry = useMemo(() => {
|
|
@@ -118,8 +120,12 @@ function RockField() {
|
|
|
118
120
|
return (_jsx(_Fragment, { children: rocks.map((rock) => (_jsxs("mesh", { position: [rock.x, 0.25 * rock.scale, rock.z], rotation: [0.1, rock.rotation, -0.08], scale: [rock.scale * 1.4, rock.scale * 0.7, rock.scale], castShadow: true, receiveShadow: true, children: [_jsx("dodecahedronGeometry", { args: [0.8, 1] }), _jsx(DefaultSurface, { color: "#6b6f63", roughness: 0.95 })] }, rock.id))) }));
|
|
119
121
|
}
|
|
120
122
|
function WorldEnvironment({ environment: Environment }) {
|
|
123
|
+
// Dev diagnostic (no-op unless armed): a missing environment component means the default green
|
|
124
|
+
// ground/rocks stand in for an unauthored scene. Re-zero then report so this is idempotent.
|
|
125
|
+
beginFallbackSeam("ground");
|
|
121
126
|
if (Environment !== undefined)
|
|
122
127
|
return _jsx(Environment, {});
|
|
128
|
+
reportFallbackSeam("ground", "noScene");
|
|
123
129
|
return (_jsxs(_Fragment, { children: [_jsx(GroundPlane, {}), _jsx(RockField, {})] }));
|
|
124
130
|
}
|
|
125
131
|
function WorldActors({ entitySprites, entityModels, objectModels, objectStyles, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
|
@@ -133,22 +139,44 @@ function WorldActors({ entitySprites, entityModels, objectModels, objectStyles,
|
|
|
133
139
|
const relation = ctx.scene.entity.canReceive(entity.id, "damage") === null ? "hostile" : "friendly";
|
|
134
140
|
ctx.scene.entity.setTarget(controlledId, relation === "hostile" || entity.role === "npc" ? entity.id : null);
|
|
135
141
|
};
|
|
142
|
+
// Dev diagnostic (no-op unless armed): re-zero the entity/object seams before this render re-tallies
|
|
143
|
+
// which actors fall back to primitives. Committing the log runs in a useEffect after the render.
|
|
144
|
+
beginFallbackSeam("entity");
|
|
145
|
+
beginFallbackSeam("object");
|
|
146
|
+
useEffect(() => {
|
|
147
|
+
endFallbackPass();
|
|
148
|
+
});
|
|
136
149
|
return (_jsxs(_Fragment, { children: [entityIds.map((entityId) => {
|
|
137
150
|
const entity = ctx.scene.entity.get(entityId);
|
|
138
151
|
if (entity === null || entity.name === WORLD_ITEM_ENTITY_NAME)
|
|
139
152
|
return null;
|
|
140
153
|
if (hideLocalActor && entityId === controlledId)
|
|
141
154
|
return null;
|
|
142
|
-
|
|
155
|
+
const custom = renderEntity?.(entity);
|
|
156
|
+
const model = resolveEntityModel(entityModels?.[entity.name], assets, entity.name);
|
|
157
|
+
const sprite = entitySprites?.[entity.name];
|
|
158
|
+
// Entity models resolve via the hard resolveModel (throws on a present-but-missing id), so
|
|
159
|
+
// reaching the primitive capsule with no model or sprite means the mapping was omitted.
|
|
160
|
+
if ((custom === undefined || custom === null) && model === undefined && sprite === undefined) {
|
|
161
|
+
reportFallbackSeam("entity", "omittedMapping");
|
|
162
|
+
}
|
|
163
|
+
return (_jsx(EntityMarker, { entity: entity, custom: custom, model: model, sprite: sprite, isLocal: entityId === controlledId, targeted: entityId === targetId, selected: selectedIds.has(entityId), onSelect: handleSelect }, entityId));
|
|
143
164
|
}), objectIds.map((instanceId) => {
|
|
144
165
|
const object = ctx.scene.object.get(instanceId);
|
|
145
166
|
if (object === null)
|
|
146
167
|
return null;
|
|
168
|
+
const custom = renderObject?.(object);
|
|
147
169
|
const model = resolveModel(objectModels?.[object.catalogId], assets, {
|
|
148
170
|
seam: "objectModels",
|
|
149
171
|
key: object.catalogId,
|
|
150
172
|
}) ?? tryResolveCatalogModel(object.catalogId, assets);
|
|
151
|
-
|
|
173
|
+
const style = objectStyles?.[object.catalogId];
|
|
174
|
+
// Reaching the primitive box means no model resolved. A present-but-unresolved objectModels
|
|
175
|
+
// key implies its asset pack is not pulled; an absent key is an omitted (often intended) mapping.
|
|
176
|
+
if ((custom === undefined || custom === null) && model === undefined && style?.hidden !== true) {
|
|
177
|
+
reportFallbackSeam("object", objectModels?.[object.catalogId] === undefined ? "omittedMapping" : "unpulledPack");
|
|
178
|
+
}
|
|
179
|
+
return (_jsx(ObjectMarker, { object: object, custom: custom, model: model, style: style }, instanceId));
|
|
152
180
|
})] }));
|
|
153
181
|
}
|
|
154
182
|
export function WorldView({ entitySprites, entityModels, objectModels, objectStyles, environment, assets, renderEntity, renderObject, selectedIds, hideLocalActor, }) {
|
package/dist/worldSky.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { SkyEnvironmentDescriptor, WorldFeature } from "@jgengine/core/world/features";
|
|
2
|
-
/**
|
|
2
|
+
/**
|
|
3
|
+
* The sky in effect for a world feature (#196.1). Environments answer their declared sky; place
|
|
4
|
+
* worlds (`world()` — substrate + laws, no sky fields) get the engine default sky for 3D grounds,
|
|
5
|
+
* while `board` grounds get none (the game owns the 2D face it draws). An authored scene document's
|
|
6
|
+
* sky arrives through `backdrop`/`lighting`, which wins over this at the presentation layer.
|
|
7
|
+
* @internal
|
|
8
|
+
*/
|
|
3
9
|
export declare function resolveWorldSky(world: WorldFeature | undefined): SkyEnvironmentDescriptor | undefined;
|
|
4
10
|
/** Deterministic HSL color from a stable instance id (default entity/object tint). @internal */
|
|
5
11
|
export declare function colorFromId(id: string): string;
|
package/dist/worldSky.js
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
|
-
/** The
|
|
1
|
+
/** The engine default sky a 3D place world renders when no sky is authored in its scene document. */
|
|
2
|
+
const PLACE_DEFAULT_SKY = { kind: "sky", preset: "day", timeOfDay: false };
|
|
3
|
+
/**
|
|
4
|
+
* The sky in effect for a world feature (#196.1). Environments answer their declared sky; place
|
|
5
|
+
* worlds (`world()` — substrate + laws, no sky fields) get the engine default sky for 3D grounds,
|
|
6
|
+
* while `board` grounds get none (the game owns the 2D face it draws). An authored scene document's
|
|
7
|
+
* sky arrives through `backdrop`/`lighting`, which wins over this at the presentation layer.
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
2
10
|
export function resolveWorldSky(world) {
|
|
11
|
+
if (world?.kind === "place")
|
|
12
|
+
return world.ground.mode === "board" ? undefined : PLACE_DEFAULT_SKY;
|
|
3
13
|
return world?.kind === "environment" ? world.sky : undefined;
|
|
4
14
|
}
|
|
5
15
|
/** Deterministic HSL color from a stable instance id (default entity/object tint). @internal */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jgengine/shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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.13.0",
|
|
39
|
+
"@jgengine/react": "^0.13.0",
|
|
40
|
+
"@jgengine/ws": "^0.13.0"
|
|
41
41
|
},
|
|
42
42
|
"peerDependencies": {
|
|
43
43
|
"@react-three/drei": "^10.0.0",
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: game-design
|
|
3
|
+
description: Design, audit, implement, and validate player experience, loops, systems, progression, and balance.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Game design
|
|
7
|
+
|
|
8
|
+
## Ownership
|
|
9
|
+
|
|
10
|
+
Own the intended player experience and the rules that produce it: fantasy, pillars, verbs, decisions, feedback, loops, resources, progression, challenge, failure, onboarding, and social dynamics. Treat design claims as hypotheses proved by play, not a feature inventory or genre imitation — no genre kits, presets, or archetypes; every game is a custom composition of needs (see [AGENTS.md](../../../AGENTS.md)).
|
|
11
|
+
|
|
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
|
+
|
|
14
|
+
Read [references/application-playbook.md](references/application-playbook.md) only when the task explicitly calls for design depth — a design brief, a redesign, or "make this fun/better" work; it is not a mandatory step of creating a game. Read [references/field-guide.md](references/field-guide.md) when the task needs deeper loop, economy, progression, genre, accessibility, or research guidance.
|
|
15
|
+
|
|
16
|
+
## Choose the mode
|
|
17
|
+
|
|
18
|
+
Greenfield: produce a vertical-slice contract (promise, pillars, core decision, loops, progression, failure/recovery) and build the smallest slice testing the riskiest assumption — see playbook §1. Existing game: reconstruct what it actually does from code and play, rank root problems by impact/confidence/reach/effort — see playbook §2.
|
|
19
|
+
|
|
20
|
+
“Improve with game design” is an implementation request, not just an audit: select the highest-leverage intervention, implement it through shared engine seams, and prove the changed player behavior.
|
|
21
|
+
|
|
22
|
+
## Required output contract
|
|
23
|
+
|
|
24
|
+
Every design pass yields an observed journey, a north star (promise, pillars, anti-pillars, core decision), a loop/flow model, a ranked bottleneck list with cited evidence, an intervention stack, a chosen design slice, and acceptance evidence. See [references/application-playbook.md](references/application-playbook.md) for the full contract and execution rules (verb->feedback->consequence chains, loop/progression/challenge discipline, upstream ownership).
|
|
25
|
+
|
|
26
|
+
## Completion
|
|
27
|
+
|
|
28
|
+
An audit is not completion for a build/improve request. Finish when the slice is playable, a fresh player can state goal/choice/consequence, failure/recovery behave as intended, and evidence supports the pillars. Use `jgengine-verify` and `workflow` to ship.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Applying game design
|
|
2
|
+
|
|
3
|
+
Use this playbook to turn a pitch or an existing game into implemented, testable change. Its purpose is to prevent “design work” from ending as generic advice.
|
|
4
|
+
|
|
5
|
+
## Contents
|
|
6
|
+
|
|
7
|
+
1. Greenfield procedure
|
|
8
|
+
2. Existing-game audit
|
|
9
|
+
3. Mandatory models
|
|
10
|
+
4. Ranking interventions
|
|
11
|
+
5. Design-slice specification
|
|
12
|
+
6. Implementation and proof
|
|
13
|
+
7. Failure modes
|
|
14
|
+
|
|
15
|
+
## 1. Greenfield procedure
|
|
16
|
+
|
|
17
|
+
### Frame the bet
|
|
18
|
+
|
|
19
|
+
Write a compact contract before broad code or content:
|
|
20
|
+
|
|
21
|
+
| Field | Required form |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| audience/context | prior skills, player count, platform/input, session length, accessibility assumptions |
|
|
24
|
+
| player promise | “The player repeatedly [verb/decision] so they feel [specific experience]” |
|
|
25
|
+
| pillars | two or three observable behaviors or feelings |
|
|
26
|
+
| anti-pillars | experiences the game must not drift toward |
|
|
27
|
+
| core decision | situation, known information, viable options, tradeoff, consequence, recurrence |
|
|
28
|
+
| completion proof | one end-to-end scenario a target player can perform and explain |
|
|
29
|
+
| falsifier | observation that would show the design does not work |
|
|
30
|
+
|
|
31
|
+
### Build the minimum design
|
|
32
|
+
|
|
33
|
+
1. Model each core verb from cue through future consequence.
|
|
34
|
+
2. Draw moment, encounter, session, and meta loops.
|
|
35
|
+
3. Draw resources and feedback structures.
|
|
36
|
+
4. Map player skills and progression channels.
|
|
37
|
+
5. Define failure, recovery, stopping, and replay.
|
|
38
|
+
6. Create a content grammar: a small set of mechanics crossed with contexts and constraints.
|
|
39
|
+
7. Select the riskiest assumption and build one vertical design slice around it.
|
|
40
|
+
|
|
41
|
+
Do not build the entire feature list first. A vertical slice contains real input, meaningful state, feedback, one decision that recurs, failure/recovery, and a downstream consequence.
|
|
42
|
+
|
|
43
|
+
## 2. Existing-game audit
|
|
44
|
+
|
|
45
|
+
### Reconstruct reality
|
|
46
|
+
|
|
47
|
+
Inspect actual code, catalogs, scene documents, UI, tests, captures, and live play. Do not infer the game from its title or README.
|
|
48
|
+
|
|
49
|
+
Trace four journeys when applicable:
|
|
50
|
+
|
|
51
|
+
- **first minute:** first input, goal, threat, reward, confusion, and reason to continue
|
|
52
|
+
- **first objective:** learning sequence, decisions, failure, recovery, payoff, and next goal
|
|
53
|
+
- **representative session:** loop renewal, variety, resource pressure, build choices, pacing, and stopping point
|
|
54
|
+
- **campaign/meta:** unlock cadence, content reuse, escalation, economy stability, mastery, and ending/replay
|
|
55
|
+
|
|
56
|
+
For each journey, record timestamps or event order:
|
|
57
|
+
|
|
58
|
+
`cue -> player belief -> action -> system response -> feedback -> state change -> next choice`
|
|
59
|
+
|
|
60
|
+
Mark dead time, repeated decisions, forced actions, hidden consequences, false affordances, unexplained failure, and rewards that do not alter later play.
|
|
61
|
+
|
|
62
|
+
### Build a system-coupling map
|
|
63
|
+
|
|
64
|
+
Inventory verbs, enemies/problems, items/tools, progression, economy, quests/objectives, world states, social systems, and failure. For each system ask:
|
|
65
|
+
|
|
66
|
+
- Which pillar does it serve?
|
|
67
|
+
- Which loop consumes its output?
|
|
68
|
+
- What player decision does it create or transform?
|
|
69
|
+
- Where is it taught, tested, combined, and mastered?
|
|
70
|
+
- What happens if it is removed?
|
|
71
|
+
|
|
72
|
+
Flag systems that are present but disconnected, content whose only variation is numbers or nouns, and rewards that bypass the core play.
|
|
73
|
+
|
|
74
|
+
### Diagnose decision density
|
|
75
|
+
|
|
76
|
+
Do not maximize decisions per second. Find long spans where the player executes an already-solved action without new information, tradeoffs, expression, pressure, or mastery. Measure:
|
|
77
|
+
|
|
78
|
+
- time to first meaningful decision
|
|
79
|
+
- meaningful decisions per encounter/objective
|
|
80
|
+
- time between new information and commitment
|
|
81
|
+
- percentage of objectives using each verb or decision type
|
|
82
|
+
- repeated content before context changes
|
|
83
|
+
- time from failure to a useful retry
|
|
84
|
+
|
|
85
|
+
## 3. Mandatory models
|
|
86
|
+
|
|
87
|
+
### Loop table
|
|
88
|
+
|
|
89
|
+
| Horizon | Cue/goal | Decision/action | Feedback | State changed | Why repeat? | Failure risk |
|
|
90
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
91
|
+
|
|
92
|
+
Reject any row whose “state changed” or “why repeat” is empty.
|
|
93
|
+
|
|
94
|
+
### Resource-flow table
|
|
95
|
+
|
|
96
|
+
| Resource | Source | Store/cap | Spend/convert | Sink | Player decision | Feedback/runaway risk |
|
|
97
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
98
|
+
|
|
99
|
+
Include time, health, space, information, attention, action opportunities, and social trust when relevant.
|
|
100
|
+
|
|
101
|
+
### Progression table
|
|
102
|
+
|
|
103
|
+
| Unlock/reward | Channel | New decision or capability | Content it recontextualizes | Risk of invalidation/grind | Proof |
|
|
104
|
+
| --- | --- | --- | --- | --- | --- |
|
|
105
|
+
|
|
106
|
+
Channels are mastery, access, breadth, power, and expression/status. Passive percentages without a changed decision must justify their place.
|
|
107
|
+
|
|
108
|
+
### Objective/encounter variety matrix
|
|
109
|
+
|
|
110
|
+
Cross objective verbs with information state, constraints, enemy/problem behavior, topology, resource pressure, failure condition, and consequence. This exposes campaigns where every mission is the same verb with different nouns.
|
|
111
|
+
|
|
112
|
+
## 4. Ranking interventions
|
|
113
|
+
|
|
114
|
+
Separate symptoms from root causes. “Quests are boring” is a symptom. “Every quest uses the same kill-count transition and no objective changes space, resources, or strategy” is a testable cause.
|
|
115
|
+
|
|
116
|
+
Rank each candidate:
|
|
117
|
+
|
|
118
|
+
| Candidate | Player behavior changed | Pillar impact | Reach | Evidence/confidence | Effort/risk | Dependencies | Shared owner |
|
|
119
|
+
| --- | --- | --- | --- | --- | --- | --- | --- |
|
|
120
|
+
|
|
121
|
+
Use three horizons:
|
|
122
|
+
|
|
123
|
+
- **Minimum correction:** remove the largest contradiction or unblock the loop.
|
|
124
|
+
- **Strong target:** make one representative slice deliver the promise end to end.
|
|
125
|
+
- **Multiplier:** expand the proven grammar across content, progression, social play, or replay.
|
|
126
|
+
|
|
127
|
+
Choose by leverage, not novelty. Prefer a change that improves many encounters or future games over bespoke content with the same underlying decision.
|
|
128
|
+
|
|
129
|
+
## 5. Design-slice specification
|
|
130
|
+
|
|
131
|
+
The selected slice must state:
|
|
132
|
+
|
|
133
|
+
- before behavior and evidence
|
|
134
|
+
- target behavior and player belief
|
|
135
|
+
- mechanics/dynamics expected to cause the change
|
|
136
|
+
- objective/encounter sequence and pacing beats
|
|
137
|
+
- resources, rewards, failure, recovery, and downstream consequence
|
|
138
|
+
- affected catalogs/config, shared package seams, scene semantics, UI feedback, and tests
|
|
139
|
+
- tuning variables with starting hypotheses and safe bounds
|
|
140
|
+
- explicit non-goals and content held constant
|
|
141
|
+
- acceptance scenario and falsifiers
|
|
142
|
+
|
|
143
|
+
If another game could use the mechanic, define the package primitive first and adopt it here. Keep game-local code for content and feel.
|
|
144
|
+
|
|
145
|
+
## 6. Implementation and proof
|
|
146
|
+
|
|
147
|
+
When the user asks to build or improve:
|
|
148
|
+
|
|
149
|
+
1. Implement the shared primitive or use an existing capability.
|
|
150
|
+
2. Adopt it in one complete game slice.
|
|
151
|
+
3. Author spatial content through `level-design` and the editor.
|
|
152
|
+
4. Add deterministic tests for transitions, resources, failure, and progression.
|
|
153
|
+
5. Exercise the observable scenario with representative input.
|
|
154
|
+
6. Capture before/after evidence for behavior, not only appearance.
|
|
155
|
+
7. Evaluate the falsifier honestly; revise or remove a failed intervention.
|
|
156
|
+
|
|
157
|
+
Useful evidence includes time to first decision, objective completion, route choice, build choice, resource state, failure cause, retry time, objective-verb distribution, and a player’s ability to explain goal/action/consequence. Metrics need a named hypothesis; do not optimize them in isolation.
|
|
158
|
+
|
|
159
|
+
## 7. Failure modes
|
|
160
|
+
|
|
161
|
+
- producing a long GDD instead of a playable slice
|
|
162
|
+
- listing features without naming changed decisions
|
|
163
|
+
- adding content to a shallow objective grammar
|
|
164
|
+
- using passive progression to disguise unchanged play
|
|
165
|
+
- copying a genre’s surface without its causal dynamics
|
|
166
|
+
- treating more rewards as more motivation
|
|
167
|
+
- fixing comprehension only with text or HUD
|
|
168
|
+
- reporting a diagnosis after the user asked for improvement
|
|
169
|
+
- calling deterministic tests proof that the experience works
|
|
170
|
+
- calling a prettier screenshot proof that the loop works
|