@plasius/renderer 1.0.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 (79) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/CODE_OF_CONDUCT.md +79 -0
  3. package/CONTRIBUTORS.md +27 -0
  4. package/LICENSE +203 -0
  5. package/README.md +70 -0
  6. package/SECURITY.md +17 -0
  7. package/dist/adaptivedpr.d.ts +2 -0
  8. package/dist/adaptivedpr.d.ts.map +1 -0
  9. package/dist/adaptivedpr.js +65 -0
  10. package/dist/camera/cameraRigProfile.d.ts +12 -0
  11. package/dist/camera/cameraRigProfile.d.ts.map +1 -0
  12. package/dist/camera/cameraRigProfile.js +18 -0
  13. package/dist/camera/managedCameraController.d.ts +49 -0
  14. package/dist/camera/managedCameraController.d.ts.map +1 -0
  15. package/dist/camera/managedCameraController.js +271 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +3 -0
  19. package/dist/landscape.d.ts +2 -0
  20. package/dist/landscape.d.ts.map +1 -0
  21. package/dist/landscape.js +120 -0
  22. package/dist/player/player.d.ts +8 -0
  23. package/dist/player/player.d.ts.map +1 -0
  24. package/dist/player/player.js +203 -0
  25. package/dist/player/playerstore.d.ts +205 -0
  26. package/dist/player/playerstore.d.ts.map +1 -0
  27. package/dist/player/playerstore.js +500 -0
  28. package/dist/renderStateProvider.d.ts +57 -0
  29. package/dist/renderStateProvider.d.ts.map +1 -0
  30. package/dist/renderStateProvider.js +50 -0
  31. package/dist/renderer.d.ts +9 -0
  32. package/dist/renderer.d.ts.map +1 -0
  33. package/dist/renderer.js +165 -0
  34. package/dist/scene.d.ts +7 -0
  35. package/dist/scene.d.ts.map +1 -0
  36. package/dist/scene.js +10 -0
  37. package/dist/shaders/fragment/landscapeFragmentShader.js +141 -0
  38. package/dist/shaders/landscapeShader.d.ts +13 -0
  39. package/dist/shaders/landscapeShader.d.ts.map +1 -0
  40. package/dist/shaders/landscapeShader.js +25 -0
  41. package/dist/shaders/vertex/landscapeVertexShader.js +67 -0
  42. package/dist/styles/renderer.module.css +90 -0
  43. package/dist/worldSpaceCompositor.d.ts +50 -0
  44. package/dist/worldSpaceCompositor.d.ts.map +1 -0
  45. package/dist/worldSpaceCompositor.js +159 -0
  46. package/dist/xr/rendererXrBridge.d.ts +12 -0
  47. package/dist/xr/rendererXrBridge.d.ts.map +1 -0
  48. package/dist/xr/rendererXrBridge.js +17 -0
  49. package/docs/adrs/adr-0001-renderer-package-scope.md +21 -0
  50. package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
  51. package/docs/adrs/adr-0003-world-space-compositor-contracts.md +34 -0
  52. package/docs/adrs/adr-template.md +35 -0
  53. package/docs/design/0001-public-package-scope.md +18 -0
  54. package/docs/tdrs/index.md +3 -0
  55. package/docs/tdrs/tdr-0001-renderer-public-package-standards-alignment.md +19 -0
  56. package/legal/CLA-REGISTRY.csv +1 -0
  57. package/legal/CLA.md +22 -0
  58. package/legal/CORPORATE_CLA.md +57 -0
  59. package/legal/INDIVIDUAL_CLA.md +91 -0
  60. package/package.json +117 -0
  61. package/src/adaptivedpr.tsx +74 -0
  62. package/src/camera/cameraRigProfile.ts +29 -0
  63. package/src/camera/managedCameraController.tsx +401 -0
  64. package/src/global.d.ts +10 -0
  65. package/src/index.ts +3 -0
  66. package/src/landscape.tsx +321 -0
  67. package/src/player/player.tsx +257 -0
  68. package/src/player/playerstore.tsx +733 -0
  69. package/src/renderStateProvider.tsx +121 -0
  70. package/src/renderer.tsx +294 -0
  71. package/src/scene.tsx +42 -0
  72. package/src/shaders/fragment/landscapeFragmentShader.d.ts +4 -0
  73. package/src/shaders/fragment/landscapeFragmentShader.js +141 -0
  74. package/src/shaders/landscapeShader.tsx +39 -0
  75. package/src/shaders/vertex/landscapeVertexShader.d.ts +4 -0
  76. package/src/shaders/vertex/landscapeVertexShader.js +67 -0
  77. package/src/styles/renderer.module.css +90 -0
  78. package/src/worldSpaceCompositor.ts +265 -0
  79. package/src/xr/rendererXrBridge.ts +44 -0
@@ -0,0 +1,271 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { useFrame, useThree } from "@react-three/fiber";
3
+ import { createCameraManager, } from "@plasius/gpu-camera";
4
+ import { Vector3 } from "three";
5
+ function clamp(value, min, max) {
6
+ return Math.min(max, Math.max(min, value));
7
+ }
8
+ function length3(value) {
9
+ return Math.hypot(value[0], value[1], value[2]);
10
+ }
11
+ function normalize3(value, fallback) {
12
+ const len = length3(value);
13
+ if (len <= Number.EPSILON) {
14
+ return [...fallback];
15
+ }
16
+ return [value[0] / len, value[1] / len, value[2] / len];
17
+ }
18
+ function sub3(a, b) {
19
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
20
+ }
21
+ function add3(a, b) {
22
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
23
+ }
24
+ function scale3(value, scalar) {
25
+ return [value[0] * scalar, value[1] * scalar, value[2] * scalar];
26
+ }
27
+ function cross3(a, b) {
28
+ return [
29
+ a[1] * b[2] - a[2] * b[1],
30
+ a[2] * b[0] - a[0] * b[2],
31
+ a[0] * b[1] - a[1] * b[0],
32
+ ];
33
+ }
34
+ export function resolveCameraAspect(width, height) {
35
+ if (!Number.isFinite(width) || !Number.isFinite(height) || height <= 0) {
36
+ return 1;
37
+ }
38
+ return Math.max(1 / 4096, width / height);
39
+ }
40
+ export function createRendererCameraManager(options) {
41
+ return createCameraManager({
42
+ maxParallelViews: options?.maxParallelViews ?? 2,
43
+ maxHotCameras: options?.maxHotCameras ?? 3,
44
+ });
45
+ }
46
+ export function derivePanDeltaFromCameraState(cameraState, deltaX, deltaY, panSpeed) {
47
+ const position = cameraState.transform.position;
48
+ const target = cameraState.transform.target;
49
+ const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
50
+ const forward = normalize3(sub3(target, position), [0, 0, -1]);
51
+ const right = normalize3(cross3(forward, up), [1, 0, 0]);
52
+ const radius = Math.max(0.01, length3(sub3(position, target)));
53
+ const scale = Math.max(0.00001, panSpeed * radius);
54
+ const horizontal = scale3(right, -deltaX * scale);
55
+ const vertical = scale3(up, deltaY * scale);
56
+ return add3(horizontal, vertical);
57
+ }
58
+ export function buildCameraDefinitionFromThreeCamera(camera, aspect) {
59
+ const position = [camera.position.x, camera.position.y, camera.position.z];
60
+ const up = [camera.up.x, camera.up.y, camera.up.z];
61
+ const worldDirection = new Vector3(0, 0, -1);
62
+ camera.getWorldDirection?.(worldDirection);
63
+ const target = [
64
+ position[0] + worldDirection.x,
65
+ position[1] + worldDirection.y,
66
+ position[2] + worldDirection.z,
67
+ ];
68
+ if (camera.isOrthographicCamera) {
69
+ return {
70
+ transform: { position, target, up },
71
+ projection: {
72
+ kind: "orthographic",
73
+ left: camera.left ?? -1,
74
+ right: camera.right ?? 1,
75
+ top: camera.top ?? 1,
76
+ bottom: camera.bottom ?? -1,
77
+ near: camera.near ?? 0.1,
78
+ far: camera.far ?? 2000,
79
+ aspect,
80
+ },
81
+ };
82
+ }
83
+ return {
84
+ transform: { position, target, up },
85
+ projection: {
86
+ kind: "perspective",
87
+ fovY: camera.fov ?? 50,
88
+ near: camera.near ?? 0.1,
89
+ far: camera.far ?? 2000,
90
+ aspect,
91
+ },
92
+ };
93
+ }
94
+ export function syncThreeCameraFromManagedState(camera, cameraState, aspect) {
95
+ const transform = cameraState.transform;
96
+ const up = transform.up ?? [0, 1, 0];
97
+ camera.position.set(transform.position[0], transform.position[1], transform.position[2]);
98
+ camera.up.set(up[0], up[1], up[2]);
99
+ camera.lookAt(transform.target[0], transform.target[1], transform.target[2]);
100
+ const projection = cameraState.projection;
101
+ if (projection.kind === "perspective" && camera.isPerspectiveCamera) {
102
+ camera.fov = projection.fovY;
103
+ camera.near = projection.near;
104
+ camera.far = projection.far;
105
+ camera.aspect = aspect;
106
+ camera.updateProjectionMatrix?.();
107
+ return;
108
+ }
109
+ if (projection.kind === "orthographic" && camera.isOrthographicCamera) {
110
+ const sourceAspect = projection.aspect || 1;
111
+ const aspectScale = sourceAspect > 0 ? aspect / sourceAspect : 1;
112
+ camera.left = projection.left * aspectScale;
113
+ camera.right = projection.right * aspectScale;
114
+ camera.top = projection.top;
115
+ camera.bottom = projection.bottom;
116
+ camera.near = projection.near;
117
+ camera.far = projection.far;
118
+ camera.updateProjectionMatrix?.();
119
+ }
120
+ }
121
+ export function ManagedCameraController({ manager, profile, cameraId = "main", enabled = true, }) {
122
+ const { camera, gl, size } = useThree();
123
+ const pointerRef = useRef({
124
+ active: false,
125
+ mode: null,
126
+ x: 0,
127
+ y: 0,
128
+ });
129
+ const managedThreeCamera = camera;
130
+ const aspect = resolveCameraAspect(size.width, size.height);
131
+ useEffect(() => {
132
+ const cameraDefinition = buildCameraDefinitionFromThreeCamera(managedThreeCamera, aspect);
133
+ if (manager.hasCamera(cameraId)) {
134
+ manager.updateCamera(cameraId, cameraDefinition);
135
+ }
136
+ else {
137
+ manager.registerCamera({
138
+ id: cameraId,
139
+ priority: 100,
140
+ ...cameraDefinition,
141
+ });
142
+ }
143
+ manager.activateCamera(cameraId);
144
+ }, [manager, cameraId, managedThreeCamera, aspect]);
145
+ useEffect(() => {
146
+ const registered = manager.getCamera(cameraId);
147
+ if (!registered) {
148
+ return;
149
+ }
150
+ if (registered.projection.kind === "perspective") {
151
+ manager.updateCamera(cameraId, {
152
+ projection: {
153
+ ...registered.projection,
154
+ aspect,
155
+ },
156
+ });
157
+ return;
158
+ }
159
+ manager.updateCamera(cameraId, {
160
+ projection: {
161
+ ...registered.projection,
162
+ aspect,
163
+ },
164
+ });
165
+ }, [manager, cameraId, aspect]);
166
+ useEffect(() => {
167
+ if (!enabled) {
168
+ pointerRef.current.active = false;
169
+ pointerRef.current.mode = null;
170
+ return;
171
+ }
172
+ const element = gl.domElement;
173
+ if (!element) {
174
+ return;
175
+ }
176
+ const onPointerDown = (event) => {
177
+ if (event.button === 0 && !event.shiftKey) {
178
+ pointerRef.current.mode = "orbit";
179
+ }
180
+ else if (event.button === 2 || event.button === 1 || event.shiftKey) {
181
+ pointerRef.current.mode = "pan";
182
+ }
183
+ else {
184
+ pointerRef.current.mode = null;
185
+ }
186
+ if (!pointerRef.current.mode) {
187
+ return;
188
+ }
189
+ pointerRef.current.active = true;
190
+ pointerRef.current.x = event.clientX;
191
+ pointerRef.current.y = event.clientY;
192
+ element.setPointerCapture?.(event.pointerId);
193
+ };
194
+ const onPointerMove = (event) => {
195
+ if (!pointerRef.current.active || !pointerRef.current.mode) {
196
+ return;
197
+ }
198
+ const deltaX = event.clientX - pointerRef.current.x;
199
+ const deltaY = event.clientY - pointerRef.current.y;
200
+ pointerRef.current.x = event.clientX;
201
+ pointerRef.current.y = event.clientY;
202
+ if (pointerRef.current.mode === "orbit") {
203
+ manager.applyControl(cameraId, {
204
+ type: "orbit",
205
+ deltaAzimuth: -deltaX * profile.orbitSpeed,
206
+ deltaPolar: -deltaY * profile.orbitSpeed,
207
+ }, {
208
+ minDistance: profile.minDistance,
209
+ maxDistance: profile.maxDistance,
210
+ minPolarAngle: profile.minPolarAngle,
211
+ maxPolarAngle: profile.maxPolarAngle,
212
+ makeActive: true,
213
+ });
214
+ return;
215
+ }
216
+ const activeCamera = manager.getCamera(cameraId);
217
+ if (!activeCamera) {
218
+ return;
219
+ }
220
+ const delta = derivePanDeltaFromCameraState(activeCamera, deltaX, deltaY, profile.panSpeed);
221
+ manager.applyControl(cameraId, {
222
+ type: "pan",
223
+ delta,
224
+ }, { makeActive: true });
225
+ };
226
+ const onPointerUp = (event) => {
227
+ pointerRef.current.active = false;
228
+ pointerRef.current.mode = null;
229
+ element.releasePointerCapture?.(event.pointerId);
230
+ };
231
+ const onWheel = (event) => {
232
+ event.preventDefault();
233
+ const activeCamera = manager.getCamera(cameraId);
234
+ if (!activeCamera) {
235
+ return;
236
+ }
237
+ const distanceToTarget = Math.max(0.01, length3(sub3(activeCamera.transform.position, activeCamera.transform.target)));
238
+ const step = -event.deltaY * profile.dollySpeed * Math.max(0.05, distanceToTarget * 0.2);
239
+ manager.applyControl(cameraId, { type: "dolly", distance: step }, {
240
+ minDistance: profile.minDistance,
241
+ maxDistance: profile.maxDistance,
242
+ makeActive: true,
243
+ });
244
+ };
245
+ const onContextMenu = (event) => {
246
+ event.preventDefault();
247
+ };
248
+ element.addEventListener("pointerdown", onPointerDown);
249
+ window.addEventListener("pointermove", onPointerMove);
250
+ window.addEventListener("pointerup", onPointerUp);
251
+ element.addEventListener("wheel", onWheel, { passive: false });
252
+ element.addEventListener("contextmenu", onContextMenu);
253
+ return () => {
254
+ element.removeEventListener("pointerdown", onPointerDown);
255
+ window.removeEventListener("pointermove", onPointerMove);
256
+ window.removeEventListener("pointerup", onPointerUp);
257
+ element.removeEventListener("wheel", onWheel);
258
+ element.removeEventListener("contextmenu", onContextMenu);
259
+ };
260
+ }, [manager, cameraId, profile, gl, enabled]);
261
+ useFrame(() => {
262
+ const snapshot = manager.getSnapshot();
263
+ const activeId = snapshot.activeCameraId ?? cameraId;
264
+ const activeCamera = manager.getCamera(activeId);
265
+ if (!activeCamera) {
266
+ return;
267
+ }
268
+ syncThreeCameraFromManagedState(managedThreeCamera, activeCamera, aspect);
269
+ });
270
+ return null;
271
+ }
@@ -0,0 +1,4 @@
1
+ import "./shaders/landscapeShader.js";
2
+ export * from "./renderer.js";
3
+ export * from "./worldSpaceCompositor.js";
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,8BAA8B,CAAC;AACtC,cAAc,eAAe,CAAC;AAC9B,cAAc,2BAA2B,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ import "./shaders/landscapeShader.js";
2
+ export * from "./renderer.js";
3
+ export * from "./worldSpaceCompositor.js";
@@ -0,0 +1,2 @@
1
+ export declare function Landscape(): import("react").JSX.Element;
2
+ //# sourceMappingURL=landscape.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"landscape.d.ts","sourceRoot":"","sources":["../src/landscape.tsx"],"names":[],"mappings":"AA4GA,wBAAgB,SAAS,gCAsDxB"}
@@ -0,0 +1,120 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import * as THREE from "three";
3
+ import { useMemo } from "react";
4
+ import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
5
+ import { RigidBody } from "@react-three/rapier";
6
+ const HEX_RADIUS = 66;
7
+ //const TILE_RESOLUTION = 2048;
8
+ const AREA_WIDTH = 2000;
9
+ const AREA_HEIGHT = 2000;
10
+ // Generate a single high-resolution hexagon as BufferGeometry
11
+ function generateHexTile(radius) {
12
+ const geom = new THREE.BufferGeometry();
13
+ const vertices = [];
14
+ const indices = [];
15
+ const segments = 30; // More segments for smoother curve
16
+ const angleStep = (Math.PI * 2) / segments;
17
+ // Center point
18
+ vertices.push(0, 0, 0);
19
+ // Outer ring vertices
20
+ for (let i = 0; i <= segments; i++) {
21
+ const angle = angleStep * i;
22
+ const x = radius * Math.cos(angle);
23
+ const z = -radius * Math.sin(angle); // flip Z axis for pointy top upward
24
+ vertices.push(x, 0, z);
25
+ }
26
+ // Create triangle fan indices
27
+ for (let i = 1; i <= segments; i++) {
28
+ indices.push(0, i, i + 1);
29
+ }
30
+ geom.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
31
+ geom.setIndex(indices);
32
+ geom.computeVertexNormals();
33
+ return geom;
34
+ }
35
+ // Axial to world coordinates (pointy-topped hex)
36
+ function axialToWorld(q, r, radius) {
37
+ const x = ((radius * 3) / 2) * q;
38
+ const z = radius * Math.sqrt(3) * (r + q / 2);
39
+ return [x, z];
40
+ }
41
+ // Simple seeded noise for per-tile deformation
42
+ function seededNoise(x, z, seed = 42) {
43
+ const s = Math.sin(x * 12.9898 + z * 78.233 + seed) * 43758.5453;
44
+ return (s - Math.floor(s)) * 2 - 1;
45
+ }
46
+ // Deform only the interior (non-edge) vertices of a hex tile
47
+ function deformHexInterior(geom, radius, seed = 42) {
48
+ const pos = geom.attributes.position;
49
+ const vertexCount = pos.count;
50
+ for (let i = 0; i < vertexCount; i++) {
51
+ const x = pos.getX(i);
52
+ const z = pos.getZ(i);
53
+ const r = Math.sqrt(x * x + z * z);
54
+ // Only deform vertices inside the hex edge (leave edge vertices flat)
55
+ if (r < radius * 0.98) {
56
+ // Blend multiple nearby noise samples for smoother curvature
57
+ const n = (dx, dz) => seededNoise(x + dx, z + dz, seed);
58
+ const smoothY = (n(0, 0) * 4 +
59
+ n(-1, 0) +
60
+ n(1, 0) +
61
+ n(0, -1) +
62
+ n(0, 1) +
63
+ n(-1, -1) +
64
+ n(1, 1) +
65
+ n(-1, 1) +
66
+ n(1, -1)) /
67
+ 12;
68
+ pos.setY(i, smoothY);
69
+ }
70
+ }
71
+ pos.needsUpdate = true;
72
+ geom.computeVertexNormals();
73
+ }
74
+ function generateHexDepthMap(cols, rows, seed = 42) {
75
+ const map = [];
76
+ for (let q = 0; q < cols; q++) {
77
+ const row = [];
78
+ for (let r = 0; r < rows; r++) {
79
+ const value = Math.sin((q + seed) * 0.3) * Math.cos((r + seed) * 0.3);
80
+ row.push(value);
81
+ }
82
+ map.push(row);
83
+ }
84
+ return map;
85
+ }
86
+ export function Landscape() {
87
+ const geometry = useMemo(() => {
88
+ const radius = HEX_RADIUS;
89
+ const tiles = [];
90
+ const spacingX = (3 / 2) * radius;
91
+ const spacingZ = Math.sqrt(3) * radius;
92
+ const cols = Math.ceil(AREA_WIDTH / spacingX);
93
+ const rows = Math.ceil(AREA_HEIGHT / spacingZ);
94
+ const offset = Math.floor(cols / 2);
95
+ const depthMap = generateHexDepthMap(cols, rows);
96
+ const elevationScale = 20;
97
+ const baseHex = generateHexTile(radius);
98
+ for (let q = -Math.floor(cols / 2); q < Math.ceil(cols / 2); q++) {
99
+ for (let r = -Math.floor(rows / 2); r < Math.ceil(rows / 2); r++) {
100
+ const hex = baseHex.clone();
101
+ const dq = q + offset;
102
+ const dr = r + offset;
103
+ const elevation = depthMap?.[dq]?.[dr] ?? 0;
104
+ deformHexInterior(hex, radius, 42 + q * 1000 + r + Math.floor(elevation * 100));
105
+ const [x, z] = axialToWorld(q, r, radius);
106
+ hex.translate(x, elevation * elevationScale, z);
107
+ tiles.push(hex);
108
+ }
109
+ }
110
+ const merged = BufferGeometryUtils.mergeGeometries(tiles, false);
111
+ // Remove normals before merging vertices to ensure proper welding
112
+ if (merged.attributes.normal) {
113
+ merged.deleteAttribute("normal");
114
+ }
115
+ const stitched = BufferGeometryUtils.mergeVertices(merged, 1e-2);
116
+ stitched.computeVertexNormals();
117
+ return stitched;
118
+ }, []);
119
+ return (_jsx(RigidBody, { type: "fixed", colliders: "trimesh", children: _jsx("mesh", { geometry: geometry, castShadow: true, receiveShadow: true, children: _jsx("landscapeShaderMaterial", { color: "#77aa66" }) }) }));
120
+ }
@@ -0,0 +1,8 @@
1
+ import React, { type PropsWithChildren } from "react";
2
+ import type { CameraManager } from "@plasius/gpu-camera";
3
+ declare function WrappedPlayer({ cameraManager, cameraId, children, }: PropsWithChildren<{
4
+ cameraManager: CameraManager;
5
+ cameraId: string;
6
+ }>): React.JSX.Element;
7
+ export { WrappedPlayer as Player };
8
+ //# sourceMappingURL=player.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"player.d.ts","sourceRoot":"","sources":["../../src/player/player.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAqB,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AACzE,OAAO,KAAK,EAAE,aAAa,EAAQ,MAAM,qBAAqB,CAAC;AAiP/D,iBAAS,aAAa,CAAC,EACrB,aAAa,EACb,QAAQ,EACR,QAAQ,GACT,EAAE,iBAAiB,CAAC;IAAE,aAAa,EAAE,aAAa,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,qBAQvE;AAED,OAAO,EAAE,aAAa,IAAI,MAAM,EAAE,CAAC"}
@@ -0,0 +1,203 @@
1
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { useEffect, useRef } from "react";
3
+ import { PlayerStore } from "./playerstore.js";
4
+ function length3(value) {
5
+ return Math.hypot(value[0], value[1], value[2]);
6
+ }
7
+ function normalize3(value, fallback) {
8
+ const len = length3(value);
9
+ if (len <= Number.EPSILON) {
10
+ return [...fallback];
11
+ }
12
+ return [value[0] / len, value[1] / len, value[2] / len];
13
+ }
14
+ function add3(a, b) {
15
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
16
+ }
17
+ function sub3(a, b) {
18
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
19
+ }
20
+ function cross3(a, b) {
21
+ return [
22
+ a[1] * b[2] - a[2] * b[1],
23
+ a[2] * b[0] - a[0] * b[2],
24
+ a[0] * b[1] - a[1] * b[0],
25
+ ];
26
+ }
27
+ function Player({ cameraManager, cameraId, children, }) {
28
+ // Local refs for movement-relevant stats (used by the rAF ticker)
29
+ const strRef = useRef(1);
30
+ const dexRef = useRef(1);
31
+ const endRef = useRef(1);
32
+ const movingKeys = useRef(new Set());
33
+ const movingRef = useRef(false);
34
+ const runLoadRef = useRef(0); // seconds of continuous run load
35
+ const lastTickRef = useRef(performance.now());
36
+ // Subscribe via inline selector (useSyncExternalStore under the hood)
37
+ const physEff = PlayerStore.useSelector((s) => {
38
+ const phys = s?.attributesBase?.physical ?? {
39
+ strength: 1,
40
+ dexterity: 1,
41
+ endurance: 1,
42
+ };
43
+ const g = s?.attributesGear ?? {};
44
+ const e = s?.attributesEffects ?? {};
45
+ let strength = (phys.strength ?? 1) + (g.strength ?? 0) + (e.strength ?? 0);
46
+ let dexterity = (phys.dexterity ?? 1) + (g.dexterity ?? 0) + (e.dexterity ?? 0);
47
+ let endurance = (phys.endurance ?? 1) + (g.endurance ?? 0) + (e.endurance ?? 0);
48
+ // Add equipped item modifiers
49
+ if (s?.equipment && s?.items) {
50
+ for (const slot of Object.keys(s.equipment)) {
51
+ const itemId = s.equipment[slot];
52
+ const item = s.items[itemId];
53
+ const m = item?.modifiers;
54
+ if (m) {
55
+ strength += m.strength ?? 0;
56
+ dexterity += m.dexterity ?? 0;
57
+ endurance += m.endurance ?? 0;
58
+ }
59
+ }
60
+ }
61
+ // Add status effect attribute modifiers
62
+ if (s?.effects) {
63
+ for (const eff of Object.values(s.effects)) {
64
+ const mods = eff?.modifiers?.attributes;
65
+ if (mods) {
66
+ strength += mods.strength ?? 0;
67
+ dexterity += mods.dexterity ?? 0;
68
+ endurance += mods.endurance ?? 0;
69
+ }
70
+ }
71
+ }
72
+ return { strength, dexterity, endurance };
73
+ });
74
+ // Keep refs in sync for rAF without causing extra renders
75
+ useEffect(() => {
76
+ strRef.current = physEff.strength;
77
+ dexRef.current = physEff.dexterity;
78
+ endRef.current = physEff.endurance;
79
+ }, [physEff.strength, physEff.dexterity, physEff.endurance]);
80
+ useEffect(() => {
81
+ window.focus();
82
+ }, []);
83
+ // Fatigue/exhaustion ticker
84
+ useEffect(() => {
85
+ let raf = 0;
86
+ const tick = () => {
87
+ const now = performance.now();
88
+ const dt = (now - lastTickRef.current) / 1000;
89
+ lastTickRef.current = now;
90
+ const ln = (x) => Math.log(1 + Math.max(0, x) / 25);
91
+ const endurance = endRef.current;
92
+ const recovery = 1.5 + ln(endurance); // recover faster with Endurance
93
+ if (movingRef.current) {
94
+ runLoadRef.current = Math.max(0, runLoadRef.current + dt);
95
+ }
96
+ else {
97
+ runLoadRef.current = Math.max(0, runLoadRef.current - dt * recovery);
98
+ }
99
+ raf = requestAnimationFrame(tick);
100
+ };
101
+ lastTickRef.current = performance.now();
102
+ raf = requestAnimationFrame(tick);
103
+ return () => cancelAnimationFrame(raf);
104
+ }, []);
105
+ // Movement handlers
106
+ useEffect(() => {
107
+ const ln = (x) => Math.log(1 + Math.max(0, x) / 25);
108
+ const baseSpeed = () => {
109
+ const dex = dexRef.current;
110
+ const str = strRef.current;
111
+ const dexBoost = 0.25 * ln(dex);
112
+ const strBoost = 0.1 * ln(str);
113
+ return 1 + dexBoost + strBoost;
114
+ };
115
+ const currentSpeed = () => {
116
+ const endurance = endRef.current;
117
+ const cap = 4 + 2 * ln(endurance);
118
+ const load = runLoadRef.current;
119
+ if (load <= cap)
120
+ return baseSpeed();
121
+ const over = Math.min(1, (load - cap) / cap);
122
+ const fatigue = 1 - 0.5 * over; // down to 50%
123
+ return baseSpeed() * fatigue;
124
+ };
125
+ const handleKeyDown = (event) => {
126
+ const cameraState = cameraManager.getCamera(cameraId);
127
+ if (!cameraState)
128
+ return;
129
+ const position = [...cameraState.transform.position];
130
+ const target = [...cameraState.transform.target];
131
+ const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
132
+ const forward = normalize3(sub3(target, position), [0, 0, -1]);
133
+ const forwardY = forward[1];
134
+ const planarForward = normalize3([forward[0], 0, forward[2]], [0, 0, -1]);
135
+ const right = normalize3(cross3(planarForward, up), [1, 0, 0]);
136
+ const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
137
+ if (moveKeys.has(event.key)) {
138
+ movingKeys.current.add(event.key);
139
+ movingRef.current = true;
140
+ }
141
+ switch (event.key) {
142
+ case "w":
143
+ position[0] += planarForward[0] * currentSpeed();
144
+ position[1] += planarForward[1] * currentSpeed();
145
+ position[2] += planarForward[2] * currentSpeed();
146
+ break;
147
+ case "s":
148
+ position[0] -= planarForward[0] * currentSpeed();
149
+ position[1] -= planarForward[1] * currentSpeed();
150
+ position[2] -= planarForward[2] * currentSpeed();
151
+ break;
152
+ case "a":
153
+ position[0] -= right[0] * currentSpeed();
154
+ position[1] -= right[1] * currentSpeed();
155
+ position[2] -= right[2] * currentSpeed();
156
+ break;
157
+ case "d":
158
+ position[0] += right[0] * currentSpeed();
159
+ position[1] += right[1] * currentSpeed();
160
+ position[2] += right[2] * currentSpeed();
161
+ break;
162
+ case "q":
163
+ position[1] += currentSpeed();
164
+ break;
165
+ case "e":
166
+ position[1] -= currentSpeed();
167
+ break;
168
+ default:
169
+ break;
170
+ }
171
+ const nextTarget = add3(position, [
172
+ planarForward[0],
173
+ forwardY,
174
+ planarForward[2],
175
+ ]);
176
+ cameraManager.applyControl(cameraId, {
177
+ type: "set-look-at",
178
+ position,
179
+ target: nextTarget,
180
+ up,
181
+ }, { makeActive: true });
182
+ };
183
+ const handleKeyUp = (event) => {
184
+ const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
185
+ if (moveKeys.has(event.key)) {
186
+ movingKeys.current.delete(event.key);
187
+ if (movingKeys.current.size === 0)
188
+ movingRef.current = false;
189
+ }
190
+ };
191
+ window.addEventListener("keydown", handleKeyDown);
192
+ window.addEventListener("keyup", handleKeyUp);
193
+ return () => {
194
+ window.removeEventListener("keydown", handleKeyDown);
195
+ window.removeEventListener("keyup", handleKeyUp);
196
+ };
197
+ }, [cameraManager, cameraId]);
198
+ return _jsx(_Fragment, { children: children });
199
+ }
200
+ function WrappedPlayer({ cameraManager, cameraId, children, }) {
201
+ return (_jsx(PlayerStore.Provider, { children: _jsx(Player, { cameraManager: cameraManager, cameraId: cameraId, children: children }) }));
202
+ }
203
+ export { WrappedPlayer as Player };