@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.
- package/CHANGELOG.md +59 -0
- package/CODE_OF_CONDUCT.md +79 -0
- package/CONTRIBUTORS.md +27 -0
- package/LICENSE +203 -0
- package/README.md +70 -0
- package/SECURITY.md +17 -0
- package/dist/adaptivedpr.d.ts +2 -0
- package/dist/adaptivedpr.d.ts.map +1 -0
- package/dist/adaptivedpr.js +65 -0
- package/dist/camera/cameraRigProfile.d.ts +12 -0
- package/dist/camera/cameraRigProfile.d.ts.map +1 -0
- package/dist/camera/cameraRigProfile.js +18 -0
- package/dist/camera/managedCameraController.d.ts +49 -0
- package/dist/camera/managedCameraController.d.ts.map +1 -0
- package/dist/camera/managedCameraController.js +271 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/landscape.d.ts +2 -0
- package/dist/landscape.d.ts.map +1 -0
- package/dist/landscape.js +120 -0
- package/dist/player/player.d.ts +8 -0
- package/dist/player/player.d.ts.map +1 -0
- package/dist/player/player.js +203 -0
- package/dist/player/playerstore.d.ts +205 -0
- package/dist/player/playerstore.d.ts.map +1 -0
- package/dist/player/playerstore.js +500 -0
- package/dist/renderStateProvider.d.ts +57 -0
- package/dist/renderStateProvider.d.ts.map +1 -0
- package/dist/renderStateProvider.js +50 -0
- package/dist/renderer.d.ts +9 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +165 -0
- package/dist/scene.d.ts +7 -0
- package/dist/scene.d.ts.map +1 -0
- package/dist/scene.js +10 -0
- package/dist/shaders/fragment/landscapeFragmentShader.js +141 -0
- package/dist/shaders/landscapeShader.d.ts +13 -0
- package/dist/shaders/landscapeShader.d.ts.map +1 -0
- package/dist/shaders/landscapeShader.js +25 -0
- package/dist/shaders/vertex/landscapeVertexShader.js +67 -0
- package/dist/styles/renderer.module.css +90 -0
- package/dist/worldSpaceCompositor.d.ts +50 -0
- package/dist/worldSpaceCompositor.d.ts.map +1 -0
- package/dist/worldSpaceCompositor.js +159 -0
- package/dist/xr/rendererXrBridge.d.ts +12 -0
- package/dist/xr/rendererXrBridge.d.ts.map +1 -0
- package/dist/xr/rendererXrBridge.js +17 -0
- package/docs/adrs/adr-0001-renderer-package-scope.md +21 -0
- package/docs/adrs/adr-0002-public-repo-governance.md +24 -0
- package/docs/adrs/adr-0003-world-space-compositor-contracts.md +34 -0
- package/docs/adrs/adr-template.md +35 -0
- package/docs/design/0001-public-package-scope.md +18 -0
- package/docs/tdrs/index.md +3 -0
- package/docs/tdrs/tdr-0001-renderer-public-package-standards-alignment.md +19 -0
- package/legal/CLA-REGISTRY.csv +1 -0
- package/legal/CLA.md +22 -0
- package/legal/CORPORATE_CLA.md +57 -0
- package/legal/INDIVIDUAL_CLA.md +91 -0
- package/package.json +117 -0
- package/src/adaptivedpr.tsx +74 -0
- package/src/camera/cameraRigProfile.ts +29 -0
- package/src/camera/managedCameraController.tsx +401 -0
- package/src/global.d.ts +10 -0
- package/src/index.ts +3 -0
- package/src/landscape.tsx +321 -0
- package/src/player/player.tsx +257 -0
- package/src/player/playerstore.tsx +733 -0
- package/src/renderStateProvider.tsx +121 -0
- package/src/renderer.tsx +294 -0
- package/src/scene.tsx +42 -0
- package/src/shaders/fragment/landscapeFragmentShader.d.ts +4 -0
- package/src/shaders/fragment/landscapeFragmentShader.js +141 -0
- package/src/shaders/landscapeShader.tsx +39 -0
- package/src/shaders/vertex/landscapeVertexShader.d.ts +4 -0
- package/src/shaders/vertex/landscapeVertexShader.js +67 -0
- package/src/styles/renderer.module.css +90 -0
- package/src/worldSpaceCompositor.ts +265 -0
- package/src/xr/rendererXrBridge.ts +44 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
|
4
|
+
import { RigidBody } from "@react-three/rapier";
|
|
5
|
+
|
|
6
|
+
const HEX_RADIUS = 66;
|
|
7
|
+
//const TILE_RESOLUTION = 2048;
|
|
8
|
+
const AREA_WIDTH = 2000;
|
|
9
|
+
const AREA_HEIGHT = 2000;
|
|
10
|
+
|
|
11
|
+
// Generate a single high-resolution hexagon as BufferGeometry
|
|
12
|
+
function generateHexTile(radius: number): THREE.BufferGeometry {
|
|
13
|
+
const geom = new THREE.BufferGeometry();
|
|
14
|
+
const vertices: number[] = [];
|
|
15
|
+
const indices: number[] = [];
|
|
16
|
+
|
|
17
|
+
const segments = 30; // More segments for smoother curve
|
|
18
|
+
const angleStep = (Math.PI * 2) / segments;
|
|
19
|
+
|
|
20
|
+
// Center point
|
|
21
|
+
vertices.push(0, 0, 0);
|
|
22
|
+
|
|
23
|
+
// Outer ring vertices
|
|
24
|
+
for (let i = 0; i <= segments; i++) {
|
|
25
|
+
const angle = angleStep * i;
|
|
26
|
+
const x = radius * Math.cos(angle);
|
|
27
|
+
const z = -radius * Math.sin(angle); // flip Z axis for pointy top upward
|
|
28
|
+
vertices.push(x, 0, z);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create triangle fan indices
|
|
32
|
+
for (let i = 1; i <= segments; i++) {
|
|
33
|
+
indices.push(0, i, i + 1);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
geom.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
|
|
37
|
+
geom.setIndex(indices);
|
|
38
|
+
geom.computeVertexNormals();
|
|
39
|
+
return geom;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Axial to world coordinates (pointy-topped hex)
|
|
43
|
+
function axialToWorld(q: number, r: number, radius: number): [number, number] {
|
|
44
|
+
const x = ((radius * 3) / 2) * q;
|
|
45
|
+
const z = radius * Math.sqrt(3) * (r + q / 2);
|
|
46
|
+
return [x, z];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Simple seeded noise for per-tile deformation
|
|
50
|
+
function seededNoise(x: number, z: number, seed = 42): number {
|
|
51
|
+
const s = Math.sin(x * 12.9898 + z * 78.233 + seed) * 43758.5453;
|
|
52
|
+
return (s - Math.floor(s)) * 2 - 1;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Deform only the interior (non-edge) vertices of a hex tile
|
|
56
|
+
function deformHexInterior(
|
|
57
|
+
geom: THREE.BufferGeometry,
|
|
58
|
+
radius: number,
|
|
59
|
+
seed = 42
|
|
60
|
+
): void {
|
|
61
|
+
const pos = geom.attributes.position as THREE.BufferAttribute;
|
|
62
|
+
const vertexCount = pos.count;
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < vertexCount; i++) {
|
|
65
|
+
const x = pos.getX(i);
|
|
66
|
+
const z = pos.getZ(i);
|
|
67
|
+
const r = Math.sqrt(x * x + z * z);
|
|
68
|
+
|
|
69
|
+
// Only deform vertices inside the hex edge (leave edge vertices flat)
|
|
70
|
+
if (r < radius * 0.98) {
|
|
71
|
+
// Blend multiple nearby noise samples for smoother curvature
|
|
72
|
+
const n = (dx: number, dz: number) => seededNoise(x + dx, z + dz, seed);
|
|
73
|
+
const smoothY =
|
|
74
|
+
(n(0, 0) * 4 +
|
|
75
|
+
n(-1, 0) +
|
|
76
|
+
n(1, 0) +
|
|
77
|
+
n(0, -1) +
|
|
78
|
+
n(0, 1) +
|
|
79
|
+
n(-1, -1) +
|
|
80
|
+
n(1, 1) +
|
|
81
|
+
n(-1, 1) +
|
|
82
|
+
n(1, -1)) /
|
|
83
|
+
12;
|
|
84
|
+
pos.setY(i, smoothY);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
pos.needsUpdate = true;
|
|
89
|
+
geom.computeVertexNormals();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function generateHexDepthMap(
|
|
93
|
+
cols: number,
|
|
94
|
+
rows: number,
|
|
95
|
+
seed = 42
|
|
96
|
+
): number[][] {
|
|
97
|
+
const map: number[][] = [];
|
|
98
|
+
for (let q = 0; q < cols; q++) {
|
|
99
|
+
const row: number[] = [];
|
|
100
|
+
for (let r = 0; r < rows; r++) {
|
|
101
|
+
const value = Math.sin((q + seed) * 0.3) * Math.cos((r + seed) * 0.3);
|
|
102
|
+
row.push(value);
|
|
103
|
+
}
|
|
104
|
+
map.push(row);
|
|
105
|
+
}
|
|
106
|
+
return map;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function Landscape() {
|
|
110
|
+
const geometry = useMemo(() => {
|
|
111
|
+
const radius = HEX_RADIUS;
|
|
112
|
+
const tiles: THREE.BufferGeometry[] = [];
|
|
113
|
+
|
|
114
|
+
const spacingX = (3 / 2) * radius;
|
|
115
|
+
const spacingZ = Math.sqrt(3) * radius;
|
|
116
|
+
const cols = Math.ceil(AREA_WIDTH / spacingX);
|
|
117
|
+
const rows = Math.ceil(AREA_HEIGHT / spacingZ);
|
|
118
|
+
|
|
119
|
+
const offset = Math.floor(cols / 2);
|
|
120
|
+
const depthMap = generateHexDepthMap(cols, rows);
|
|
121
|
+
const elevationScale = 20;
|
|
122
|
+
|
|
123
|
+
const baseHex = generateHexTile(radius);
|
|
124
|
+
|
|
125
|
+
for (let q = -Math.floor(cols / 2); q < Math.ceil(cols / 2); q++) {
|
|
126
|
+
for (let r = -Math.floor(rows / 2); r < Math.ceil(rows / 2); r++) {
|
|
127
|
+
const hex = baseHex.clone();
|
|
128
|
+
|
|
129
|
+
const dq = q + offset;
|
|
130
|
+
const dr = r + offset;
|
|
131
|
+
const elevation = depthMap?.[dq]?.[dr] ?? 0;
|
|
132
|
+
|
|
133
|
+
deformHexInterior(
|
|
134
|
+
hex,
|
|
135
|
+
radius,
|
|
136
|
+
42 + q * 1000 + r + Math.floor(elevation * 100)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
const [x, z] = axialToWorld(q, r, radius);
|
|
140
|
+
hex.translate(x, elevation * elevationScale, z);
|
|
141
|
+
|
|
142
|
+
tiles.push(hex);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const merged = BufferGeometryUtils.mergeGeometries(tiles, false);
|
|
147
|
+
// Remove normals before merging vertices to ensure proper welding
|
|
148
|
+
if (merged.attributes.normal) {
|
|
149
|
+
merged.deleteAttribute("normal");
|
|
150
|
+
}
|
|
151
|
+
const stitched = BufferGeometryUtils.mergeVertices(merged, 1e-2);
|
|
152
|
+
stitched.computeVertexNormals();
|
|
153
|
+
return stitched;
|
|
154
|
+
}, []);
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<RigidBody type="fixed" colliders="trimesh">
|
|
158
|
+
<mesh geometry={geometry} castShadow receiveShadow>
|
|
159
|
+
<landscapeShaderMaterial color="#77aa66" />
|
|
160
|
+
</mesh>
|
|
161
|
+
</RigidBody>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
|
|
167
|
+
import * as THREE from "three";
|
|
168
|
+
import * as BufferGeometryUtils from "three/examples/jsm/utils/BufferGeometryUtils.js";
|
|
169
|
+
import { useFrame } from "@react-three/fiber";
|
|
170
|
+
import { useRef, useMemo } from "react";
|
|
171
|
+
import { Mesh } from "three";
|
|
172
|
+
|
|
173
|
+
import "./shaders/landscapeShader.js";
|
|
174
|
+
|
|
175
|
+
// Deterministic, coherent angle function for seamless tile rotation
|
|
176
|
+
function coherentAngle(x: number, y: number, scale: number = 0.2) {
|
|
177
|
+
const seed = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
|
|
178
|
+
const noise = seed - Math.floor(seed);
|
|
179
|
+
return (noise - 0.5) * scale;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function generateHexTile(
|
|
183
|
+
edgeLength: number,
|
|
184
|
+
resolution: number
|
|
185
|
+
): THREE.BufferGeometry {
|
|
186
|
+
const radius = edgeLength;
|
|
187
|
+
const height = Math.sqrt(3) * radius;
|
|
188
|
+
const width = 2 * radius;
|
|
189
|
+
|
|
190
|
+
const geom = new THREE.PlaneGeometry(width, height, resolution, resolution);
|
|
191
|
+
|
|
192
|
+
// Mask out corners beyond hexagon bounds
|
|
193
|
+
const pos = geom.attributes.position;
|
|
194
|
+
const indicesToKeep = [];
|
|
195
|
+
|
|
196
|
+
for (let i = 0; i < pos.count; i++) {
|
|
197
|
+
const x = pos.getX(i);
|
|
198
|
+
const y = pos.getY(i); // plane lies flat, so this is along Z in world
|
|
199
|
+
const px = Math.abs(x);
|
|
200
|
+
const py = Math.abs(y);
|
|
201
|
+
const angle = Math.atan2(py, px);
|
|
202
|
+
|
|
203
|
+
const maxX = radius;
|
|
204
|
+
const maxY = Math.tan(Math.PI / 6) * (maxX - px);
|
|
205
|
+
if (py < height / 2 && py < maxY + 1e-2) {
|
|
206
|
+
indicesToKeep.push(i);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Build new geometry with only the allowed vertices
|
|
211
|
+
const indexAttr = [];
|
|
212
|
+
for (let i = 0; i < geom.index!.count; i += 3) {
|
|
213
|
+
const a = geom.index!.getX(i);
|
|
214
|
+
const b = geom.index!.getX(i + 1);
|
|
215
|
+
const c = geom.index!.getX(i + 2);
|
|
216
|
+
if (
|
|
217
|
+
indicesToKeep.includes(a) &&
|
|
218
|
+
indicesToKeep.includes(b) &&
|
|
219
|
+
indicesToKeep.includes(c)
|
|
220
|
+
) {
|
|
221
|
+
indexAttr.push(a, b, c);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const trimmed = geom.clone();
|
|
226
|
+
trimmed.setIndex(indexAttr);
|
|
227
|
+
trimmed.computeVertexNormals();
|
|
228
|
+
return trimmed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let persistedGeometry: THREE.BufferGeometry | undefined = undefined;
|
|
232
|
+
|
|
233
|
+
interface LandscapeProps {
|
|
234
|
+
width?: number;
|
|
235
|
+
height?: number;
|
|
236
|
+
resolution?: number;
|
|
237
|
+
children?: React.ReactNode;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function Landscape({
|
|
241
|
+
width = 100,
|
|
242
|
+
height = 100,
|
|
243
|
+
resolution = 64,
|
|
244
|
+
children,
|
|
245
|
+
}: LandscapeProps) {
|
|
246
|
+
const ref = useRef<Mesh>(null);
|
|
247
|
+
const materialRef = useRef<any>(null);
|
|
248
|
+
|
|
249
|
+
useFrame(({ clock }) => {
|
|
250
|
+
if (materialRef.current) {
|
|
251
|
+
materialRef.current.uniforms.time.value = clock.getElapsedTime();
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
const radius = 6.6;
|
|
256
|
+
const hexWidth = 2 * radius;
|
|
257
|
+
const hexHeight = Math.sqrt(3) * radius;
|
|
258
|
+
|
|
259
|
+
const spacingX = (3 / 2) * radius;
|
|
260
|
+
const spacingZ = hexHeight;
|
|
261
|
+
|
|
262
|
+
const cols = Math.ceil(width / spacingX);
|
|
263
|
+
const rows = Math.ceil(height / spacingZ);
|
|
264
|
+
|
|
265
|
+
const geometry = useMemo(() => {
|
|
266
|
+
if (persistedGeometry !== undefined) return persistedGeometry;
|
|
267
|
+
|
|
268
|
+
const geometries: THREE.BufferGeometry[] = [];
|
|
269
|
+
|
|
270
|
+
for (let x = 0; x < cols; x++) {
|
|
271
|
+
for (let y = 0; y < rows; y++) {
|
|
272
|
+
const overlap = 0.1;
|
|
273
|
+
// Hex grid layout: pointy-top
|
|
274
|
+
const hexHeight = Math.sqrt(3) * radius;
|
|
275
|
+
const q = x;
|
|
276
|
+
const r = y;
|
|
277
|
+
const posX = radius * 1.5 * q;
|
|
278
|
+
const posZ = Math.sqrt(3) * radius * (r + q / 2);
|
|
279
|
+
|
|
280
|
+
const dx = x - cols / 2 + 0.5;
|
|
281
|
+
const dy = y - rows / 2 + 0.5;
|
|
282
|
+
const distance = Math.sqrt(dx * dx + dy * dy);
|
|
283
|
+
const angleFactor = distance / Math.max(cols, rows);
|
|
284
|
+
|
|
285
|
+
const rotX = coherentAngle(x, y) * angleFactor;
|
|
286
|
+
const rotY = coherentAngle(y, x) * angleFactor;
|
|
287
|
+
|
|
288
|
+
const baseGeom = generateHexTile(radius, resolution);
|
|
289
|
+
baseGeom.rotateX(-Math.PI / 2); // flat base
|
|
290
|
+
baseGeom.rotateX(rotX);
|
|
291
|
+
baseGeom.rotateZ(rotY);
|
|
292
|
+
baseGeom.translate(posX - width / 2, 0, posZ - height / 2);
|
|
293
|
+
|
|
294
|
+
geometries.push(baseGeom);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
persistedGeometry = BufferGeometryUtils.mergeGeometries(geometries, false);
|
|
299
|
+
|
|
300
|
+
// Post-process: stitch vertices along tile edges to ensure seamless normals
|
|
301
|
+
if (persistedGeometry) {
|
|
302
|
+
const tolerance = 1e-3; // merge close vertices
|
|
303
|
+
const merged = BufferGeometryUtils.mergeVertices(
|
|
304
|
+
persistedGeometry,
|
|
305
|
+
tolerance
|
|
306
|
+
);
|
|
307
|
+
persistedGeometry.dispose(); // clean up old geometry
|
|
308
|
+
persistedGeometry = merged;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return persistedGeometry;
|
|
312
|
+
}, [cols, rows, radius, width, height, resolution]);
|
|
313
|
+
|
|
314
|
+
//<landscapeShaderMaterial ref={materialRef} />
|
|
315
|
+
return (
|
|
316
|
+
<mesh ref={ref} geometry={geometry} receiveShadow>
|
|
317
|
+
{children}
|
|
318
|
+
</mesh>
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
*/
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import React, { useEffect, useRef, type PropsWithChildren } from "react";
|
|
2
|
+
import type { CameraManager, Vec3 } from "@plasius/gpu-camera";
|
|
3
|
+
import { PlayerStore } from "./playerstore.js";
|
|
4
|
+
|
|
5
|
+
function length3(value: Vec3): number {
|
|
6
|
+
return Math.hypot(value[0], value[1], value[2]);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function normalize3(value: Vec3, fallback: Vec3): Vec3 {
|
|
10
|
+
const len = length3(value);
|
|
11
|
+
if (len <= Number.EPSILON) {
|
|
12
|
+
return [...fallback];
|
|
13
|
+
}
|
|
14
|
+
return [value[0] / len, value[1] / len, value[2] / len];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function add3(a: Vec3, b: Vec3): Vec3 {
|
|
18
|
+
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function sub3(a: Vec3, b: Vec3): Vec3 {
|
|
22
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function cross3(a: Vec3, b: Vec3): Vec3 {
|
|
26
|
+
return [
|
|
27
|
+
a[1] * b[2] - a[2] * b[1],
|
|
28
|
+
a[2] * b[0] - a[0] * b[2],
|
|
29
|
+
a[0] * b[1] - a[1] * b[0],
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function Player({
|
|
34
|
+
cameraManager,
|
|
35
|
+
cameraId,
|
|
36
|
+
children,
|
|
37
|
+
}: PropsWithChildren<{ cameraManager: CameraManager; cameraId: string }>) {
|
|
38
|
+
// Local refs for movement-relevant stats (used by the rAF ticker)
|
|
39
|
+
const strRef = useRef(1);
|
|
40
|
+
const dexRef = useRef(1);
|
|
41
|
+
const endRef = useRef(1);
|
|
42
|
+
const movingKeys = useRef<Set<string>>(new Set());
|
|
43
|
+
const movingRef = useRef(false);
|
|
44
|
+
const runLoadRef = useRef(0); // seconds of continuous run load
|
|
45
|
+
const lastTickRef = useRef(performance.now());
|
|
46
|
+
|
|
47
|
+
// Subscribe via inline selector (useSyncExternalStore under the hood)
|
|
48
|
+
const physEff = PlayerStore.useSelector((s) => {
|
|
49
|
+
const phys = s?.attributesBase?.physical ?? {
|
|
50
|
+
strength: 1,
|
|
51
|
+
dexterity: 1,
|
|
52
|
+
endurance: 1,
|
|
53
|
+
};
|
|
54
|
+
const g = s?.attributesGear ?? {};
|
|
55
|
+
const e = s?.attributesEffects ?? {};
|
|
56
|
+
|
|
57
|
+
let strength =
|
|
58
|
+
(phys.strength ?? 1) + (g.strength ?? 0) + (e.strength ?? 0);
|
|
59
|
+
let dexterity =
|
|
60
|
+
(phys.dexterity ?? 1) + (g.dexterity ?? 0) + (e.dexterity ?? 0);
|
|
61
|
+
let endurance =
|
|
62
|
+
(phys.endurance ?? 1) + (g.endurance ?? 0) + (e.endurance ?? 0);
|
|
63
|
+
|
|
64
|
+
// Add equipped item modifiers
|
|
65
|
+
if (s?.equipment && s?.items) {
|
|
66
|
+
for (const slot of Object.keys(s.equipment)) {
|
|
67
|
+
const itemId = (s.equipment as any)[slot];
|
|
68
|
+
const item = s.items[itemId];
|
|
69
|
+
const m = item?.modifiers as
|
|
70
|
+
| Partial<Record<"strength" | "dexterity" | "endurance", number>>
|
|
71
|
+
| undefined;
|
|
72
|
+
if (m) {
|
|
73
|
+
strength += m.strength ?? 0;
|
|
74
|
+
dexterity += m.dexterity ?? 0;
|
|
75
|
+
endurance += m.endurance ?? 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Add status effect attribute modifiers
|
|
81
|
+
if (s?.effects) {
|
|
82
|
+
for (const eff of Object.values(s.effects) as any[]) {
|
|
83
|
+
const mods = eff?.modifiers?.attributes as
|
|
84
|
+
| Partial<Record<"strength" | "dexterity" | "endurance", number>>
|
|
85
|
+
| undefined;
|
|
86
|
+
if (mods) {
|
|
87
|
+
strength += mods.strength ?? 0;
|
|
88
|
+
dexterity += mods.dexterity ?? 0;
|
|
89
|
+
endurance += mods.endurance ?? 0;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return { strength, dexterity, endurance };
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
// Keep refs in sync for rAF without causing extra renders
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
strRef.current = physEff.strength;
|
|
100
|
+
dexRef.current = physEff.dexterity;
|
|
101
|
+
endRef.current = physEff.endurance;
|
|
102
|
+
}, [physEff.strength, physEff.dexterity, physEff.endurance]);
|
|
103
|
+
|
|
104
|
+
useEffect(() => {
|
|
105
|
+
window.focus();
|
|
106
|
+
}, []);
|
|
107
|
+
|
|
108
|
+
// Fatigue/exhaustion ticker
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
let raf = 0;
|
|
111
|
+
const tick = () => {
|
|
112
|
+
const now = performance.now();
|
|
113
|
+
const dt = (now - lastTickRef.current) / 1000;
|
|
114
|
+
lastTickRef.current = now;
|
|
115
|
+
|
|
116
|
+
const ln = (x: number) => Math.log(1 + Math.max(0, x) / 25);
|
|
117
|
+
const endurance = endRef.current;
|
|
118
|
+
const recovery = 1.5 + ln(endurance); // recover faster with Endurance
|
|
119
|
+
|
|
120
|
+
if (movingRef.current) {
|
|
121
|
+
runLoadRef.current = Math.max(0, runLoadRef.current + dt);
|
|
122
|
+
} else {
|
|
123
|
+
runLoadRef.current = Math.max(0, runLoadRef.current - dt * recovery);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
raf = requestAnimationFrame(tick);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
lastTickRef.current = performance.now();
|
|
130
|
+
raf = requestAnimationFrame(tick);
|
|
131
|
+
return () => cancelAnimationFrame(raf);
|
|
132
|
+
}, []);
|
|
133
|
+
|
|
134
|
+
// Movement handlers
|
|
135
|
+
useEffect(() => {
|
|
136
|
+
const ln = (x: number) => Math.log(1 + Math.max(0, x) / 25);
|
|
137
|
+
|
|
138
|
+
const baseSpeed = () => {
|
|
139
|
+
const dex = dexRef.current;
|
|
140
|
+
const str = strRef.current;
|
|
141
|
+
const dexBoost = 0.25 * ln(dex);
|
|
142
|
+
const strBoost = 0.1 * ln(str);
|
|
143
|
+
return 1 + dexBoost + strBoost;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const currentSpeed = () => {
|
|
147
|
+
const endurance = endRef.current;
|
|
148
|
+
const cap = 4 + 2 * ln(endurance);
|
|
149
|
+
const load = runLoadRef.current;
|
|
150
|
+
if (load <= cap) return baseSpeed();
|
|
151
|
+
const over = Math.min(1, (load - cap) / cap);
|
|
152
|
+
const fatigue = 1 - 0.5 * over; // down to 50%
|
|
153
|
+
return baseSpeed() * fatigue;
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
157
|
+
const cameraState = cameraManager.getCamera(cameraId);
|
|
158
|
+
if (!cameraState) return;
|
|
159
|
+
|
|
160
|
+
const position = [...cameraState.transform.position] as Vec3;
|
|
161
|
+
const target = [...cameraState.transform.target] as Vec3;
|
|
162
|
+
const up = normalize3(cameraState.transform.up ?? [0, 1, 0], [0, 1, 0]);
|
|
163
|
+
|
|
164
|
+
const forward = normalize3(sub3(target, position), [0, 0, -1]);
|
|
165
|
+
const forwardY = forward[1];
|
|
166
|
+
const planarForward = normalize3([forward[0], 0, forward[2]], [0, 0, -1]);
|
|
167
|
+
const right = normalize3(cross3(planarForward, up), [1, 0, 0]);
|
|
168
|
+
|
|
169
|
+
const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
|
|
170
|
+
if (moveKeys.has(event.key)) {
|
|
171
|
+
movingKeys.current.add(event.key);
|
|
172
|
+
movingRef.current = true;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
switch (event.key) {
|
|
176
|
+
case "w":
|
|
177
|
+
position[0] += planarForward[0] * currentSpeed();
|
|
178
|
+
position[1] += planarForward[1] * currentSpeed();
|
|
179
|
+
position[2] += planarForward[2] * currentSpeed();
|
|
180
|
+
break;
|
|
181
|
+
case "s":
|
|
182
|
+
position[0] -= planarForward[0] * currentSpeed();
|
|
183
|
+
position[1] -= planarForward[1] * currentSpeed();
|
|
184
|
+
position[2] -= planarForward[2] * currentSpeed();
|
|
185
|
+
break;
|
|
186
|
+
case "a":
|
|
187
|
+
position[0] -= right[0] * currentSpeed();
|
|
188
|
+
position[1] -= right[1] * currentSpeed();
|
|
189
|
+
position[2] -= right[2] * currentSpeed();
|
|
190
|
+
break;
|
|
191
|
+
case "d":
|
|
192
|
+
position[0] += right[0] * currentSpeed();
|
|
193
|
+
position[1] += right[1] * currentSpeed();
|
|
194
|
+
position[2] += right[2] * currentSpeed();
|
|
195
|
+
break;
|
|
196
|
+
case "q":
|
|
197
|
+
position[1] += currentSpeed();
|
|
198
|
+
break;
|
|
199
|
+
case "e":
|
|
200
|
+
position[1] -= currentSpeed();
|
|
201
|
+
break;
|
|
202
|
+
default:
|
|
203
|
+
break;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const nextTarget = add3(position, [
|
|
207
|
+
planarForward[0],
|
|
208
|
+
forwardY,
|
|
209
|
+
planarForward[2],
|
|
210
|
+
]);
|
|
211
|
+
|
|
212
|
+
cameraManager.applyControl(
|
|
213
|
+
cameraId,
|
|
214
|
+
{
|
|
215
|
+
type: "set-look-at",
|
|
216
|
+
position,
|
|
217
|
+
target: nextTarget,
|
|
218
|
+
up,
|
|
219
|
+
},
|
|
220
|
+
{ makeActive: true }
|
|
221
|
+
);
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const handleKeyUp = (event: KeyboardEvent) => {
|
|
225
|
+
const moveKeys = new Set(["w", "a", "s", "d", "q", "e"]);
|
|
226
|
+
if (moveKeys.has(event.key)) {
|
|
227
|
+
movingKeys.current.delete(event.key);
|
|
228
|
+
if (movingKeys.current.size === 0) movingRef.current = false;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
233
|
+
window.addEventListener("keyup", handleKeyUp);
|
|
234
|
+
return () => {
|
|
235
|
+
window.removeEventListener("keydown", handleKeyDown);
|
|
236
|
+
window.removeEventListener("keyup", handleKeyUp);
|
|
237
|
+
};
|
|
238
|
+
}, [cameraManager, cameraId]);
|
|
239
|
+
|
|
240
|
+
return <>{children}</>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function WrappedPlayer({
|
|
244
|
+
cameraManager,
|
|
245
|
+
cameraId,
|
|
246
|
+
children,
|
|
247
|
+
}: PropsWithChildren<{ cameraManager: CameraManager; cameraId: string }>) {
|
|
248
|
+
return (
|
|
249
|
+
<PlayerStore.Provider>
|
|
250
|
+
<Player cameraManager={cameraManager} cameraId={cameraId}>
|
|
251
|
+
{children}
|
|
252
|
+
</Player>
|
|
253
|
+
</PlayerStore.Provider>
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export { WrappedPlayer as Player };
|