@jgengine/shell 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.md +26 -1
  3. package/dist/GameHost.d.ts +12 -0
  4. package/dist/GameHost.js +33 -0
  5. package/dist/GamePlayer.d.ts +11 -0
  6. package/dist/GamePlayer.js +24 -0
  7. package/dist/GamePlayerShell.d.ts +33 -1
  8. package/dist/GamePlayerShell.js +439 -28
  9. package/dist/behaviour.d.ts +31 -0
  10. package/dist/behaviour.js +63 -0
  11. package/dist/camera/GameCameraRig.d.ts +8 -3
  12. package/dist/camera/GameCameraRig.js +28 -14
  13. package/dist/camera/GameFirstPersonCamera.js +2 -0
  14. package/dist/camera/GameInspectionCamera.d.ts +6 -0
  15. package/dist/camera/GameInspectionCamera.js +24 -0
  16. package/dist/camera/cameraRigs.d.ts +5 -1
  17. package/dist/camera/cameraRigs.js +43 -6
  18. package/dist/camera/index.d.ts +4 -2
  19. package/dist/camera/index.js +4 -2
  20. package/dist/camera/inspectionCameraMath.d.ts +25 -0
  21. package/dist/camera/inspectionCameraMath.js +44 -0
  22. package/dist/camera/rigMath.d.ts +40 -1
  23. package/dist/camera/rigMath.js +55 -0
  24. package/dist/camera/rigResolve.d.ts +15 -0
  25. package/dist/camera/rigResolve.js +52 -0
  26. package/dist/defineGame.d.ts +16 -0
  27. package/dist/defineGame.js +51 -0
  28. package/dist/devtools/DevtoolsOverlay.d.ts +14 -0
  29. package/dist/devtools/DevtoolsOverlay.js +334 -0
  30. package/dist/environment/Daylight.d.ts +50 -0
  31. package/dist/environment/Daylight.js +100 -0
  32. package/dist/environment/EnvironmentScene.js +18 -26
  33. package/dist/environment/GroundPad.d.ts +7 -0
  34. package/dist/environment/GroundPad.js +12 -0
  35. package/dist/environment/daylightCycle.d.ts +26 -0
  36. package/dist/environment/daylightCycle.js +116 -0
  37. package/dist/environment/groundPadMath.d.ts +13 -0
  38. package/dist/environment/groundPadMath.js +10 -0
  39. package/dist/environment/index.d.ts +2 -0
  40. package/dist/environment/index.js +2 -0
  41. package/dist/materialOverride.d.ts +8 -0
  42. package/dist/materialOverride.js +32 -0
  43. package/dist/multiplayer.d.ts +16 -9
  44. package/dist/multiplayer.js +62 -12
  45. package/dist/pointer/PointerProbe.js +14 -1
  46. package/dist/pointer/pointerService.d.ts +2 -0
  47. package/dist/pointer/pointerService.js +45 -24
  48. package/dist/registry.d.ts +4 -1
  49. package/dist/registry.js +3 -1
  50. package/dist/render/modelRender.d.ts +14 -0
  51. package/dist/render/modelRender.js +79 -0
  52. package/dist/terrain/index.d.ts +1 -1
  53. package/dist/terrain/index.js +1 -1
  54. package/dist/terrain/terrainMath.d.ts +1 -0
  55. package/dist/terrain/terrainMath.js +8 -2
  56. package/dist/touch/TouchControlsOverlay.d.ts +18 -0
  57. package/dist/touch/TouchControlsOverlay.js +173 -0
  58. package/dist/water/OceanShader.d.ts +1 -1
  59. package/dist/water/OceanShader.js +3 -3
  60. package/dist/world/DataObjects.d.ts +44 -0
  61. package/dist/world/DataObjects.js +75 -0
  62. package/dist/world/GridWorldScene.d.ts +10 -0
  63. package/dist/world/GridWorldScene.js +54 -0
  64. package/dist/world/WorldHud.d.ts +5 -1
  65. package/dist/world/WorldHud.js +6 -2
  66. package/dist/world/WorldItems.js +21 -2
  67. package/llms.txt +2382 -0
  68. package/package.json +6 -5
  69. package/dist/demo/builderDemo.d.ts +0 -2
  70. package/dist/demo/builderDemo.js +0 -189
  71. package/dist/demo/demoGame.d.ts +0 -2
  72. package/dist/demo/demoGame.js +0 -208
  73. package/dist/demo/environmentShowcase.d.ts +0 -2
  74. package/dist/demo/environmentShowcase.js +0 -15
  75. package/dist/demo/mapDemo.d.ts +0 -2
  76. package/dist/demo/mapDemo.js +0 -206
  77. package/dist/demo/pointerDemo.d.ts +0 -2
  78. package/dist/demo/pointerDemo.js +0 -151
  79. package/dist/demo/sensorShowcase.d.ts +0 -2
  80. package/dist/demo/sensorShowcase.js +0 -131
  81. package/dist/demo/survivalDemo.d.ts +0 -2
  82. package/dist/demo/survivalDemo.js +0 -291
@@ -0,0 +1,79 @@
1
+ import * as THREE from "three";
2
+ import { clone as cloneSkinned } from "three/examples/jsm/utils/SkeletonUtils.js";
3
+ export const PAINT_TEXTURE_SIZE = 512;
4
+ export function cloneModelScene(source) {
5
+ const clone = cloneSkinned(source);
6
+ clone.traverse((node) => {
7
+ const mesh = node;
8
+ if (!mesh.isMesh)
9
+ return;
10
+ mesh.material = Array.isArray(mesh.material)
11
+ ? mesh.material.map((material) => material.clone())
12
+ : mesh.material.clone();
13
+ });
14
+ return clone;
15
+ }
16
+ function isMeshStandardMaterial(material) {
17
+ return material.isMeshStandardMaterial === true;
18
+ }
19
+ export function standardMaterialsOf(root) {
20
+ const materials = [];
21
+ root.traverse((node) => {
22
+ const mesh = node;
23
+ if (!mesh.isMesh)
24
+ return;
25
+ const list = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
26
+ for (const material of list)
27
+ if (isMeshStandardMaterial(material))
28
+ materials.push(material);
29
+ });
30
+ return materials;
31
+ }
32
+ export function createPaintCanvas(seed, size = PAINT_TEXTURE_SIZE) {
33
+ const canvas = document.createElement("canvas");
34
+ canvas.width = size;
35
+ canvas.height = size;
36
+ const context = canvas.getContext("2d");
37
+ const seedImage = seed.map?.image;
38
+ if (seedImage !== undefined && seedImage.width) {
39
+ context.drawImage(seedImage, 0, 0, size, size);
40
+ }
41
+ else {
42
+ context.fillStyle = `#${seed.color.getHexString()}`;
43
+ context.fillRect(0, 0, size, size);
44
+ }
45
+ const texture = new THREE.CanvasTexture(canvas);
46
+ texture.colorSpace = seed.map?.colorSpace ?? THREE.SRGBColorSpace;
47
+ return { canvas, context, texture };
48
+ }
49
+ export function drawPaintStrokes(paint, strokes) {
50
+ const { canvas, context, texture } = paint;
51
+ for (const stroke of strokes) {
52
+ context.fillStyle = stroke.color;
53
+ context.beginPath();
54
+ context.arc(stroke.u * canvas.width, (1 - stroke.v) * canvas.height, stroke.radius * canvas.width, 0, Math.PI * 2);
55
+ context.fill();
56
+ }
57
+ texture.needsUpdate = true;
58
+ }
59
+ export function applyPaintTexture(root, paint) {
60
+ for (const material of standardMaterialsOf(root)) {
61
+ material.map = paint.texture;
62
+ material.needsUpdate = true;
63
+ }
64
+ }
65
+ export function syncPaintCanvas(paint, seedColor, strokes, drawnCount) {
66
+ if (strokes.length < drawnCount) {
67
+ const { canvas, context } = paint;
68
+ context.clearRect(0, 0, canvas.width, canvas.height);
69
+ context.fillStyle = `#${seedColor.getHexString()}`;
70
+ context.fillRect(0, 0, canvas.width, canvas.height);
71
+ drawPaintStrokes(paint, strokes);
72
+ return strokes.length;
73
+ }
74
+ if (strokes.length > drawnCount) {
75
+ drawPaintStrokes(paint, strokes.slice(drawnCount));
76
+ return strokes.length;
77
+ }
78
+ return drawnCount;
79
+ }
@@ -6,5 +6,5 @@ export { TerraformBrushCursor, type TerraformBrushCursorProps } from "./Terrafor
6
6
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, type GrassBladeGeometryOptions, type GrassRange, type ResolvedGrassBladeGeometryOptions, } from "./grassGeometry.js";
7
7
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, type GrassMaterialHandle, type GrassMaterialOptions, type GrassShaderUniforms, type GrassWindOptions, } from "./grassMaterial.js";
8
8
  export { createSeededRandom, hashNoise2, seedToUint32, type TerrainSeed } from "./random.js";
9
- export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
9
+ export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, type FieldGroundOptions, type ProceduralTerrainConfig, type ResolvedTerrainSegments, type ResolvedTerrainSize, type TerrainArea, type TerrainHeightSampler, type TerrainVertexColorOptions, } from "./terrainMath.js";
10
10
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, type FractalNoiseConfig, type NoiseFieldConfig, type TerrainField, type TerrainNormal, } from "@jgengine/core/world/terrain";
@@ -6,5 +6,5 @@ export { TerraformBrushCursor } from "./TerraformBrushCursor.js";
6
6
  export { createGrassBladeGeometry, resolveGrassBladeGeometryOptions, resolveGrassRange, } from "./grassGeometry.js";
7
7
  export { createGrassMaterial, DEFAULT_GRASS_WIND, resolveGrassWind, } from "./grassMaterial.js";
8
8
  export { createSeededRandom, hashNoise2, seedToUint32 } from "./random.js";
9
- export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
9
+ export { createFieldGroundGeometry, createProceduralGroundGeometry, createProceduralTerrainSampler, normalizeHeightBlend, resolveTerrainSegments, resolveTerrainSize, toNoiseFieldConfig, } from "./terrainMath.js";
10
10
  export { arenaField, flatField, fractalNoise, noiseField, resolveGroundStep, resolveTerrainField, valueNoise, withNormal, } from "@jgengine/core/world/terrain";
@@ -27,6 +27,7 @@ export interface TerrainVertexColorOptions {
27
27
  waterline?: THREE.ColorRepresentation;
28
28
  waterlineHeight?: number;
29
29
  }
30
+ export declare function normalizeHeightBlend(height: number, minHeight: number, maxHeight: number): number;
30
31
  export declare function resolveTerrainSize(size?: TerrainArea): ResolvedTerrainSize;
31
32
  export declare function resolveTerrainSegments(segments?: ProceduralTerrainConfig["segments"]): ResolvedTerrainSegments;
32
33
  export declare function toNoiseFieldConfig(config?: ProceduralTerrainConfig): NoiseFieldConfig;
@@ -1,6 +1,12 @@
1
1
  import { noiseField } from "@jgengine/core/world/terrain";
2
2
  import * as THREE from "three";
3
3
  import {} from "./random.js";
4
+ export function normalizeHeightBlend(height, minHeight, maxHeight) {
5
+ const range = maxHeight - minHeight;
6
+ if (range <= 1e-6)
7
+ return 0.5;
8
+ return THREE.MathUtils.clamp((height - minHeight) / range, 0, 1);
9
+ }
4
10
  export function resolveTerrainSize(size = 40) {
5
11
  return typeof size === "number" ? { width: size, depth: size } : { width: size[0], depth: size[1] };
6
12
  }
@@ -66,7 +72,7 @@ function buildGroundGeometry(size, segments, sampler, opts) {
66
72
  positions[index + 2] = z;
67
73
  uvs[uvIndex] = u;
68
74
  uvs[uvIndex + 1] = v;
69
- const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
75
+ const blend = normalizeHeightBlend(y, minHeight, maxHeight);
70
76
  const color = low.clone().lerp(high, blend);
71
77
  if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
72
78
  color.lerp(waterline, 0.65);
@@ -133,7 +139,7 @@ export function createProceduralGroundGeometry(config = {}, colors = {}) {
133
139
  positions[index + 2] = z;
134
140
  uvs[uvIndex] = u;
135
141
  uvs[uvIndex + 1] = v;
136
- const blend = THREE.MathUtils.clamp((y - minHeight) / (maxHeight - minHeight), 0, 1);
142
+ const blend = normalizeHeightBlend(y, minHeight, maxHeight);
137
143
  const color = low.clone().lerp(high, blend);
138
144
  if (waterline !== null && y <= (colors.waterlineHeight ?? 0))
139
145
  color.lerp(waterline, 0.65);
@@ -0,0 +1,18 @@
1
+ import { type MutableRefObject } from "react";
2
+ import { type TouchScheme } from "@jgengine/core/input/touchScheme";
3
+ export interface TouchCodeSink {
4
+ onCodeDown(code: string): void;
5
+ onCodeUp(code: string): void;
6
+ }
7
+ export declare function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }: {
8
+ scheme: TouchScheme;
9
+ sink: TouchCodeSink;
10
+ yawRef: MutableRefObject<number>;
11
+ pitchRef: MutableRefObject<number>;
12
+ maxPitch: number;
13
+ onPrimaryTap: () => void;
14
+ }): import("react").JSX.Element;
15
+ export declare function TouchControlsDock({ scheme, sink }: {
16
+ scheme: TouchScheme;
17
+ sink: TouchCodeSink;
18
+ }): import("react").JSX.Element;
@@ -0,0 +1,173 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useMemo, useRef } from "react";
3
+ import { createGestureSurfaceTracker } from "@jgengine/core/input/gestureSurface";
4
+ import { createTouchGestureTracker } from "@jgengine/core/input/touchGestures";
5
+ import { touchCode } from "@jgengine/core/input/touchScheme";
6
+ import { GameIcon, iconForAction, isGameIconName } from "@jgengine/react/gameIcons";
7
+ const JOYSTICK_SIZE = 132;
8
+ const JOYSTICK_THUMB = 52;
9
+ const JOYSTICK_DEADZONE = 0.28;
10
+ const JOYSTICK_AXIS_THRESHOLD = 0.42;
11
+ const LOOK_TAP_TUNING = { tapMoveThresholdPx: 12, tapMaxMs: 280, longPressMs: 450 };
12
+ function pressOnce(sink, action) {
13
+ const code = touchCode(action);
14
+ sink.onCodeDown(code);
15
+ sink.onCodeUp(code);
16
+ }
17
+ export function TouchPlaySurface({ scheme, sink, yawRef, pitchRef, maxPitch, onPrimaryTap, }) {
18
+ const pointerIdRef = useRef(null);
19
+ const gestureTracker = useMemo(() => (scheme.gestures === null ? null : createGestureSurfaceTracker(scheme.gestures)), [scheme]);
20
+ const lookTracker = useMemo(() => (scheme.look ? createTouchGestureTracker(LOOK_TAP_TUNING) : null), [scheme]);
21
+ const release = (event) => {
22
+ if (event.pointerId !== pointerIdRef.current)
23
+ return;
24
+ pointerIdRef.current = null;
25
+ };
26
+ return (_jsx("div", { className: "absolute inset-0 touch-none", onPointerDown: (event) => {
27
+ if (event.pointerType !== "touch" || pointerIdRef.current !== null)
28
+ return;
29
+ pointerIdRef.current = event.pointerId;
30
+ event.currentTarget.setPointerCapture(event.pointerId);
31
+ if (lookTracker !== null)
32
+ lookTracker.begin(event.clientX, event.clientY, performance.now());
33
+ else
34
+ gestureTracker?.begin(event.clientX, event.clientY, performance.now());
35
+ }, onPointerMove: (event) => {
36
+ if (event.pointerId !== pointerIdRef.current)
37
+ return;
38
+ event.stopPropagation();
39
+ if (lookTracker !== null) {
40
+ const delta = lookTracker.move(event.clientX, event.clientY);
41
+ if (delta !== null) {
42
+ yawRef.current -= delta.dx * scheme.lookSensitivity;
43
+ pitchRef.current = Math.max(-maxPitch, Math.min(maxPitch, pitchRef.current - delta.dy * scheme.lookSensitivity));
44
+ }
45
+ return;
46
+ }
47
+ for (const action of gestureTracker?.move(event.clientX, event.clientY) ?? [])
48
+ pressOnce(sink, action);
49
+ }, onPointerUp: (event) => {
50
+ if (event.pointerId !== pointerIdRef.current)
51
+ return;
52
+ event.stopPropagation();
53
+ release(event);
54
+ if (lookTracker !== null) {
55
+ if (lookTracker.end(performance.now()) === "tap") {
56
+ if (scheme.gestures?.tap !== undefined)
57
+ pressOnce(sink, scheme.gestures.tap);
58
+ else
59
+ onPrimaryTap();
60
+ }
61
+ return;
62
+ }
63
+ for (const action of gestureTracker?.end(event.clientX, event.clientY, performance.now()) ?? []) {
64
+ pressOnce(sink, action);
65
+ }
66
+ }, onPointerCancel: (event) => {
67
+ release(event);
68
+ lookTracker?.cancel();
69
+ gestureTracker?.cancel();
70
+ } }));
71
+ }
72
+ function joystickDirections(joystick, nx, ny) {
73
+ const active = new Set();
74
+ if (Math.hypot(nx, ny) < JOYSTICK_DEADZONE)
75
+ return active;
76
+ if (joystick.up !== null && ny < -JOYSTICK_AXIS_THRESHOLD)
77
+ active.add(joystick.up);
78
+ if (joystick.down !== null && ny > JOYSTICK_AXIS_THRESHOLD)
79
+ active.add(joystick.down);
80
+ if (joystick.left !== null && nx < -JOYSTICK_AXIS_THRESHOLD)
81
+ active.add(joystick.left);
82
+ if (joystick.right !== null && nx > JOYSTICK_AXIS_THRESHOLD)
83
+ active.add(joystick.right);
84
+ return active;
85
+ }
86
+ function VirtualJoystick({ joystick, sink }) {
87
+ const baseRef = useRef(null);
88
+ const thumbRef = useRef(null);
89
+ const pointerIdRef = useRef(null);
90
+ const activeRef = useRef(new Set());
91
+ const applyVector = (clientX, clientY) => {
92
+ const base = baseRef.current;
93
+ const thumb = thumbRef.current;
94
+ if (base === null || thumb === null)
95
+ return;
96
+ const rect = base.getBoundingClientRect();
97
+ const radius = rect.width / 2;
98
+ let nx = (clientX - rect.left - radius) / radius;
99
+ let ny = (clientY - rect.top - radius) / radius;
100
+ const length = Math.hypot(nx, ny);
101
+ if (length > 1) {
102
+ nx /= length;
103
+ ny /= length;
104
+ }
105
+ const travel = radius - JOYSTICK_THUMB / 2;
106
+ thumb.style.transform = `translate(${nx * travel}px, ${ny * travel}px)`;
107
+ const next = joystickDirections(joystick, nx, ny);
108
+ for (const action of activeRef.current) {
109
+ if (!next.has(action))
110
+ sink.onCodeUp(touchCode(action));
111
+ }
112
+ for (const action of next) {
113
+ if (!activeRef.current.has(action))
114
+ sink.onCodeDown(touchCode(action));
115
+ }
116
+ activeRef.current = next;
117
+ };
118
+ const releaseAll = () => {
119
+ pointerIdRef.current = null;
120
+ for (const action of activeRef.current)
121
+ sink.onCodeUp(touchCode(action));
122
+ activeRef.current = new Set();
123
+ if (thumbRef.current !== null)
124
+ thumbRef.current.style.transform = "translate(0px, 0px)";
125
+ };
126
+ return (_jsx("div", { ref: baseRef, className: "pointer-events-auto relative flex touch-none select-none items-center justify-center rounded-full border border-white/20 bg-white/10 backdrop-blur-sm", style: { width: JOYSTICK_SIZE, height: JOYSTICK_SIZE }, onPointerDown: (event) => {
127
+ if (pointerIdRef.current !== null)
128
+ return;
129
+ pointerIdRef.current = event.pointerId;
130
+ event.currentTarget.setPointerCapture(event.pointerId);
131
+ applyVector(event.clientX, event.clientY);
132
+ }, onPointerMove: (event) => {
133
+ if (event.pointerId !== pointerIdRef.current)
134
+ return;
135
+ event.stopPropagation();
136
+ applyVector(event.clientX, event.clientY);
137
+ }, onPointerUp: (event) => {
138
+ if (event.pointerId !== pointerIdRef.current)
139
+ return;
140
+ event.stopPropagation();
141
+ releaseAll();
142
+ }, onPointerCancel: () => releaseAll(), children: _jsx("div", { ref: thumbRef, className: "rounded-full border border-white/30 bg-white/25", style: { width: JOYSTICK_THUMB, height: JOYSTICK_THUMB } }) }));
143
+ }
144
+ function touchButtonIcon(button) {
145
+ if (button.icon === false)
146
+ return null;
147
+ if (button.icon !== null && isGameIconName(button.icon))
148
+ return button.icon;
149
+ return iconForAction(button.action);
150
+ }
151
+ function TouchActionButton({ button, sink }) {
152
+ const pointerIdRef = useRef(null);
153
+ const icon = touchButtonIcon(button);
154
+ const releaseIfHeld = () => {
155
+ if (pointerIdRef.current === null)
156
+ return;
157
+ pointerIdRef.current = null;
158
+ sink.onCodeUp(touchCode(button.action));
159
+ };
160
+ return (_jsx("button", { type: "button", "aria-label": button.label, className: "pointer-events-auto flex h-14 w-14 touch-none select-none items-center justify-center rounded-full border border-white/25 bg-white/10 text-center text-[10px] font-semibold uppercase leading-tight tracking-wide text-white/90 backdrop-blur-sm active:border-white/50 active:bg-white/30", onPointerDown: (event) => {
161
+ if (pointerIdRef.current !== null)
162
+ return;
163
+ pointerIdRef.current = event.pointerId;
164
+ event.currentTarget.setPointerCapture(event.pointerId);
165
+ sink.onCodeDown(touchCode(button.action));
166
+ }, onPointerUp: (event) => {
167
+ event.stopPropagation();
168
+ releaseIfHeld();
169
+ }, onPointerCancel: () => releaseIfHeld(), onContextMenu: (event) => event.preventDefault(), children: icon !== null ? _jsx(GameIcon, { name: icon, size: 26 }) : _jsx("span", { className: "px-1", children: button.label }) }));
170
+ }
171
+ export function TouchControlsDock({ scheme, sink }) {
172
+ return (_jsxs("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 z-40 flex items-end justify-between gap-4 px-5", style: { paddingBottom: "calc(env(safe-area-inset-bottom, 0px) + 20px)" }, children: [_jsx("div", { children: scheme.joystick !== null ? _jsx(VirtualJoystick, { joystick: scheme.joystick, sink: sink }) : null }), _jsx("div", { className: "flex max-w-[60%] flex-wrap items-end justify-end gap-3", children: scheme.buttons.map((button) => (_jsx(TouchActionButton, { button: button, sink: sink }, button.action))) })] }));
173
+ }
@@ -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 common = horizontal * k * sine;\n tangent += vec3(-common * direction.x * direction.x, amplitude * k * direction.x * cosine, -common * direction.x * direction.y);\n bitangent += vec3(-common * direction.x * direction.y, amplitude * k * direction.y * cosine, -common * 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";
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
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\nvoid main() {\n vec3 normal = normalize(vNormal);\n vec3 viewDirection = normalize(cameraPosition - vWorldPosition);\n float fresnel = pow(1.0 - max(dot(normal, viewDirection), 0.0), 4.5);\n float depthMix = smoothstep(-1.8, 1.6, vWaveHeight);\n vec3 waterColor = mix(uDeepColor, uShallowColor, depthMix);\n waterColor = mix(waterColor, uCrestColor, smoothstep(0.15, 1.2, vWaveHeight) * 0.28);\n waterColor = mix(waterColor, vec3(0.86, 0.95, 1.0), fresnel * uFresnelStrength);\n float horizon = smoothstep(0.0, 1.0, 1.0 - abs(normal.y));\n waterColor = mix(waterColor, uShallowColor, horizon * uHorizonBlend);\n float foam = clamp(vCrest * uFoamIntensity, 0.0, 1.0);\n vec3 color = mix(waterColor, uFoamColor, foam);\n gl_FragColor = vec4(color, uOpacity);\n}\n";
@@ -32,9 +32,9 @@ void main() {
32
32
  displaced.x += horizontal * direction.x * cosine;
33
33
  displaced.z += horizontal * direction.y * cosine;
34
34
  displaced.y += amplitude * sine;
35
- float common = horizontal * k * sine;
36
- tangent += vec3(-common * direction.x * direction.x, amplitude * k * direction.x * cosine, -common * direction.x * direction.y);
37
- bitangent += vec3(-common * direction.x * direction.y, amplitude * k * direction.y * cosine, -common * direction.y * direction.y);
35
+ float slopeTerm = horizontal * k * sine;
36
+ tangent += vec3(-slopeTerm * direction.x * direction.x, amplitude * k * direction.x * cosine, -slopeTerm * direction.x * direction.y);
37
+ bitangent += vec3(-slopeTerm * direction.x * direction.y, amplitude * k * direction.y * cosine, -slopeTerm * direction.y * direction.y);
38
38
  crest = max(crest, smoothstep(uFoamThreshold, uFoamThreshold + uFoamSoftness, sine * steepness + uFoamCoverage * 0.35));
39
39
  }
40
40
 
@@ -0,0 +1,44 @@
1
+ import { type ReactNode } from "react";
2
+ import * as THREE from "three";
3
+ /**
4
+ * Renders one placed 3D object per data item — a 3D bar chart, a heatmap, a city
5
+ * of buildings, a crowd. By default each item is an extruded box (all boxes share
6
+ * one `InstancedMesh`, so hundreds cost one draw call), sized and colored from the
7
+ * item; `renderItem` swaps the box for arbitrary content (a sprite, a GLB, a full
8
+ * entity). Genre-agnostic: the caller owns the data `T`, this owns the placement.
9
+ */
10
+ export interface DataObjectsProps<T> {
11
+ data: readonly T[];
12
+ /** World `[x, z]` for an item (placed on the ground plane). */
13
+ position: (item: T, index: number) => readonly [number, number];
14
+ /** Box height (Y-scale) for an item. */
15
+ height: (item: T, index: number) => number;
16
+ /** Box color for an item (any CSS/hex/THREE color). */
17
+ color: (item: T, index: number) => THREE.ColorRepresentation;
18
+ /** Footprint on X and Z. Default 0.28. */
19
+ cellSize?: number;
20
+ /** Index of the highlighted item, painted with `hoverColor`. */
21
+ hovered?: number | null;
22
+ hoverColor?: THREE.ColorRepresentation;
23
+ /** Called with the item index under the pointer, or null on exit. */
24
+ onHover?: (index: number | null) => void;
25
+ /** Staggered grow-in; restarts whenever `data` identity changes. */
26
+ grow?: {
27
+ duration?: number;
28
+ delay?: (item: T, index: number) => number;
29
+ };
30
+ castShadow?: boolean;
31
+ receiveShadow?: boolean;
32
+ /**
33
+ * Render arbitrary content per item instead of an instanced box — a sprite, a
34
+ * GLB, a full entity. The layout positions each on the ground at its `[x, z]`
35
+ * and hands you the resolved `height` to size against (e.g. one walking figure
36
+ * per unit of value). When set, the instanced-box path is bypassed.
37
+ */
38
+ renderItem?: (item: T, index: number, info: {
39
+ x: number;
40
+ z: number;
41
+ height: number;
42
+ }) => ReactNode;
43
+ }
44
+ export declare function DataObjects<T>({ data, position, height, color, cellSize, hovered, hoverColor, onHover, grow, castShadow, receiveShadow, renderItem, }: DataObjectsProps<T>): import("react").JSX.Element;
@@ -0,0 +1,75 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useFrame } from "@react-three/fiber";
3
+ import { useEffect, useMemo, useRef } from "react";
4
+ import * as THREE from "three";
5
+ function clamp01(v) {
6
+ return v < 0 ? 0 : v > 1 ? 1 : v;
7
+ }
8
+ export function DataObjects({ data, position, height, color, cellSize = 0.28, hovered = null, hoverColor = "#ffffff", onHover, grow, castShadow = true, receiveShadow = true, renderItem, }) {
9
+ const meshRef = useRef(null);
10
+ const dummy = useMemo(() => new THREE.Object3D(), []);
11
+ const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
12
+ const material = useMemo(() => new THREE.MeshStandardMaterial({ roughness: 0.62, metalness: 0.04 }), []);
13
+ const hover = useMemo(() => new THREE.Color(hoverColor), [hoverColor]);
14
+ const startRef = useRef(null);
15
+ const capacity = data.length;
16
+ const layout = useMemo(() => {
17
+ const positions = new Float32Array(capacity * 2);
18
+ const targets = new Float32Array(capacity);
19
+ const delays = new Float32Array(capacity);
20
+ const colors = [];
21
+ data.forEach((item, i) => {
22
+ const [x, z] = position(item, i);
23
+ positions[i * 2] = x;
24
+ positions[i * 2 + 1] = z;
25
+ targets[i] = height(item, i);
26
+ delays[i] = grow?.delay?.(item, i) ?? 0;
27
+ colors.push(new THREE.Color(color(item, i)));
28
+ });
29
+ return { positions, targets, delays, colors };
30
+ // eslint-disable-next-line react-hooks/exhaustive-deps
31
+ }, [data]);
32
+ useEffect(() => {
33
+ startRef.current = null;
34
+ }, [data]);
35
+ useEffect(() => () => {
36
+ geometry.dispose();
37
+ material.dispose();
38
+ }, [geometry, material]);
39
+ const duration = grow?.duration ?? 0;
40
+ useFrame((state) => {
41
+ const mesh = meshRef.current;
42
+ if (mesh === null)
43
+ return;
44
+ if (startRef.current === null)
45
+ startRef.current = state.clock.elapsedTime;
46
+ const elapsed = state.clock.elapsedTime - startRef.current;
47
+ const { positions, targets, delays, colors } = layout;
48
+ for (let i = 0; i < capacity; i += 1) {
49
+ const grown = duration <= 0 ? 1 : clamp01((elapsed - delays[i]) / duration);
50
+ const eased = 1 - Math.pow(1 - grown, 3);
51
+ const h = Math.max(targets[i] * eased, 0.001);
52
+ dummy.position.set(positions[i * 2], h / 2, positions[i * 2 + 1]);
53
+ dummy.scale.set(cellSize, h, cellSize);
54
+ dummy.updateMatrix();
55
+ mesh.setMatrixAt(i, dummy.matrix);
56
+ mesh.setColorAt(i, i === hovered ? hover : colors[i]);
57
+ }
58
+ mesh.instanceMatrix.needsUpdate = true;
59
+ if (mesh.instanceColor !== null)
60
+ mesh.instanceColor.needsUpdate = true;
61
+ });
62
+ function handleMove(event) {
63
+ if (onHover === undefined)
64
+ return;
65
+ event.stopPropagation();
66
+ onHover(event.instanceId ?? null);
67
+ }
68
+ if (renderItem !== undefined) {
69
+ return (_jsx("group", { children: data.map((item, i) => {
70
+ const [x, z] = position(item, i);
71
+ return (_jsx("group", { position: [x, 0, z], children: renderItem(item, i, { x, z, height: height(item, i) }) }, i));
72
+ }) }));
73
+ }
74
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, Math.max(capacity, 1)], castShadow: castShadow, receiveShadow: receiveShadow, frustumCulled: false, onPointerMove: onHover === undefined ? undefined : handleMove, onPointerOut: onHover === undefined ? undefined : () => onHover(null) }, capacity));
75
+ }
@@ -0,0 +1,10 @@
1
+ import type { WorldFeature } from "@jgengine/core/world/features";
2
+ export interface GridWorldSceneProps {
3
+ feature: WorldFeature;
4
+ }
5
+ /**
6
+ * Data-driven renderer for the `biomes()`/`voxel()`/`plots()`/`tilemap()` world-feature kinds
7
+ * (#207.1): one `InstancedMesh` of extruded, colored boxes built from each feature's declared
8
+ * `cells`, following the same direct-buffer pattern as `InstancedBodies`.
9
+ */
10
+ export declare function GridWorldScene({ feature }: GridWorldSceneProps): import("react").JSX.Element | null;
@@ -0,0 +1,54 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from "react";
3
+ import * as THREE from "three";
4
+ import { resolveGridInstances } from "@jgengine/core/world/gridInstances";
5
+ function isGridWorldFeature(feature) {
6
+ return (feature.kind === "biomes" || feature.kind === "voxel" || feature.kind === "plots" || feature.kind === "tilemap");
7
+ }
8
+ /**
9
+ * Data-driven renderer for the `biomes()`/`voxel()`/`plots()`/`tilemap()` world-feature kinds
10
+ * (#207.1): one `InstancedMesh` of extruded, colored boxes built from each feature's declared
11
+ * `cells`, following the same direct-buffer pattern as `InstancedBodies`.
12
+ */
13
+ export function GridWorldScene({ feature }) {
14
+ const instances = useMemo(() => (isGridWorldFeature(feature) ? resolveGridInstances(feature) : []), [feature]);
15
+ const meshRef = useRef(null);
16
+ const geometry = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
17
+ const material = useMemo(() => new THREE.MeshStandardMaterial({ roughness: 0.9, metalness: 0 }), []);
18
+ const instanceColor = useMemo(() => new THREE.InstancedBufferAttribute(new Float32Array(Math.max(instances.length, 1) * 3), 3), [instances.length]);
19
+ useEffect(() => {
20
+ const mesh = meshRef.current;
21
+ if (mesh === null)
22
+ return;
23
+ mesh.instanceColor = instanceColor;
24
+ mesh.count = instances.length;
25
+ const m = mesh.instanceMatrix.array;
26
+ const c = instanceColor.array;
27
+ const color = new THREE.Color();
28
+ for (let i = 0; i < instances.length; i += 1) {
29
+ const instance = instances[i];
30
+ const o = i * 16;
31
+ m[o] = instance.scale[0];
32
+ m[o + 5] = instance.scale[1];
33
+ m[o + 10] = instance.scale[2];
34
+ m[o + 12] = instance.position[0];
35
+ m[o + 13] = instance.position[1];
36
+ m[o + 14] = instance.position[2];
37
+ m[o + 15] = 1;
38
+ color.set(instance.color);
39
+ const co = i * 3;
40
+ c[co] = color.r;
41
+ c[co + 1] = color.g;
42
+ c[co + 2] = color.b;
43
+ }
44
+ mesh.instanceMatrix.needsUpdate = true;
45
+ instanceColor.needsUpdate = true;
46
+ }, [instances, instanceColor]);
47
+ useEffect(() => () => {
48
+ geometry.dispose();
49
+ material.dispose();
50
+ }, [geometry, material]);
51
+ if (instances.length === 0)
52
+ return null;
53
+ return (_jsx("instancedMesh", { ref: meshRef, args: [geometry, material, instances.length], castShadow: true, receiveShadow: true }));
54
+ }
@@ -1,6 +1,10 @@
1
- export declare function WorldEntityBars({ statId, height }: {
1
+ import type { SceneEntity } from "@jgengine/core/scene/entityStore";
2
+ import type { CatalogEntityRole } from "@jgengine/core/runtime/gameContext";
3
+ export declare function WorldEntityBars({ statId, height, roles, resolveRole, }: {
2
4
  statId: string;
3
5
  height?: number;
6
+ roles?: readonly CatalogEntityRole[];
7
+ resolveRole?: (entity: SceneEntity) => CatalogEntityRole | undefined;
4
8
  }): import("react").JSX.Element;
5
9
  export declare function WorldFloatText({ height, lifeMs }: {
6
10
  height?: number;
@@ -3,6 +3,7 @@ import { Html, Line } from "@react-three/drei";
3
3
  import { useFrame, useThree } from "@react-three/fiber";
4
4
  import { useEffect, useMemo, useRef, useState } from "react";
5
5
  import * as THREE from "three";
6
+ import { worldHealthBarAllowsRole } from "@jgengine/core/game/playableGame";
6
7
  import { useGameContext } from "@jgengine/react/provider";
7
8
  import { useEntityStat, useSceneEntities } from "@jgengine/react/hooks";
8
9
  import { resolveFloatTextStyle } from "./floatTextStyle.js";
@@ -15,11 +16,14 @@ function EntityBar({ entity, statId, height }) {
15
16
  const percent = range <= 0 ? 0 : Math.max(0, Math.min(1, (stat.current - stat.min) / range));
16
17
  return (_jsx(Html, { position: [entity.position[0], entity.position[1] + height, entity.position[2]], center: true, distanceFactor: 12, zIndexRange: [20, 0], children: _jsx("div", { className: "h-2.5 w-28 overflow-hidden rounded-sm border border-black/70 bg-black/70 shadow", children: _jsx("div", { className: "h-full bg-gradient-to-r from-rose-600 to-red-400", style: { width: `${percent * 100}%` } }) }) }));
17
18
  }
18
- export function WorldEntityBars({ statId, height = 2.2 }) {
19
+ export function WorldEntityBars({ statId, height = 2.2, roles, resolveRole, }) {
19
20
  const ctx = useGameContext();
20
21
  const entities = useSceneEntities();
21
22
  const playerId = ctx.player.userId;
22
- return (_jsx(_Fragment, { children: entities.map((entity) => entity.id === playerId ? null : (_jsx(EntityBar, { entity: entity, statId: statId, height: height }, entity.id))) }));
23
+ return (_jsx(_Fragment, { children: entities
24
+ .filter((entity) => entity.id !== playerId)
25
+ .filter((entity) => worldHealthBarAllowsRole(roles, resolveRole?.(entity)))
26
+ .map((entity) => (_jsx(EntityBar, { entity: entity, statId: statId, height: height }, entity.id))) }));
23
27
  }
24
28
  function FloatTextItem({ event, lifeMs }) {
25
29
  const [shown, setShown] = useState(false);
@@ -1,18 +1,37 @@
1
1
  import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Html, Line } from "@react-three/drei";
3
- import { useMemo } from "react";
3
+ import { useMemo, useRef } from "react";
4
+ import { useFrame } from "@react-three/fiber";
4
5
  import { resolveWorldItemPresentation } from "@jgengine/core/game/worldItem";
5
6
  import { useGameContext } from "@jgengine/react/provider";
6
7
  import { useWorldItems } from "@jgengine/react/hooks";
7
8
  import { POINTER_ENTITY_KEY } from "../pointer/pointerService.js";
8
9
  const DEFAULT_BEAM_HEIGHT = 2.5;
9
10
  const FALLBACK_COLOR = "#e5e7eb";
11
+ const REST_HEIGHT = 0.32;
12
+ const SPAWN_DURATION = 0.4;
10
13
  function WorldItemMarker({ instanceId, position, color, beam, label, beamHeight, }) {
11
14
  const beamPoints = useMemo(() => [
12
15
  [0, 0, 0],
13
16
  [0, beamHeight, 0],
14
17
  ], [beamHeight]);
15
- return (_jsxs("group", { position: [position[0], position[1], position[2]], userData: { [POINTER_ENTITY_KEY]: instanceId }, children: [_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.26, 0.4, 24] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85 })] }), _jsxs("mesh", { "position-y": 0.32, children: [_jsx("octahedronGeometry", { args: [0.22, 0] }), _jsx("meshStandardMaterial", { color: color, emissive: color, emissiveIntensity: 0.6 })] }), beam ? _jsx(Line, { points: beamPoints, color: color, lineWidth: 2, transparent: true, opacity: 0.55 }) : null, label !== undefined && label.length > 0 ? (_jsx(Html, { position: [0, beamHeight + 0.2, 0], center: true, distanceFactor: 12, zIndexRange: [15, 0], children: _jsx("div", { className: "whitespace-nowrap rounded-sm border px-1.5 py-0.5 text-[11px] font-bold shadow", style: { borderColor: color, color, backgroundColor: "rgba(10,10,14,0.75)" }, children: label }) })) : null] }));
18
+ // Drop animation: the gem leaps up on spawn, settles, then idles with a slow
19
+ // spin + hover so a fresh drop reads as "something just fell here". Age is
20
+ // accumulated from frame deltas (Date.now is unavailable to keep runs pure).
21
+ const gemRef = useRef(null);
22
+ const ageRef = useRef(0);
23
+ useFrame((_state, delta) => {
24
+ const gem = gemRef.current;
25
+ if (gem === null)
26
+ return;
27
+ const age = ageRef.current + delta;
28
+ ageRef.current = age;
29
+ const spawn = age < SPAWN_DURATION ? Math.sin((age / SPAWN_DURATION) * Math.PI) * 0.35 : 0;
30
+ const bob = Math.sin(age * 2.2) * 0.06;
31
+ gem.position.y = REST_HEIGHT + bob + spawn;
32
+ gem.rotation.y = age * 1.6;
33
+ });
34
+ return (_jsxs("group", { position: [position[0], position[1], position[2]], userData: { [POINTER_ENTITY_KEY]: instanceId }, children: [_jsxs("mesh", { "rotation-x": -Math.PI / 2, "position-y": 0.03, children: [_jsx("ringGeometry", { args: [0.26, 0.4, 24] }), _jsx("meshBasicMaterial", { color: color, transparent: true, opacity: 0.85 })] }), _jsx("group", { ref: gemRef, "position-y": REST_HEIGHT, children: _jsxs("mesh", { children: [_jsx("octahedronGeometry", { args: [0.22, 0] }), _jsx("meshStandardMaterial", { color: color, emissive: color, emissiveIntensity: 0.6 })] }) }), beam ? _jsx(Line, { points: beamPoints, color: color, lineWidth: 2, transparent: true, opacity: 0.55 }) : null, label !== undefined && label.length > 0 ? (_jsx(Html, { position: [0, beamHeight + 0.2, 0], center: true, distanceFactor: 12, zIndexRange: [15, 0], children: _jsx("div", { className: "whitespace-nowrap rounded-sm border px-1.5 py-0.5 text-[11px] font-bold shadow", style: { borderColor: color, color, backgroundColor: "rgba(10,10,14,0.75)" }, children: label }) })) : null] }));
16
35
  }
17
36
  /** Rarity→beam/color/label render binding + loot-filter overlay (#32/#33) for every dropped `worldItem`. */
18
37
  export function WorldItems({ config }) {