@almadar/ui 5.95.0 → 5.97.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/dist/avl/index.cjs +1665 -1416
- package/dist/avl/index.js +660 -411
- package/dist/components/core/atoms/AtlasImage.d.ts +58 -0
- package/dist/components/core/atoms/index.d.ts +1 -0
- package/dist/components/game/2d/molecules/Canvas2D.d.ts +4 -0
- package/dist/components/game/2d/templates/GameShell.d.ts +19 -23
- package/dist/components/game/3d/index.cjs +893 -439
- package/dist/components/game/3d/index.js +726 -268
- package/dist/components/game/3d/molecules/GameCanvas3D.d.ts +34 -3
- package/dist/components/game/3d/molecules/ModelLoader.d.ts +9 -1
- package/dist/components/game/3d/molecules/index.d.ts +0 -1
- package/dist/components/game/shared/isometricTypes.d.ts +2 -0
- package/dist/components/index.cjs +1645 -1392
- package/dist/components/index.js +637 -387
- package/dist/marketing/index.cjs +208 -73
- package/dist/marketing/index.js +166 -28
- package/dist/providers/index.cjs +1533 -1284
- package/dist/providers/index.js +636 -387
- package/dist/runtime/index.cjs +1526 -1277
- package/dist/runtime/index.js +640 -391
- package/package.json +1 -1
- package/dist/components/game/3d/molecules/Canvas3D.d.ts +0 -45
- /package/dist/{components/game/shared → lib}/atlasSlice.d.ts +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var React8 = require('react');
|
|
4
4
|
var fiber = require('@react-three/fiber');
|
|
5
5
|
var THREE6 = require('three');
|
|
6
6
|
var jsxRuntime = require('react/jsx-runtime');
|
|
@@ -18,8 +18,6 @@ var LucideIcons = require('lucide-react');
|
|
|
18
18
|
var postprocessing = require('@react-three/postprocessing');
|
|
19
19
|
var hooks = require('@almadar/ui/hooks');
|
|
20
20
|
|
|
21
|
-
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
22
|
-
|
|
23
21
|
function _interopNamespace(e) {
|
|
24
22
|
if (e && e.__esModule) return e;
|
|
25
23
|
var n = Object.create(null);
|
|
@@ -38,7 +36,7 @@ function _interopNamespace(e) {
|
|
|
38
36
|
return Object.freeze(n);
|
|
39
37
|
}
|
|
40
38
|
|
|
41
|
-
var
|
|
39
|
+
var React8__namespace = /*#__PURE__*/_interopNamespace(React8);
|
|
42
40
|
var THREE6__namespace = /*#__PURE__*/_interopNamespace(THREE6);
|
|
43
41
|
var LucideIcons__namespace = /*#__PURE__*/_interopNamespace(LucideIcons);
|
|
44
42
|
|
|
@@ -47,8 +45,8 @@ var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { en
|
|
|
47
45
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
48
46
|
function Scene3D({ background = "#1a1a2e", fog, children }) {
|
|
49
47
|
const { scene } = fiber.useThree();
|
|
50
|
-
const initializedRef =
|
|
51
|
-
|
|
48
|
+
const initializedRef = React8.useRef(false);
|
|
49
|
+
React8.useEffect(() => {
|
|
52
50
|
if (initializedRef.current) return;
|
|
53
51
|
initializedRef.current = true;
|
|
54
52
|
if (background.startsWith("#") || background.startsWith("rgb")) {
|
|
@@ -69,7 +67,7 @@ function Scene3D({ background = "#1a1a2e", fog, children }) {
|
|
|
69
67
|
}, [scene, background, fog]);
|
|
70
68
|
return /* @__PURE__ */ jsxRuntime.jsx(jsxRuntime.Fragment, { children });
|
|
71
69
|
}
|
|
72
|
-
var Camera3D =
|
|
70
|
+
var Camera3D = React8.forwardRef(
|
|
73
71
|
({
|
|
74
72
|
mode = "isometric",
|
|
75
73
|
position = [10, 10, 10],
|
|
@@ -82,10 +80,10 @@ var Camera3D = React6.forwardRef(
|
|
|
82
80
|
onChange
|
|
83
81
|
}, ref) => {
|
|
84
82
|
const { camera, set, viewport } = fiber.useThree();
|
|
85
|
-
const controlsRef =
|
|
86
|
-
const initialPosition =
|
|
87
|
-
const initialTarget =
|
|
88
|
-
|
|
83
|
+
const controlsRef = React8.useRef(null);
|
|
84
|
+
const initialPosition = React8.useRef(new THREE6__namespace.Vector3(...position));
|
|
85
|
+
const initialTarget = React8.useRef(new THREE6__namespace.Vector3(...target));
|
|
86
|
+
React8.useEffect(() => {
|
|
89
87
|
let newCamera;
|
|
90
88
|
if (mode === "isometric") {
|
|
91
89
|
const aspect = viewport.aspect;
|
|
@@ -116,7 +114,7 @@ var Camera3D = React6.forwardRef(
|
|
|
116
114
|
onChange(camera);
|
|
117
115
|
}
|
|
118
116
|
});
|
|
119
|
-
|
|
117
|
+
React8.useImperativeHandle(ref, () => ({
|
|
120
118
|
getCamera: () => camera,
|
|
121
119
|
setPosition: (x, y, z) => {
|
|
122
120
|
camera.position.set(x, y, z);
|
|
@@ -258,7 +256,7 @@ function Canvas3DLoadingState({
|
|
|
258
256
|
] });
|
|
259
257
|
}
|
|
260
258
|
var log = logger.createLogger("almadar:ui:game:canvas3d:error-boundary");
|
|
261
|
-
var Canvas3DErrorBoundary = class extends
|
|
259
|
+
var Canvas3DErrorBoundary = class extends React8.Component {
|
|
262
260
|
constructor(props) {
|
|
263
261
|
super(props);
|
|
264
262
|
__publicField(this, "handleReset", () => {
|
|
@@ -330,6 +328,36 @@ var Canvas3DErrorBoundary = class extends React6.Component {
|
|
|
330
328
|
}
|
|
331
329
|
};
|
|
332
330
|
var log2 = logger.createLogger("almadar:ui:game:model-loader");
|
|
331
|
+
var gltfCache = /* @__PURE__ */ new Map();
|
|
332
|
+
function loadGltfCached(url, assetRoot) {
|
|
333
|
+
const key = `${assetRoot}|${url}`;
|
|
334
|
+
let entry = gltfCache.get(key);
|
|
335
|
+
if (!entry) {
|
|
336
|
+
const pending = {
|
|
337
|
+
promise: new Promise((resolveDone) => {
|
|
338
|
+
const loader = new GLTFLoader.GLTFLoader();
|
|
339
|
+
loader.setResourcePath(assetRoot);
|
|
340
|
+
loader.load(
|
|
341
|
+
url,
|
|
342
|
+
(gltf) => {
|
|
343
|
+
log2.debug("Loaded", { url, clips: gltf.animations.length });
|
|
344
|
+
pending.gltf = { scene: gltf.scene, animations: gltf.animations };
|
|
345
|
+
resolveDone();
|
|
346
|
+
},
|
|
347
|
+
void 0,
|
|
348
|
+
(err) => {
|
|
349
|
+
log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
350
|
+
pending.error = err instanceof Error ? err : new Error(String(err));
|
|
351
|
+
resolveDone();
|
|
352
|
+
}
|
|
353
|
+
);
|
|
354
|
+
})
|
|
355
|
+
};
|
|
356
|
+
entry = pending;
|
|
357
|
+
gltfCache.set(key, entry);
|
|
358
|
+
}
|
|
359
|
+
return entry;
|
|
360
|
+
}
|
|
333
361
|
function detectAssetRoot(modelUrl) {
|
|
334
362
|
const idx = modelUrl.indexOf("/3d/");
|
|
335
363
|
if (idx !== -1) {
|
|
@@ -337,42 +365,30 @@ function detectAssetRoot(modelUrl) {
|
|
|
337
365
|
}
|
|
338
366
|
return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
|
|
339
367
|
}
|
|
368
|
+
function stateFromEntry(entry) {
|
|
369
|
+
if (!entry) return { model: null, clips: [], isLoading: false, error: null };
|
|
370
|
+
if (entry.gltf) return { model: entry.gltf.scene, clips: entry.gltf.animations, isLoading: false, error: null };
|
|
371
|
+
return { model: null, clips: [], isLoading: !entry.error, error: entry.error ?? null };
|
|
372
|
+
}
|
|
340
373
|
function useGLTFModel(url, resourceBasePath) {
|
|
341
|
-
const [state, setState] =
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
});
|
|
346
|
-
React6.useEffect(() => {
|
|
374
|
+
const [state, setState] = React8.useState(
|
|
375
|
+
() => stateFromEntry(url ? loadGltfCached(url, resourceBasePath || detectAssetRoot(url)) : null)
|
|
376
|
+
);
|
|
377
|
+
React8.useEffect(() => {
|
|
347
378
|
if (!url) {
|
|
348
|
-
setState({ model: null, isLoading: false, error: null });
|
|
379
|
+
setState({ model: null, clips: [], isLoading: false, error: null });
|
|
349
380
|
return;
|
|
350
381
|
}
|
|
351
|
-
|
|
352
|
-
setState((
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
model: gltf.scene,
|
|
362
|
-
isLoading: false,
|
|
363
|
-
error: null
|
|
364
|
-
});
|
|
365
|
-
},
|
|
366
|
-
void 0,
|
|
367
|
-
(err) => {
|
|
368
|
-
log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
|
|
369
|
-
setState({
|
|
370
|
-
model: null,
|
|
371
|
-
isLoading: false,
|
|
372
|
-
error: err instanceof Error ? err : new Error(String(err))
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
);
|
|
382
|
+
const entry = loadGltfCached(url, resourceBasePath || detectAssetRoot(url));
|
|
383
|
+
setState(stateFromEntry(entry));
|
|
384
|
+
if (entry.gltf || entry.error) return;
|
|
385
|
+
let cancelled = false;
|
|
386
|
+
void entry.promise.then(() => {
|
|
387
|
+
if (!cancelled) setState(stateFromEntry(entry));
|
|
388
|
+
});
|
|
389
|
+
return () => {
|
|
390
|
+
cancelled = true;
|
|
391
|
+
};
|
|
376
392
|
}, [url, resourceBasePath]);
|
|
377
393
|
return state;
|
|
378
394
|
}
|
|
@@ -388,22 +404,45 @@ function ModelLoader({
|
|
|
388
404
|
fallbackGeometry = "box",
|
|
389
405
|
castShadow = true,
|
|
390
406
|
receiveShadow = true,
|
|
391
|
-
resourceBasePath
|
|
407
|
+
resourceBasePath,
|
|
408
|
+
animation,
|
|
409
|
+
tint
|
|
392
410
|
}) {
|
|
393
|
-
const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
|
|
394
|
-
const model =
|
|
411
|
+
const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
|
|
412
|
+
const model = React8.useMemo(() => {
|
|
395
413
|
if (!loadedModel) return null;
|
|
396
414
|
const cloned = SkeletonUtils.clone(loadedModel);
|
|
397
415
|
cloned.updateMatrixWorld(true);
|
|
416
|
+
const tintColor = tint ? new THREE6__namespace.Color(tint) : null;
|
|
398
417
|
cloned.traverse((child) => {
|
|
399
418
|
if (child instanceof THREE6__namespace.Mesh) {
|
|
400
419
|
child.castShadow = castShadow;
|
|
401
420
|
child.receiveShadow = receiveShadow;
|
|
421
|
+
if (tintColor && child.material instanceof THREE6__namespace.MeshStandardMaterial) {
|
|
422
|
+
const mat = child.material.clone();
|
|
423
|
+
mat.color.multiply(tintColor);
|
|
424
|
+
child.material = mat;
|
|
425
|
+
}
|
|
402
426
|
}
|
|
403
427
|
});
|
|
404
428
|
return cloned;
|
|
405
|
-
}, [loadedModel, castShadow, receiveShadow]);
|
|
406
|
-
const
|
|
429
|
+
}, [loadedModel, castShadow, receiveShadow, tint]);
|
|
430
|
+
const mixer = React8.useMemo(() => model ? new THREE6__namespace.AnimationMixer(model) : null, [model]);
|
|
431
|
+
React8.useEffect(() => {
|
|
432
|
+
if (!mixer || !animation || clips.length === 0) return;
|
|
433
|
+
const wanted = animation.toLowerCase();
|
|
434
|
+
const clip = clips.find((c) => c.name === animation) ?? clips.find((c) => c.name.toLowerCase() === wanted);
|
|
435
|
+
if (!clip) return;
|
|
436
|
+
const action = mixer.clipAction(clip);
|
|
437
|
+
action.reset().fadeIn(0.15).play();
|
|
438
|
+
return () => {
|
|
439
|
+
action.fadeOut(0.15);
|
|
440
|
+
};
|
|
441
|
+
}, [mixer, clips, animation]);
|
|
442
|
+
fiber.useFrame((_, delta) => {
|
|
443
|
+
mixer?.update(delta);
|
|
444
|
+
});
|
|
445
|
+
const normFactor = React8.useMemo(() => {
|
|
407
446
|
if (!model) return 1;
|
|
408
447
|
const box = new THREE6__namespace.Box3().setFromObject(model);
|
|
409
448
|
const size = new THREE6__namespace.Vector3();
|
|
@@ -412,11 +451,11 @@ function ModelLoader({
|
|
|
412
451
|
if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
|
|
413
452
|
return 1 / maxDim;
|
|
414
453
|
}, [model]);
|
|
415
|
-
const scaleArray =
|
|
454
|
+
const scaleArray = React8.useMemo(() => {
|
|
416
455
|
const base = typeof scale === "number" ? [scale, scale, scale] : scale;
|
|
417
456
|
return [base[0] * normFactor, base[1] * normFactor, base[2] * normFactor];
|
|
418
457
|
}, [scale, normFactor]);
|
|
419
|
-
const rotationRad =
|
|
458
|
+
const rotationRad = React8.useMemo(() => {
|
|
420
459
|
return [
|
|
421
460
|
rotation[0] * Math.PI / 180,
|
|
422
461
|
rotation[1] * Math.PI / 180,
|
|
@@ -696,21 +735,21 @@ var DEFAULT_OPTIONS = {
|
|
|
696
735
|
};
|
|
697
736
|
function useThree3(options = {}) {
|
|
698
737
|
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
699
|
-
const containerRef =
|
|
700
|
-
const canvasRef =
|
|
701
|
-
const rendererRef =
|
|
702
|
-
const sceneRef =
|
|
703
|
-
const cameraRef =
|
|
704
|
-
const controlsRef =
|
|
705
|
-
const gridHelperRef =
|
|
706
|
-
const rafRef =
|
|
707
|
-
const [isReady, setIsReady] =
|
|
708
|
-
const [dimensions, setDimensions] =
|
|
709
|
-
const initialCameraPosition =
|
|
738
|
+
const containerRef = React8.useRef(null);
|
|
739
|
+
const canvasRef = React8.useRef(null);
|
|
740
|
+
const rendererRef = React8.useRef(null);
|
|
741
|
+
const sceneRef = React8.useRef(null);
|
|
742
|
+
const cameraRef = React8.useRef(null);
|
|
743
|
+
const controlsRef = React8.useRef(null);
|
|
744
|
+
const gridHelperRef = React8.useRef(null);
|
|
745
|
+
const rafRef = React8.useRef(0);
|
|
746
|
+
const [isReady, setIsReady] = React8.useState(false);
|
|
747
|
+
const [dimensions, setDimensions] = React8.useState({ width: 0, height: 0 });
|
|
748
|
+
const initialCameraPosition = React8.useMemo(
|
|
710
749
|
() => new THREE6__namespace.Vector3(...opts.cameraPosition),
|
|
711
750
|
[]
|
|
712
751
|
);
|
|
713
|
-
|
|
752
|
+
React8.useEffect(() => {
|
|
714
753
|
if (!containerRef.current) return;
|
|
715
754
|
const container = containerRef.current;
|
|
716
755
|
const { clientWidth, clientHeight } = container;
|
|
@@ -803,7 +842,7 @@ function useThree3(options = {}) {
|
|
|
803
842
|
scene.clear();
|
|
804
843
|
};
|
|
805
844
|
}, []);
|
|
806
|
-
|
|
845
|
+
React8.useEffect(() => {
|
|
807
846
|
if (!cameraRef.current || !containerRef.current) return;
|
|
808
847
|
const container = containerRef.current;
|
|
809
848
|
const { clientWidth, clientHeight } = container;
|
|
@@ -838,20 +877,20 @@ function useThree3(options = {}) {
|
|
|
838
877
|
animate();
|
|
839
878
|
}
|
|
840
879
|
}, [opts.cameraMode]);
|
|
841
|
-
const setCameraPosition =
|
|
880
|
+
const setCameraPosition = React8.useCallback((x, y, z) => {
|
|
842
881
|
if (cameraRef.current) {
|
|
843
882
|
cameraRef.current.position.set(x, y, z);
|
|
844
883
|
controlsRef.current?.update();
|
|
845
884
|
}
|
|
846
885
|
}, []);
|
|
847
|
-
const lookAt =
|
|
886
|
+
const lookAt = React8.useCallback((x, y, z) => {
|
|
848
887
|
if (cameraRef.current) {
|
|
849
888
|
cameraRef.current.lookAt(x, y, z);
|
|
850
889
|
controlsRef.current?.target.set(x, y, z);
|
|
851
890
|
controlsRef.current?.update();
|
|
852
891
|
}
|
|
853
892
|
}, []);
|
|
854
|
-
const resetCamera =
|
|
893
|
+
const resetCamera = React8.useCallback(() => {
|
|
855
894
|
if (cameraRef.current) {
|
|
856
895
|
cameraRef.current.position.copy(initialCameraPosition);
|
|
857
896
|
cameraRef.current.lookAt(0, 0, 0);
|
|
@@ -861,7 +900,7 @@ function useThree3(options = {}) {
|
|
|
861
900
|
}
|
|
862
901
|
}
|
|
863
902
|
}, [initialCameraPosition]);
|
|
864
|
-
const fitView =
|
|
903
|
+
const fitView = React8.useCallback(
|
|
865
904
|
(bounds) => {
|
|
866
905
|
if (!cameraRef.current) return;
|
|
867
906
|
const centerX = (bounds.minX + bounds.maxX) / 2;
|
|
@@ -892,20 +931,20 @@ function useThree3(options = {}) {
|
|
|
892
931
|
}
|
|
893
932
|
function useAssetLoader(options = {}) {
|
|
894
933
|
const { preloadUrls = [], loader: customLoader } = options;
|
|
895
|
-
const loaderRef =
|
|
896
|
-
const [state, setState] =
|
|
934
|
+
const loaderRef = React8.useRef(customLoader || new AssetLoader());
|
|
935
|
+
const [state, setState] = React8.useState({
|
|
897
936
|
isLoading: false,
|
|
898
937
|
progress: 0,
|
|
899
938
|
loaded: 0,
|
|
900
939
|
total: 0,
|
|
901
940
|
errors: []
|
|
902
941
|
});
|
|
903
|
-
|
|
942
|
+
React8.useEffect(() => {
|
|
904
943
|
if (preloadUrls.length > 0) {
|
|
905
944
|
preload(preloadUrls);
|
|
906
945
|
}
|
|
907
946
|
}, []);
|
|
908
|
-
const updateProgress =
|
|
947
|
+
const updateProgress = React8.useCallback((loaded, total) => {
|
|
909
948
|
setState((prev) => ({
|
|
910
949
|
...prev,
|
|
911
950
|
loaded,
|
|
@@ -913,7 +952,7 @@ function useAssetLoader(options = {}) {
|
|
|
913
952
|
progress: total > 0 ? Math.round(loaded / total * 100) : 0
|
|
914
953
|
}));
|
|
915
954
|
}, []);
|
|
916
|
-
const loadModel =
|
|
955
|
+
const loadModel = React8.useCallback(
|
|
917
956
|
async (url) => {
|
|
918
957
|
setState((prev) => ({ ...prev, isLoading: true }));
|
|
919
958
|
try {
|
|
@@ -936,7 +975,7 @@ function useAssetLoader(options = {}) {
|
|
|
936
975
|
},
|
|
937
976
|
[]
|
|
938
977
|
);
|
|
939
|
-
const loadOBJ =
|
|
978
|
+
const loadOBJ = React8.useCallback(
|
|
940
979
|
async (url) => {
|
|
941
980
|
setState((prev) => ({ ...prev, isLoading: true }));
|
|
942
981
|
try {
|
|
@@ -959,7 +998,7 @@ function useAssetLoader(options = {}) {
|
|
|
959
998
|
},
|
|
960
999
|
[]
|
|
961
1000
|
);
|
|
962
|
-
const loadTexture =
|
|
1001
|
+
const loadTexture = React8.useCallback(
|
|
963
1002
|
async (url) => {
|
|
964
1003
|
setState((prev) => ({ ...prev, isLoading: true }));
|
|
965
1004
|
try {
|
|
@@ -982,7 +1021,7 @@ function useAssetLoader(options = {}) {
|
|
|
982
1021
|
},
|
|
983
1022
|
[]
|
|
984
1023
|
);
|
|
985
|
-
const preload =
|
|
1024
|
+
const preload = React8.useCallback(
|
|
986
1025
|
async (urls) => {
|
|
987
1026
|
setState((prev) => ({
|
|
988
1027
|
...prev,
|
|
@@ -1021,27 +1060,27 @@ function useAssetLoader(options = {}) {
|
|
|
1021
1060
|
},
|
|
1022
1061
|
[updateProgress]
|
|
1023
1062
|
);
|
|
1024
|
-
const hasModel =
|
|
1063
|
+
const hasModel = React8.useCallback((url) => {
|
|
1025
1064
|
return loaderRef.current.hasModel(url);
|
|
1026
1065
|
}, []);
|
|
1027
|
-
const hasTexture =
|
|
1066
|
+
const hasTexture = React8.useCallback((url) => {
|
|
1028
1067
|
return loaderRef.current.hasTexture(url);
|
|
1029
1068
|
}, []);
|
|
1030
|
-
const getModel =
|
|
1069
|
+
const getModel = React8.useCallback((url) => {
|
|
1031
1070
|
try {
|
|
1032
1071
|
return loaderRef.current.getModel(url);
|
|
1033
1072
|
} catch {
|
|
1034
1073
|
return void 0;
|
|
1035
1074
|
}
|
|
1036
1075
|
}, []);
|
|
1037
|
-
const getTexture =
|
|
1076
|
+
const getTexture = React8.useCallback((url) => {
|
|
1038
1077
|
try {
|
|
1039
1078
|
return loaderRef.current.getTexture(url);
|
|
1040
1079
|
} catch {
|
|
1041
1080
|
return void 0;
|
|
1042
1081
|
}
|
|
1043
1082
|
}, []);
|
|
1044
|
-
const clearCache =
|
|
1083
|
+
const clearCache = React8.useCallback(() => {
|
|
1045
1084
|
loaderRef.current.clearCache();
|
|
1046
1085
|
setState({
|
|
1047
1086
|
isLoading: false,
|
|
@@ -1065,25 +1104,25 @@ function useAssetLoader(options = {}) {
|
|
|
1065
1104
|
};
|
|
1066
1105
|
}
|
|
1067
1106
|
function useSceneGraph() {
|
|
1068
|
-
const nodesRef =
|
|
1069
|
-
const addNode =
|
|
1107
|
+
const nodesRef = React8.useRef(/* @__PURE__ */ new Map());
|
|
1108
|
+
const addNode = React8.useCallback((node) => {
|
|
1070
1109
|
const existing = nodesRef.current.get(node.id);
|
|
1071
1110
|
if (existing) {
|
|
1072
1111
|
existing.mesh.removeFromParent();
|
|
1073
1112
|
}
|
|
1074
1113
|
nodesRef.current.set(node.id, node);
|
|
1075
1114
|
}, []);
|
|
1076
|
-
const removeNode =
|
|
1115
|
+
const removeNode = React8.useCallback((id) => {
|
|
1077
1116
|
const node = nodesRef.current.get(id);
|
|
1078
1117
|
if (node) {
|
|
1079
1118
|
node.mesh.removeFromParent();
|
|
1080
1119
|
nodesRef.current.delete(id);
|
|
1081
1120
|
}
|
|
1082
1121
|
}, []);
|
|
1083
|
-
const getNode =
|
|
1122
|
+
const getNode = React8.useCallback((id) => {
|
|
1084
1123
|
return nodesRef.current.get(id);
|
|
1085
1124
|
}, []);
|
|
1086
|
-
const updateNodePosition =
|
|
1125
|
+
const updateNodePosition = React8.useCallback(
|
|
1087
1126
|
(id, x, y, z) => {
|
|
1088
1127
|
const node = nodesRef.current.get(id);
|
|
1089
1128
|
if (node) {
|
|
@@ -1093,7 +1132,7 @@ function useSceneGraph() {
|
|
|
1093
1132
|
},
|
|
1094
1133
|
[]
|
|
1095
1134
|
);
|
|
1096
|
-
const updateNodeGridPosition =
|
|
1135
|
+
const updateNodeGridPosition = React8.useCallback(
|
|
1097
1136
|
(id, gridX, gridZ) => {
|
|
1098
1137
|
const node = nodesRef.current.get(id);
|
|
1099
1138
|
if (node) {
|
|
@@ -1102,7 +1141,7 @@ function useSceneGraph() {
|
|
|
1102
1141
|
},
|
|
1103
1142
|
[]
|
|
1104
1143
|
);
|
|
1105
|
-
const getNodeAtGrid =
|
|
1144
|
+
const getNodeAtGrid = React8.useCallback(
|
|
1106
1145
|
(x, z, type) => {
|
|
1107
1146
|
return Array.from(nodesRef.current.values()).find((node) => {
|
|
1108
1147
|
const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
|
|
@@ -1111,10 +1150,10 @@ function useSceneGraph() {
|
|
|
1111
1150
|
},
|
|
1112
1151
|
[]
|
|
1113
1152
|
);
|
|
1114
|
-
const getNodesByType =
|
|
1153
|
+
const getNodesByType = React8.useCallback((type) => {
|
|
1115
1154
|
return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
|
|
1116
1155
|
}, []);
|
|
1117
|
-
const getNodesInBounds =
|
|
1156
|
+
const getNodesInBounds = React8.useCallback(
|
|
1118
1157
|
(minX, maxX, minZ, maxZ) => {
|
|
1119
1158
|
return Array.from(nodesRef.current.values()).filter((node) => {
|
|
1120
1159
|
const { x, z } = node.gridPosition;
|
|
@@ -1123,13 +1162,13 @@ function useSceneGraph() {
|
|
|
1123
1162
|
},
|
|
1124
1163
|
[]
|
|
1125
1164
|
);
|
|
1126
|
-
const clearNodes =
|
|
1165
|
+
const clearNodes = React8.useCallback(() => {
|
|
1127
1166
|
nodesRef.current.forEach((node) => {
|
|
1128
1167
|
node.mesh.removeFromParent();
|
|
1129
1168
|
});
|
|
1130
1169
|
nodesRef.current.clear();
|
|
1131
1170
|
}, []);
|
|
1132
|
-
const countNodes =
|
|
1171
|
+
const countNodes = React8.useCallback((type) => {
|
|
1133
1172
|
if (!type) {
|
|
1134
1173
|
return nodesRef.current.size;
|
|
1135
1174
|
}
|
|
@@ -1151,9 +1190,9 @@ function useSceneGraph() {
|
|
|
1151
1190
|
}
|
|
1152
1191
|
function useRaycaster(options) {
|
|
1153
1192
|
const { camera, canvas, cellSize = 1, offsetX = 0, offsetZ = 0 } = options;
|
|
1154
|
-
const raycaster =
|
|
1155
|
-
const mouse =
|
|
1156
|
-
const clientToNDC =
|
|
1193
|
+
const raycaster = React8.useRef(new THREE6__namespace.Raycaster());
|
|
1194
|
+
const mouse = React8.useRef(new THREE6__namespace.Vector2());
|
|
1195
|
+
const clientToNDC = React8.useCallback(
|
|
1157
1196
|
(clientX, clientY) => {
|
|
1158
1197
|
if (!canvas) {
|
|
1159
1198
|
return { x: 0, y: 0 };
|
|
@@ -1166,7 +1205,7 @@ function useRaycaster(options) {
|
|
|
1166
1205
|
},
|
|
1167
1206
|
[canvas]
|
|
1168
1207
|
);
|
|
1169
|
-
const isWithinCanvas =
|
|
1208
|
+
const isWithinCanvas = React8.useCallback(
|
|
1170
1209
|
(clientX, clientY) => {
|
|
1171
1210
|
if (!canvas) return false;
|
|
1172
1211
|
const rect = canvas.getBoundingClientRect();
|
|
@@ -1174,7 +1213,7 @@ function useRaycaster(options) {
|
|
|
1174
1213
|
},
|
|
1175
1214
|
[canvas]
|
|
1176
1215
|
);
|
|
1177
|
-
const getIntersection =
|
|
1216
|
+
const getIntersection = React8.useCallback(
|
|
1178
1217
|
(clientX, clientY, objects) => {
|
|
1179
1218
|
if (!camera || !canvas) return null;
|
|
1180
1219
|
const ndc = clientToNDC(clientX, clientY);
|
|
@@ -1197,7 +1236,7 @@ function useRaycaster(options) {
|
|
|
1197
1236
|
},
|
|
1198
1237
|
[camera, canvas, clientToNDC]
|
|
1199
1238
|
);
|
|
1200
|
-
const getAllIntersections =
|
|
1239
|
+
const getAllIntersections = React8.useCallback(
|
|
1201
1240
|
(clientX, clientY, objects) => {
|
|
1202
1241
|
if (!camera || !canvas) return [];
|
|
1203
1242
|
const ndc = clientToNDC(clientX, clientY);
|
|
@@ -1216,7 +1255,7 @@ function useRaycaster(options) {
|
|
|
1216
1255
|
},
|
|
1217
1256
|
[camera, canvas, clientToNDC]
|
|
1218
1257
|
);
|
|
1219
|
-
const getGridCoordinates =
|
|
1258
|
+
const getGridCoordinates = React8.useCallback(
|
|
1220
1259
|
(clientX, clientY) => {
|
|
1221
1260
|
if (!camera || !canvas) return null;
|
|
1222
1261
|
const ndc = clientToNDC(clientX, clientY);
|
|
@@ -1234,7 +1273,7 @@ function useRaycaster(options) {
|
|
|
1234
1273
|
},
|
|
1235
1274
|
[camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
|
|
1236
1275
|
);
|
|
1237
|
-
const getTileAtPosition =
|
|
1276
|
+
const getTileAtPosition = React8.useCallback(
|
|
1238
1277
|
(clientX, clientY, scene) => {
|
|
1239
1278
|
if (!camera || !canvas) return null;
|
|
1240
1279
|
const tileMeshes = [];
|
|
@@ -1357,10 +1396,10 @@ var fallbackEventBus = {
|
|
|
1357
1396
|
}
|
|
1358
1397
|
};
|
|
1359
1398
|
function useEventBus() {
|
|
1360
|
-
const context =
|
|
1399
|
+
const context = React8.useContext(providers.EventBusContext);
|
|
1361
1400
|
const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
|
|
1362
1401
|
const scope = providers.useTraitScope();
|
|
1363
|
-
return
|
|
1402
|
+
return React8.useMemo(() => {
|
|
1364
1403
|
if (!scope) {
|
|
1365
1404
|
return {
|
|
1366
1405
|
...baseBus,
|
|
@@ -1396,7 +1435,7 @@ function useEventBus() {
|
|
|
1396
1435
|
}
|
|
1397
1436
|
function useEmitEvent() {
|
|
1398
1437
|
const eventBus = useEventBus();
|
|
1399
|
-
return
|
|
1438
|
+
return React8.useCallback(
|
|
1400
1439
|
(type, payload, source) => {
|
|
1401
1440
|
eventBus.emit(type, payload, source);
|
|
1402
1441
|
},
|
|
@@ -1423,9 +1462,9 @@ function useGameCanvas3DEvents(options) {
|
|
|
1423
1462
|
onUnitAnimation
|
|
1424
1463
|
} = options;
|
|
1425
1464
|
const emit = useEmitEvent();
|
|
1426
|
-
const optionsRef =
|
|
1465
|
+
const optionsRef = React8.useRef(options);
|
|
1427
1466
|
optionsRef.current = options;
|
|
1428
|
-
const handleTileClick =
|
|
1467
|
+
const handleTileClick = React8.useCallback(
|
|
1429
1468
|
(tile, event) => {
|
|
1430
1469
|
if (tileClickEvent) {
|
|
1431
1470
|
emit(tileClickEvent, {
|
|
@@ -1441,7 +1480,7 @@ function useGameCanvas3DEvents(options) {
|
|
|
1441
1480
|
},
|
|
1442
1481
|
[tileClickEvent, emit]
|
|
1443
1482
|
);
|
|
1444
|
-
const handleUnitClick =
|
|
1483
|
+
const handleUnitClick = React8.useCallback(
|
|
1445
1484
|
(unit, event) => {
|
|
1446
1485
|
if (unitClickEvent) {
|
|
1447
1486
|
emit(unitClickEvent, {
|
|
@@ -1460,7 +1499,7 @@ function useGameCanvas3DEvents(options) {
|
|
|
1460
1499
|
},
|
|
1461
1500
|
[unitClickEvent, emit]
|
|
1462
1501
|
);
|
|
1463
|
-
const handleFeatureClick =
|
|
1502
|
+
const handleFeatureClick = React8.useCallback(
|
|
1464
1503
|
(feature, event) => {
|
|
1465
1504
|
if (featureClickEvent) {
|
|
1466
1505
|
emit(featureClickEvent, {
|
|
@@ -1475,7 +1514,7 @@ function useGameCanvas3DEvents(options) {
|
|
|
1475
1514
|
},
|
|
1476
1515
|
[featureClickEvent, emit]
|
|
1477
1516
|
);
|
|
1478
|
-
const handleCanvasClick =
|
|
1517
|
+
const handleCanvasClick = React8.useCallback(
|
|
1479
1518
|
(event) => {
|
|
1480
1519
|
if (canvasClickEvent) {
|
|
1481
1520
|
emit(canvasClickEvent, {
|
|
@@ -1488,7 +1527,7 @@ function useGameCanvas3DEvents(options) {
|
|
|
1488
1527
|
},
|
|
1489
1528
|
[canvasClickEvent, emit]
|
|
1490
1529
|
);
|
|
1491
|
-
const handleTileHover =
|
|
1530
|
+
const handleTileHover = React8.useCallback(
|
|
1492
1531
|
(tile, event) => {
|
|
1493
1532
|
if (tile) {
|
|
1494
1533
|
if (tileHoverEvent) {
|
|
@@ -1508,7 +1547,7 @@ function useGameCanvas3DEvents(options) {
|
|
|
1508
1547
|
},
|
|
1509
1548
|
[tileHoverEvent, tileLeaveEvent, emit]
|
|
1510
1549
|
);
|
|
1511
|
-
const handleUnitAnimation =
|
|
1550
|
+
const handleUnitAnimation = React8.useCallback(
|
|
1512
1551
|
(unitId, state) => {
|
|
1513
1552
|
if (unitAnimationEvent) {
|
|
1514
1553
|
emit(unitAnimationEvent, {
|
|
@@ -1521,7 +1560,7 @@ function useGameCanvas3DEvents(options) {
|
|
|
1521
1560
|
},
|
|
1522
1561
|
[unitAnimationEvent, emit]
|
|
1523
1562
|
);
|
|
1524
|
-
const handleCameraChange =
|
|
1563
|
+
const handleCameraChange = React8.useCallback(
|
|
1525
1564
|
(position) => {
|
|
1526
1565
|
if (cameraChangeEvent) {
|
|
1527
1566
|
emit(cameraChangeEvent, {
|
|
@@ -1578,11 +1617,11 @@ function resolveSheetUrl(atlasUrl, relativeSheetPath) {
|
|
|
1578
1617
|
}
|
|
1579
1618
|
}
|
|
1580
1619
|
function useUnitSpriteAtlas(units) {
|
|
1581
|
-
const atlasCacheRef =
|
|
1582
|
-
const loadingRef =
|
|
1583
|
-
const [pendingCount, setPendingCount] =
|
|
1584
|
-
const [, forceTick] =
|
|
1585
|
-
const atlasUrls =
|
|
1620
|
+
const atlasCacheRef = React8.useRef(/* @__PURE__ */ new Map());
|
|
1621
|
+
const loadingRef = React8.useRef(/* @__PURE__ */ new Set());
|
|
1622
|
+
const [pendingCount, setPendingCount] = React8.useState(0);
|
|
1623
|
+
const [, forceTick] = React8.useState(0);
|
|
1624
|
+
const atlasUrls = React8.useMemo(() => {
|
|
1586
1625
|
const set = /* @__PURE__ */ new Set();
|
|
1587
1626
|
for (const unit of units) {
|
|
1588
1627
|
const url = unitAtlasUrl(unit);
|
|
@@ -1590,7 +1629,7 @@ function useUnitSpriteAtlas(units) {
|
|
|
1590
1629
|
}
|
|
1591
1630
|
return [...set];
|
|
1592
1631
|
}, [units]);
|
|
1593
|
-
|
|
1632
|
+
React8.useEffect(() => {
|
|
1594
1633
|
const cache = atlasCacheRef.current;
|
|
1595
1634
|
const loading = loadingRef.current;
|
|
1596
1635
|
const toLoad = atlasUrls.filter((url) => !cache.has(url) && !loading.has(url));
|
|
@@ -1614,7 +1653,7 @@ function useUnitSpriteAtlas(units) {
|
|
|
1614
1653
|
cancelled = true;
|
|
1615
1654
|
};
|
|
1616
1655
|
}, [atlasUrls]);
|
|
1617
|
-
const sheetUrls =
|
|
1656
|
+
const sheetUrls = React8.useMemo(() => {
|
|
1618
1657
|
const urls = /* @__PURE__ */ new Set();
|
|
1619
1658
|
for (const unit of units) {
|
|
1620
1659
|
const atlasUrl = unitAtlasUrl(unit);
|
|
@@ -1627,7 +1666,7 @@ function useUnitSpriteAtlas(units) {
|
|
|
1627
1666
|
}
|
|
1628
1667
|
return [...urls];
|
|
1629
1668
|
}, [units, pendingCount]);
|
|
1630
|
-
const resolveUnitFrame =
|
|
1669
|
+
const resolveUnitFrame = React8.useCallback((unitId) => {
|
|
1631
1670
|
const unit = units.find((u) => u.id === unitId);
|
|
1632
1671
|
if (!unit) return null;
|
|
1633
1672
|
const atlasUrl = unitAtlasUrl(unit);
|
|
@@ -1667,7 +1706,7 @@ function CameraController({
|
|
|
1667
1706
|
onCameraChange
|
|
1668
1707
|
}) {
|
|
1669
1708
|
const { camera } = fiber.useThree();
|
|
1670
|
-
|
|
1709
|
+
React8.useEffect(() => {
|
|
1671
1710
|
if (onCameraChange) {
|
|
1672
1711
|
onCameraChange({
|
|
1673
1712
|
x: camera.position.x,
|
|
@@ -1684,9 +1723,9 @@ function UnitSpriteBillboard({
|
|
|
1684
1723
|
height = 1.2
|
|
1685
1724
|
}) {
|
|
1686
1725
|
const texture = fiber.useLoader(THREE6__namespace.TextureLoader, sheetUrl);
|
|
1687
|
-
const meshRef =
|
|
1688
|
-
const matRef =
|
|
1689
|
-
const [aspect, setAspect] =
|
|
1726
|
+
const meshRef = React8.useRef(null);
|
|
1727
|
+
const matRef = React8.useRef(null);
|
|
1728
|
+
const [aspect, setAspect] = React8.useState(1);
|
|
1690
1729
|
fiber.useFrame(() => {
|
|
1691
1730
|
const frame = resolveFrame();
|
|
1692
1731
|
if (!frame || !texture.image) return;
|
|
@@ -1719,7 +1758,357 @@ function UnitSpriteBillboard({
|
|
|
1719
1758
|
)
|
|
1720
1759
|
] });
|
|
1721
1760
|
}
|
|
1722
|
-
|
|
1761
|
+
function FollowCamera({
|
|
1762
|
+
target,
|
|
1763
|
+
offset
|
|
1764
|
+
}) {
|
|
1765
|
+
const { camera } = fiber.useThree();
|
|
1766
|
+
const look = React8.useRef(new THREE6__namespace.Vector3(target[0], target[1], target[2]));
|
|
1767
|
+
const goal = React8.useRef(new THREE6__namespace.Vector3());
|
|
1768
|
+
fiber.useFrame((_, delta) => {
|
|
1769
|
+
const t = Math.min(1, delta * 5);
|
|
1770
|
+
goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
|
|
1771
|
+
camera.position.lerp(goal.current, t);
|
|
1772
|
+
look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
|
|
1773
|
+
camera.lookAt(look.current);
|
|
1774
|
+
});
|
|
1775
|
+
return null;
|
|
1776
|
+
}
|
|
1777
|
+
function LerpedGroup({
|
|
1778
|
+
target,
|
|
1779
|
+
enabled,
|
|
1780
|
+
children
|
|
1781
|
+
}) {
|
|
1782
|
+
const ref = React8.useRef(null);
|
|
1783
|
+
const goal = React8.useRef(new THREE6__namespace.Vector3());
|
|
1784
|
+
fiber.useFrame((_, delta) => {
|
|
1785
|
+
const g = ref.current;
|
|
1786
|
+
if (!g) return;
|
|
1787
|
+
goal.current.set(target[0], target[1], target[2]);
|
|
1788
|
+
if (enabled) g.position.lerp(goal.current, Math.min(1, delta * 10));
|
|
1789
|
+
else g.position.copy(goal.current);
|
|
1790
|
+
});
|
|
1791
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { ref, position: target, children });
|
|
1792
|
+
}
|
|
1793
|
+
var SIDE_PLATFORM_COLORS = {
|
|
1794
|
+
ground: "#5b8c3e",
|
|
1795
|
+
platform: "#8b5a2b",
|
|
1796
|
+
hazard: "#cc4444",
|
|
1797
|
+
goal: "#4488cc"
|
|
1798
|
+
};
|
|
1799
|
+
function SideScene({
|
|
1800
|
+
player,
|
|
1801
|
+
platforms,
|
|
1802
|
+
worldHeight,
|
|
1803
|
+
ppu,
|
|
1804
|
+
playerSprite,
|
|
1805
|
+
tileSprites,
|
|
1806
|
+
interpolate,
|
|
1807
|
+
features = [],
|
|
1808
|
+
events = [],
|
|
1809
|
+
assetManifest
|
|
1810
|
+
}) {
|
|
1811
|
+
const pw = player.width ?? 32;
|
|
1812
|
+
const ph = player.height ?? 48;
|
|
1813
|
+
const playerPos = [
|
|
1814
|
+
(player.x + pw / 2) / ppu,
|
|
1815
|
+
(worldHeight - player.y - ph) / ppu,
|
|
1816
|
+
0
|
|
1817
|
+
];
|
|
1818
|
+
const playerH = Math.max(ph / ppu, 0.5);
|
|
1819
|
+
return /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
|
|
1820
|
+
platforms.map((p, i) => {
|
|
1821
|
+
const platformType = p.type ?? "platform";
|
|
1822
|
+
const model = tileSprites?.[platformType]?.url;
|
|
1823
|
+
const topY = (worldHeight - p.y) / ppu;
|
|
1824
|
+
if (model) {
|
|
1825
|
+
const cells = Math.max(1, Math.round(p.width / ppu));
|
|
1826
|
+
const x0 = p.x / ppu;
|
|
1827
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { children: Array.from({ length: cells }, (_, c) => /* @__PURE__ */ jsxRuntime.jsx("group", { position: [x0 + c + 0.5, topY - 0.475, 0], children: /* @__PURE__ */ jsxRuntime.jsx(ModelLoader, { url: model, scale: 0.95, fallbackGeometry: "box", castShadow: true, receiveShadow: true }) }, c)) }, `plat-${i}`);
|
|
1828
|
+
}
|
|
1829
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1830
|
+
"mesh",
|
|
1831
|
+
{
|
|
1832
|
+
position: [(p.x + p.width / 2) / ppu, topY - p.height / 2 / ppu, 0],
|
|
1833
|
+
children: [
|
|
1834
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [p.width / ppu, p.height / ppu, 1] }),
|
|
1835
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: SIDE_PLATFORM_COLORS[platformType] ?? "#888888" })
|
|
1836
|
+
]
|
|
1837
|
+
},
|
|
1838
|
+
`plat-${i}`
|
|
1839
|
+
);
|
|
1840
|
+
}),
|
|
1841
|
+
features.map((feature, i) => {
|
|
1842
|
+
const model = feature.assetUrl ?? assetManifest?.features?.[feature.type];
|
|
1843
|
+
const fx = feature.x / ppu;
|
|
1844
|
+
const fy = (worldHeight - feature.y) / ppu;
|
|
1845
|
+
if (!model?.url) return null;
|
|
1846
|
+
return /* @__PURE__ */ jsxRuntime.jsx("group", { position: [fx, fy, 0], children: /* @__PURE__ */ jsxRuntime.jsx(ModelLoader, { url: model.url, scale: 0.6, tint: feature.color, fallbackGeometry: "sphere", castShadow: true }) }, feature.id ?? `sfeat-${i}`);
|
|
1847
|
+
}),
|
|
1848
|
+
events.map((ev, i) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
1849
|
+
EventMarker,
|
|
1850
|
+
{
|
|
1851
|
+
event: ev,
|
|
1852
|
+
position: [ev.x / ppu, (worldHeight - (ev.y ?? 0)) / ppu + 0.8, 0.2]
|
|
1853
|
+
},
|
|
1854
|
+
ev.id ?? `sev-${i}`
|
|
1855
|
+
)),
|
|
1856
|
+
/* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: playerPos, enabled: interpolate, children: playerSprite?.url ? /* @__PURE__ */ jsxRuntime.jsx("group", { position: [0, playerH / 2, 0], children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1857
|
+
ModelLoader,
|
|
1858
|
+
{
|
|
1859
|
+
url: playerSprite.url,
|
|
1860
|
+
scale: playerH,
|
|
1861
|
+
rotation: [0, player.facingRight ? 90 : -90, 0],
|
|
1862
|
+
animation: player.animation ?? "idle",
|
|
1863
|
+
fallbackGeometry: "box",
|
|
1864
|
+
castShadow: true
|
|
1865
|
+
}
|
|
1866
|
+
) }) : /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, playerH / 2, 0], children: [
|
|
1867
|
+
/* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [playerH * 0.25, playerH * 0.5, 4, 8] }),
|
|
1868
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: "#4488ff" })
|
|
1869
|
+
] }) })
|
|
1870
|
+
] });
|
|
1871
|
+
}
|
|
1872
|
+
var UNIT_BASE_MODEL_SCALE = 0.5;
|
|
1873
|
+
var UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
|
|
1874
|
+
var UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
|
|
1875
|
+
var EVENT_COLORS = {
|
|
1876
|
+
hit: "#ff5544",
|
|
1877
|
+
damage: "#ff5544",
|
|
1878
|
+
attack: "#ff8844",
|
|
1879
|
+
heal: "#44dd66",
|
|
1880
|
+
pickup: "#ffcc33",
|
|
1881
|
+
score: "#ffcc33",
|
|
1882
|
+
death: "#aa3333",
|
|
1883
|
+
win: "#66ff88",
|
|
1884
|
+
lose: "#ff6666"
|
|
1885
|
+
};
|
|
1886
|
+
function EventMarker({
|
|
1887
|
+
event,
|
|
1888
|
+
position
|
|
1889
|
+
}) {
|
|
1890
|
+
return /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { position, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1891
|
+
drei.Text,
|
|
1892
|
+
{
|
|
1893
|
+
fontSize: 0.32,
|
|
1894
|
+
color: EVENT_COLORS[event.type] ?? "#ffffff",
|
|
1895
|
+
outlineWidth: 0.02,
|
|
1896
|
+
outlineColor: "#000000",
|
|
1897
|
+
anchorX: "center",
|
|
1898
|
+
anchorY: "middle",
|
|
1899
|
+
children: event.message ?? event.type
|
|
1900
|
+
}
|
|
1901
|
+
) });
|
|
1902
|
+
}
|
|
1903
|
+
function DefaultTile({
|
|
1904
|
+
tile,
|
|
1905
|
+
position,
|
|
1906
|
+
model,
|
|
1907
|
+
isSelected,
|
|
1908
|
+
isHovered,
|
|
1909
|
+
isValidMove,
|
|
1910
|
+
isAttackTarget,
|
|
1911
|
+
onTileClick,
|
|
1912
|
+
onTileHover
|
|
1913
|
+
}) {
|
|
1914
|
+
let color = 8421504;
|
|
1915
|
+
if (tile.type === "water") color = 4491468;
|
|
1916
|
+
else if (tile.type === "grass") color = 4500036;
|
|
1917
|
+
else if (tile.type === "sand") color = 14535816;
|
|
1918
|
+
else if (tile.type === "rock") color = 8947848;
|
|
1919
|
+
else if (tile.type === "snow") color = 15658734;
|
|
1920
|
+
let emissive = 0;
|
|
1921
|
+
if (isSelected) emissive = 4473924;
|
|
1922
|
+
else if (isAttackTarget) emissive = 4456448;
|
|
1923
|
+
else if (isValidMove) emissive = 17408;
|
|
1924
|
+
else if (isHovered) emissive = 2236962;
|
|
1925
|
+
if (model?.url) {
|
|
1926
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1927
|
+
"group",
|
|
1928
|
+
{
|
|
1929
|
+
position,
|
|
1930
|
+
onClick: (e) => onTileClick(tile, e),
|
|
1931
|
+
onPointerEnter: (e) => onTileHover(tile, e),
|
|
1932
|
+
onPointerLeave: (e) => onTileHover(null, e),
|
|
1933
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1934
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1935
|
+
ModelLoader,
|
|
1936
|
+
{
|
|
1937
|
+
url: model.url,
|
|
1938
|
+
scale: 0.95,
|
|
1939
|
+
rotation: [0, tile.rotation ?? 0, 0],
|
|
1940
|
+
fallbackGeometry: "box",
|
|
1941
|
+
castShadow: true,
|
|
1942
|
+
receiveShadow: true
|
|
1943
|
+
}
|
|
1944
|
+
)
|
|
1945
|
+
}
|
|
1946
|
+
);
|
|
1947
|
+
}
|
|
1948
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1949
|
+
"mesh",
|
|
1950
|
+
{
|
|
1951
|
+
position,
|
|
1952
|
+
onClick: (e) => onTileClick(tile, e),
|
|
1953
|
+
onPointerEnter: (e) => onTileHover(tile, e),
|
|
1954
|
+
onPointerLeave: (e) => onTileHover(null, e),
|
|
1955
|
+
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1956
|
+
children: [
|
|
1957
|
+
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
|
|
1958
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, emissive })
|
|
1959
|
+
]
|
|
1960
|
+
}
|
|
1961
|
+
);
|
|
1962
|
+
}
|
|
1963
|
+
function DefaultUnit({
|
|
1964
|
+
unit,
|
|
1965
|
+
position,
|
|
1966
|
+
model,
|
|
1967
|
+
isSelected,
|
|
1968
|
+
unitScale,
|
|
1969
|
+
cellSize,
|
|
1970
|
+
resolveUnitFrame,
|
|
1971
|
+
onUnitClick
|
|
1972
|
+
}) {
|
|
1973
|
+
const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
|
|
1974
|
+
const hasAtlas = unitAtlasUrl(unit) !== null;
|
|
1975
|
+
const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
|
|
1976
|
+
const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * cellSize;
|
|
1977
|
+
const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * cellSize;
|
|
1978
|
+
const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * cellSize;
|
|
1979
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1980
|
+
"group",
|
|
1981
|
+
{
|
|
1982
|
+
position,
|
|
1983
|
+
onClick: (e) => onUnitClick(unit, e),
|
|
1984
|
+
userData: { type: "unit", unitId: unit.id },
|
|
1985
|
+
children: [
|
|
1986
|
+
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
1987
|
+
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
1988
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
1989
|
+
] }),
|
|
1990
|
+
hasAtlas && initialFrame ? (
|
|
1991
|
+
/* Animated sprite-sheet billboard — single cropped frame, by state */
|
|
1992
|
+
/* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1993
|
+
UnitSpriteBillboard,
|
|
1994
|
+
{
|
|
1995
|
+
sheetUrl: initialFrame.sheetUrl,
|
|
1996
|
+
resolveFrame: () => resolveUnitFrame(unit.id),
|
|
1997
|
+
height: billboardHeight
|
|
1998
|
+
}
|
|
1999
|
+
) })
|
|
2000
|
+
) : model?.url ? (
|
|
2001
|
+
/* GLB unit model — LOLO's `animation` field drives the named clip */
|
|
2002
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2003
|
+
ModelLoader,
|
|
2004
|
+
{
|
|
2005
|
+
url: model.url,
|
|
2006
|
+
scale: modelScale,
|
|
2007
|
+
animation: unit.animation ?? "idle",
|
|
2008
|
+
fallbackGeometry: "box",
|
|
2009
|
+
castShadow: true
|
|
2010
|
+
}
|
|
2011
|
+
)
|
|
2012
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2013
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
|
|
2014
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
|
|
2015
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2016
|
+
] }),
|
|
2017
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
|
|
2018
|
+
/* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
|
|
2019
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2020
|
+
] }),
|
|
2021
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
|
|
2022
|
+
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
|
|
2023
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2024
|
+
] })
|
|
2025
|
+
] }),
|
|
2026
|
+
unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("group", { position: [0, billboardHeight, 0], children: [
|
|
2027
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25, 0, 0], children: [
|
|
2028
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5, 0.05] }),
|
|
2029
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: 3355443 })
|
|
2030
|
+
] }),
|
|
2031
|
+
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2032
|
+
"mesh",
|
|
2033
|
+
{
|
|
2034
|
+
position: [
|
|
2035
|
+
-0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
|
|
2036
|
+
0,
|
|
2037
|
+
0.01
|
|
2038
|
+
],
|
|
2039
|
+
children: [
|
|
2040
|
+
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
|
|
2041
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2042
|
+
"meshBasicMaterial",
|
|
2043
|
+
{
|
|
2044
|
+
color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
|
|
2045
|
+
}
|
|
2046
|
+
)
|
|
2047
|
+
]
|
|
2048
|
+
}
|
|
2049
|
+
)
|
|
2050
|
+
] })
|
|
2051
|
+
]
|
|
2052
|
+
}
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
function DefaultFeature({
|
|
2056
|
+
feature,
|
|
2057
|
+
position,
|
|
2058
|
+
model,
|
|
2059
|
+
onFeatureClick
|
|
2060
|
+
}) {
|
|
2061
|
+
if (model?.url) {
|
|
2062
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2063
|
+
ModelLoader,
|
|
2064
|
+
{
|
|
2065
|
+
url: model.url,
|
|
2066
|
+
position,
|
|
2067
|
+
scale: 0.5,
|
|
2068
|
+
rotation: [0, feature.rotation ?? 0, 0],
|
|
2069
|
+
tint: feature.color,
|
|
2070
|
+
onClick: () => onFeatureClick(feature, null),
|
|
2071
|
+
fallbackGeometry: "box"
|
|
2072
|
+
}
|
|
2073
|
+
);
|
|
2074
|
+
}
|
|
2075
|
+
if (feature.type === "tree") {
|
|
2076
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2077
|
+
"group",
|
|
2078
|
+
{
|
|
2079
|
+
position,
|
|
2080
|
+
onClick: (e) => onFeatureClick(feature, e),
|
|
2081
|
+
userData: { type: "feature", featureId: feature.id },
|
|
2082
|
+
children: [
|
|
2083
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.4, 0], children: [
|
|
2084
|
+
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
|
|
2085
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 9127187 })
|
|
2086
|
+
] }),
|
|
2087
|
+
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
2088
|
+
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
|
|
2089
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 2263842 })
|
|
2090
|
+
] })
|
|
2091
|
+
]
|
|
2092
|
+
}
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
if (feature.type === "rock") {
|
|
2096
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2097
|
+
"mesh",
|
|
2098
|
+
{
|
|
2099
|
+
position: [position[0], position[1] + 0.3, position[2]],
|
|
2100
|
+
onClick: (e) => onFeatureClick(feature, e),
|
|
2101
|
+
userData: { type: "feature", featureId: feature.id },
|
|
2102
|
+
children: [
|
|
2103
|
+
/* @__PURE__ */ jsxRuntime.jsx("dodecahedronGeometry", { args: [0.3, 0] }),
|
|
2104
|
+
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 8421504 })
|
|
2105
|
+
]
|
|
2106
|
+
}
|
|
2107
|
+
);
|
|
2108
|
+
}
|
|
2109
|
+
return null;
|
|
2110
|
+
}
|
|
2111
|
+
var GameCanvas3D = React8.forwardRef(
|
|
1723
2112
|
({
|
|
1724
2113
|
tiles = [],
|
|
1725
2114
|
units = [],
|
|
@@ -1762,13 +2151,48 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1762
2151
|
selectedTileIds = [],
|
|
1763
2152
|
selectedUnitId = null,
|
|
1764
2153
|
unitScale = 1,
|
|
2154
|
+
keyMap,
|
|
2155
|
+
keyUpMap,
|
|
2156
|
+
player,
|
|
2157
|
+
platforms = [],
|
|
2158
|
+
worldHeight = 400,
|
|
2159
|
+
pixelsPerUnit = 32,
|
|
2160
|
+
playerSprite,
|
|
2161
|
+
tileSprites,
|
|
2162
|
+
assetManifest,
|
|
2163
|
+
interpolateUnits = false,
|
|
1765
2164
|
children
|
|
1766
2165
|
}, ref) => {
|
|
1767
|
-
const containerRef =
|
|
1768
|
-
const controlsRef =
|
|
1769
|
-
const [hoveredTile, setHoveredTile] =
|
|
1770
|
-
const [internalError, setInternalError] =
|
|
1771
|
-
|
|
2166
|
+
const containerRef = React8.useRef(null);
|
|
2167
|
+
const controlsRef = React8.useRef(null);
|
|
2168
|
+
const [hoveredTile, setHoveredTile] = React8.useState(null);
|
|
2169
|
+
const [internalError, setInternalError] = React8.useState(null);
|
|
2170
|
+
const eventBus = useEventBus();
|
|
2171
|
+
const keysRef = React8.useRef(/* @__PURE__ */ new Set());
|
|
2172
|
+
React8.useEffect(() => {
|
|
2173
|
+
if (!keyMap && !keyUpMap) return;
|
|
2174
|
+
const down = (e) => {
|
|
2175
|
+
if (keysRef.current.has(e.code)) return;
|
|
2176
|
+
keysRef.current.add(e.code);
|
|
2177
|
+
const ev = keyMap?.[e.code];
|
|
2178
|
+
if (ev) {
|
|
2179
|
+
eventBus.emit(`UI:${ev}`, {});
|
|
2180
|
+
e.preventDefault();
|
|
2181
|
+
}
|
|
2182
|
+
};
|
|
2183
|
+
const up = (e) => {
|
|
2184
|
+
keysRef.current.delete(e.code);
|
|
2185
|
+
const ev = keyUpMap?.[e.code];
|
|
2186
|
+
if (ev) eventBus.emit(`UI:${ev}`, {});
|
|
2187
|
+
};
|
|
2188
|
+
window.addEventListener("keydown", down);
|
|
2189
|
+
window.addEventListener("keyup", up);
|
|
2190
|
+
return () => {
|
|
2191
|
+
window.removeEventListener("keydown", down);
|
|
2192
|
+
window.removeEventListener("keyup", up);
|
|
2193
|
+
};
|
|
2194
|
+
}, [keyMap, keyUpMap, eventBus]);
|
|
2195
|
+
React8.useEffect(() => {
|
|
1772
2196
|
const el = containerRef.current;
|
|
1773
2197
|
if (!el) return;
|
|
1774
2198
|
let node = el;
|
|
@@ -1792,7 +2216,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1792
2216
|
}
|
|
1793
2217
|
}, []);
|
|
1794
2218
|
const { sheetUrls: atlasSheetUrls, resolveUnitFrame } = useUnitSpriteAtlas(units);
|
|
1795
|
-
const preloadUrls =
|
|
2219
|
+
const preloadUrls = React8.useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
|
|
1796
2220
|
const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
|
|
1797
2221
|
preloadUrls,
|
|
1798
2222
|
loader: customAssetLoader
|
|
@@ -1813,7 +2237,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1813
2237
|
onTileHover,
|
|
1814
2238
|
onUnitAnimation
|
|
1815
2239
|
});
|
|
1816
|
-
const gridBounds =
|
|
2240
|
+
const gridBounds = React8.useMemo(() => {
|
|
1817
2241
|
if (tiles.length === 0) {
|
|
1818
2242
|
return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
|
|
1819
2243
|
}
|
|
@@ -1826,14 +2250,14 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1826
2250
|
maxZ: Math.max(...zs)
|
|
1827
2251
|
};
|
|
1828
2252
|
}, [tiles]);
|
|
1829
|
-
const cameraTarget =
|
|
2253
|
+
const cameraTarget = React8.useMemo(() => {
|
|
1830
2254
|
return [
|
|
1831
2255
|
(gridBounds.minX + gridBounds.maxX) / 2,
|
|
1832
2256
|
0,
|
|
1833
2257
|
(gridBounds.minZ + gridBounds.maxZ) / 2
|
|
1834
2258
|
];
|
|
1835
2259
|
}, [gridBounds]);
|
|
1836
|
-
const gridConfig =
|
|
2260
|
+
const gridConfig = React8.useMemo(
|
|
1837
2261
|
() => ({
|
|
1838
2262
|
...DEFAULT_GRID_CONFIG,
|
|
1839
2263
|
offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
|
|
@@ -1841,7 +2265,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1841
2265
|
}),
|
|
1842
2266
|
[gridBounds]
|
|
1843
2267
|
);
|
|
1844
|
-
const gridToWorld2 =
|
|
2268
|
+
const gridToWorld2 = React8.useCallback(
|
|
1845
2269
|
(x, z, y = 0) => {
|
|
1846
2270
|
const worldX = (x - gridBounds.minX) * gridConfig.cellSize;
|
|
1847
2271
|
const worldZ = (z - gridBounds.minZ) * gridConfig.cellSize;
|
|
@@ -1849,7 +2273,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1849
2273
|
},
|
|
1850
2274
|
[gridBounds, gridConfig]
|
|
1851
2275
|
);
|
|
1852
|
-
|
|
2276
|
+
React8.useImperativeHandle(ref, () => ({
|
|
1853
2277
|
getCameraPosition: () => {
|
|
1854
2278
|
if (controlsRef.current) {
|
|
1855
2279
|
const pos = controlsRef.current.object.position;
|
|
@@ -1887,19 +2311,19 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1887
2311
|
features
|
|
1888
2312
|
})
|
|
1889
2313
|
}));
|
|
1890
|
-
const handleTileClick =
|
|
2314
|
+
const handleTileClick = React8.useCallback(
|
|
1891
2315
|
(tile, event) => {
|
|
1892
2316
|
eventHandlers.handleTileClick(tile, event);
|
|
1893
2317
|
},
|
|
1894
2318
|
[eventHandlers]
|
|
1895
2319
|
);
|
|
1896
|
-
const handleUnitClick =
|
|
2320
|
+
const handleUnitClick = React8.useCallback(
|
|
1897
2321
|
(unit, event) => {
|
|
1898
2322
|
eventHandlers.handleUnitClick(unit, event);
|
|
1899
2323
|
},
|
|
1900
2324
|
[eventHandlers]
|
|
1901
2325
|
);
|
|
1902
|
-
const handleFeatureClick =
|
|
2326
|
+
const handleFeatureClick = React8.useCallback(
|
|
1903
2327
|
(feature, event) => {
|
|
1904
2328
|
if (event) {
|
|
1905
2329
|
eventHandlers.handleFeatureClick(feature, event);
|
|
@@ -1907,7 +2331,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1907
2331
|
},
|
|
1908
2332
|
[eventHandlers]
|
|
1909
2333
|
);
|
|
1910
|
-
const handleTileHover =
|
|
2334
|
+
const handleTileHover = React8.useCallback(
|
|
1911
2335
|
(tile, event) => {
|
|
1912
2336
|
setHoveredTile(tile);
|
|
1913
2337
|
if (event) {
|
|
@@ -1916,7 +2340,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1916
2340
|
},
|
|
1917
2341
|
[eventHandlers]
|
|
1918
2342
|
);
|
|
1919
|
-
const cameraConfig =
|
|
2343
|
+
const cameraConfig = React8.useMemo(() => {
|
|
1920
2344
|
const size = Math.max(
|
|
1921
2345
|
gridBounds.maxX - gridBounds.minX,
|
|
1922
2346
|
gridBounds.maxZ - gridBounds.minZ,
|
|
@@ -1937,6 +2361,11 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1937
2361
|
position: [cx, d * 2, cz],
|
|
1938
2362
|
fov: 45
|
|
1939
2363
|
};
|
|
2364
|
+
case "follow":
|
|
2365
|
+
return {
|
|
2366
|
+
position: [cx, d * 0.5, cz + d],
|
|
2367
|
+
fov: 45
|
|
2368
|
+
};
|
|
1940
2369
|
case "perspective":
|
|
1941
2370
|
default:
|
|
1942
2371
|
return {
|
|
@@ -1945,212 +2374,26 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
1945
2374
|
};
|
|
1946
2375
|
}
|
|
1947
2376
|
}, [cameraMode, gridBounds]);
|
|
1948
|
-
const
|
|
1949
|
-
(
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
else if (isHovered) emissive = 2236962;
|
|
1969
|
-
if (tile.modelUrl) {
|
|
1970
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
1971
|
-
"group",
|
|
1972
|
-
{
|
|
1973
|
-
position,
|
|
1974
|
-
onClick: (e) => handleTileClick(tile, e),
|
|
1975
|
-
onPointerEnter: (e) => handleTileHover(tile, e),
|
|
1976
|
-
onPointerLeave: (e) => handleTileHover(null, e),
|
|
1977
|
-
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1978
|
-
children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
1979
|
-
ModelLoader,
|
|
1980
|
-
{
|
|
1981
|
-
url: tile.modelUrl.url,
|
|
1982
|
-
scale: 0.95,
|
|
1983
|
-
fallbackGeometry: "box",
|
|
1984
|
-
castShadow: true,
|
|
1985
|
-
receiveShadow: true
|
|
1986
|
-
}
|
|
1987
|
-
)
|
|
1988
|
-
}
|
|
1989
|
-
);
|
|
1990
|
-
}
|
|
1991
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
1992
|
-
"mesh",
|
|
1993
|
-
{
|
|
1994
|
-
position,
|
|
1995
|
-
onClick: (e) => handleTileClick(tile, e),
|
|
1996
|
-
onPointerEnter: (e) => handleTileHover(tile, e),
|
|
1997
|
-
onPointerLeave: (e) => handleTileHover(null, e),
|
|
1998
|
-
userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
|
|
1999
|
-
children: [
|
|
2000
|
-
/* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
|
|
2001
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color, emissive })
|
|
2002
|
-
]
|
|
2003
|
-
}
|
|
2004
|
-
);
|
|
2005
|
-
},
|
|
2006
|
-
[selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover]
|
|
2007
|
-
);
|
|
2008
|
-
const UNIT_BASE_MODEL_SCALE = 0.5;
|
|
2009
|
-
const UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
|
|
2010
|
-
const UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
|
|
2011
|
-
const DefaultUnitRenderer = React6.useCallback(
|
|
2012
|
-
({ unit, position }) => {
|
|
2013
|
-
const isSelected = selectedUnitId === unit.id;
|
|
2014
|
-
const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
|
|
2015
|
-
const hasAtlas = unitAtlasUrl(unit) !== null;
|
|
2016
|
-
const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
|
|
2017
|
-
const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * gridConfig.cellSize;
|
|
2018
|
-
const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * gridConfig.cellSize;
|
|
2019
|
-
const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * gridConfig.cellSize;
|
|
2020
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2021
|
-
"group",
|
|
2022
|
-
{
|
|
2023
|
-
position,
|
|
2024
|
-
onClick: (e) => handleUnitClick(unit, e),
|
|
2025
|
-
userData: { type: "unit", unitId: unit.id },
|
|
2026
|
-
children: [
|
|
2027
|
-
isSelected && /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
|
|
2028
|
-
/* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
|
|
2029
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
|
|
2030
|
-
] }),
|
|
2031
|
-
hasAtlas && initialFrame ? (
|
|
2032
|
-
/* Animated sprite-sheet billboard — single cropped frame, by state */
|
|
2033
|
-
/* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2034
|
-
UnitSpriteBillboard,
|
|
2035
|
-
{
|
|
2036
|
-
sheetUrl: initialFrame.sheetUrl,
|
|
2037
|
-
resolveFrame: () => resolveUnitFrame(unit.id),
|
|
2038
|
-
height: billboardHeight
|
|
2039
|
-
}
|
|
2040
|
-
) })
|
|
2041
|
-
) : unit.modelUrl ? (
|
|
2042
|
-
/* GLB unit model (box fallback while loading / on error) */
|
|
2043
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2044
|
-
ModelLoader,
|
|
2045
|
-
{
|
|
2046
|
-
url: unit.modelUrl.url,
|
|
2047
|
-
scale: modelScale,
|
|
2048
|
-
fallbackGeometry: "box",
|
|
2049
|
-
castShadow: true
|
|
2050
|
-
}
|
|
2051
|
-
)
|
|
2052
|
-
) : /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
2053
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
|
|
2054
|
-
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
|
|
2055
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2056
|
-
] }),
|
|
2057
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
|
|
2058
|
-
/* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
|
|
2059
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2060
|
-
] }),
|
|
2061
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
|
|
2062
|
-
/* @__PURE__ */ jsxRuntime.jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
|
|
2063
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color })
|
|
2064
|
-
] })
|
|
2065
|
-
] }),
|
|
2066
|
-
unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxRuntime.jsxs("group", { position: [0, billboardHeight, 0], children: [
|
|
2067
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [-0.25, 0, 0], children: [
|
|
2068
|
-
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5, 0.05] }),
|
|
2069
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: 3355443 })
|
|
2070
|
-
] }),
|
|
2071
|
-
/* @__PURE__ */ jsxRuntime.jsxs(
|
|
2072
|
-
"mesh",
|
|
2073
|
-
{
|
|
2074
|
-
position: [
|
|
2075
|
-
-0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
|
|
2076
|
-
0,
|
|
2077
|
-
0.01
|
|
2078
|
-
],
|
|
2079
|
-
children: [
|
|
2080
|
-
/* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
|
|
2081
|
-
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2082
|
-
"meshBasicMaterial",
|
|
2083
|
-
{
|
|
2084
|
-
color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
|
|
2085
|
-
}
|
|
2086
|
-
)
|
|
2087
|
-
]
|
|
2088
|
-
}
|
|
2089
|
-
)
|
|
2090
|
-
] })
|
|
2091
|
-
]
|
|
2092
|
-
}
|
|
2093
|
-
);
|
|
2094
|
-
},
|
|
2095
|
-
[selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig]
|
|
2096
|
-
);
|
|
2097
|
-
const DefaultFeatureRenderer = React6.useCallback(
|
|
2098
|
-
({
|
|
2099
|
-
feature,
|
|
2100
|
-
position
|
|
2101
|
-
}) => {
|
|
2102
|
-
if (feature.assetUrl) {
|
|
2103
|
-
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2104
|
-
ModelLoader,
|
|
2105
|
-
{
|
|
2106
|
-
url: feature.assetUrl.url,
|
|
2107
|
-
position,
|
|
2108
|
-
scale: 0.5,
|
|
2109
|
-
rotation: [0, feature.rotation ?? 0, 0],
|
|
2110
|
-
onClick: () => handleFeatureClick(feature, null),
|
|
2111
|
-
fallbackGeometry: "box"
|
|
2112
|
-
},
|
|
2113
|
-
feature.id
|
|
2114
|
-
);
|
|
2115
|
-
}
|
|
2116
|
-
if (feature.type === "tree") {
|
|
2117
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2118
|
-
"group",
|
|
2119
|
-
{
|
|
2120
|
-
position,
|
|
2121
|
-
onClick: (e) => handleFeatureClick(feature, e),
|
|
2122
|
-
userData: { type: "feature", featureId: feature.id },
|
|
2123
|
-
children: [
|
|
2124
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.4, 0], children: [
|
|
2125
|
-
/* @__PURE__ */ jsxRuntime.jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
|
|
2126
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 9127187 })
|
|
2127
|
-
] }),
|
|
2128
|
-
/* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, 0.9, 0], children: [
|
|
2129
|
-
/* @__PURE__ */ jsxRuntime.jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
|
|
2130
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 2263842 })
|
|
2131
|
-
] })
|
|
2132
|
-
]
|
|
2133
|
-
}
|
|
2134
|
-
);
|
|
2135
|
-
}
|
|
2136
|
-
if (feature.type === "rock") {
|
|
2137
|
-
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
2138
|
-
"mesh",
|
|
2139
|
-
{
|
|
2140
|
-
position: [position[0], position[1] + 0.3, position[2]],
|
|
2141
|
-
onClick: (e) => handleFeatureClick(feature, e),
|
|
2142
|
-
userData: { type: "feature", featureId: feature.id },
|
|
2143
|
-
children: [
|
|
2144
|
-
/* @__PURE__ */ jsxRuntime.jsx("dodecahedronGeometry", { args: [0.3, 0] }),
|
|
2145
|
-
/* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: 8421504 })
|
|
2146
|
-
]
|
|
2147
|
-
}
|
|
2148
|
-
);
|
|
2149
|
-
}
|
|
2150
|
-
return null;
|
|
2151
|
-
},
|
|
2152
|
-
[handleFeatureClick]
|
|
2153
|
-
);
|
|
2377
|
+
const followTarget = React8.useMemo(() => {
|
|
2378
|
+
if (player) {
|
|
2379
|
+
return [
|
|
2380
|
+
(player.x + (player.width ?? 32) / 2) / pixelsPerUnit,
|
|
2381
|
+
(worldHeight - player.y - (player.height ?? 48) / 2) / pixelsPerUnit,
|
|
2382
|
+
0
|
|
2383
|
+
];
|
|
2384
|
+
}
|
|
2385
|
+
const selected = units.find((u) => u.id === selectedUnitId);
|
|
2386
|
+
if (selected) {
|
|
2387
|
+
const sx = selected.x ?? selected.position?.x ?? 0;
|
|
2388
|
+
const sz = selected.z ?? selected.y ?? selected.position?.y ?? 0;
|
|
2389
|
+
return [
|
|
2390
|
+
(sx - gridBounds.minX) * gridConfig.cellSize,
|
|
2391
|
+
0,
|
|
2392
|
+
(sz - gridBounds.minZ) * gridConfig.cellSize
|
|
2393
|
+
];
|
|
2394
|
+
}
|
|
2395
|
+
return cameraTarget;
|
|
2396
|
+
}, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
|
|
2154
2397
|
if (externalLoading || assetsLoading && preloadAssets.length > 0) {
|
|
2155
2398
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2156
2399
|
Canvas3DLoadingState,
|
|
@@ -2203,6 +2446,13 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
2203
2446
|
},
|
|
2204
2447
|
children: [
|
|
2205
2448
|
/* @__PURE__ */ jsxRuntime.jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
|
|
2449
|
+
cameraMode === "follow" && /* @__PURE__ */ jsxRuntime.jsx(
|
|
2450
|
+
FollowCamera,
|
|
2451
|
+
{
|
|
2452
|
+
target: followTarget,
|
|
2453
|
+
offset: player ? [0, 2, 9] : [5, 7, 5]
|
|
2454
|
+
}
|
|
2455
|
+
),
|
|
2206
2456
|
/* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: 0.6 }),
|
|
2207
2457
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2208
2458
|
"directionalLight",
|
|
@@ -2210,7 +2460,9 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
2210
2460
|
position: [10, 20, 10],
|
|
2211
2461
|
intensity: 0.8,
|
|
2212
2462
|
castShadow: shadows,
|
|
2213
|
-
"shadow-mapSize": [2048, 2048]
|
|
2463
|
+
"shadow-mapSize": [2048, 2048],
|
|
2464
|
+
"shadow-bias": -4e-4,
|
|
2465
|
+
"shadow-normalBias": 0.04
|
|
2214
2466
|
}
|
|
2215
2467
|
),
|
|
2216
2468
|
/* @__PURE__ */ jsxRuntime.jsx("hemisphereLight", { intensity: 0.3, color: "#87ceeb", groundColor: "#362d1d" }),
|
|
@@ -2236,15 +2488,49 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
2236
2488
|
fadeStrength: 1
|
|
2237
2489
|
}
|
|
2238
2490
|
),
|
|
2239
|
-
|
|
2491
|
+
player ? (
|
|
2492
|
+
/* Side-scroller mode — LOLO-owned pixel space mapped to world units */
|
|
2493
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
2494
|
+
SideScene,
|
|
2495
|
+
{
|
|
2496
|
+
player,
|
|
2497
|
+
platforms,
|
|
2498
|
+
worldHeight,
|
|
2499
|
+
ppu: pixelsPerUnit,
|
|
2500
|
+
playerSprite,
|
|
2501
|
+
tileSprites,
|
|
2502
|
+
interpolate: interpolateUnits,
|
|
2503
|
+
features,
|
|
2504
|
+
events,
|
|
2505
|
+
assetManifest
|
|
2506
|
+
}
|
|
2507
|
+
)
|
|
2508
|
+
) : /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
|
|
2240
2509
|
tiles.map((tile, index) => {
|
|
2241
2510
|
const position = gridToWorld2(
|
|
2242
2511
|
tile.x,
|
|
2243
2512
|
tile.z ?? tile.y ?? 0,
|
|
2244
2513
|
tile.elevation ?? 0
|
|
2245
2514
|
);
|
|
2246
|
-
const
|
|
2247
|
-
|
|
2515
|
+
const key = tile.id ?? `tile-${index}`;
|
|
2516
|
+
if (CustomTileRenderer) {
|
|
2517
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CustomTileRenderer, { tile, position }, key);
|
|
2518
|
+
}
|
|
2519
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2520
|
+
DefaultTile,
|
|
2521
|
+
{
|
|
2522
|
+
tile,
|
|
2523
|
+
position,
|
|
2524
|
+
model: tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""],
|
|
2525
|
+
isSelected: tile.id ? selectedTileIds.includes(tile.id) : false,
|
|
2526
|
+
isHovered: hoveredTile?.id === tile.id,
|
|
2527
|
+
isValidMove: validMoves.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
|
|
2528
|
+
isAttackTarget: attackTargets.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
|
|
2529
|
+
onTileClick: handleTileClick,
|
|
2530
|
+
onTileHover: handleTileHover
|
|
2531
|
+
},
|
|
2532
|
+
key
|
|
2533
|
+
);
|
|
2248
2534
|
}),
|
|
2249
2535
|
features.map((feature, index) => {
|
|
2250
2536
|
const position = gridToWorld2(
|
|
@@ -2252,17 +2538,51 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
2252
2538
|
feature.z ?? feature.y ?? 0,
|
|
2253
2539
|
(feature.elevation ?? 0) + 0.5
|
|
2254
2540
|
);
|
|
2255
|
-
const
|
|
2256
|
-
|
|
2541
|
+
const key = feature.id ?? `feature-${index}`;
|
|
2542
|
+
if (CustomFeatureRenderer) {
|
|
2543
|
+
return /* @__PURE__ */ jsxRuntime.jsx(CustomFeatureRenderer, { feature, position }, key);
|
|
2544
|
+
}
|
|
2545
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2546
|
+
DefaultFeature,
|
|
2547
|
+
{
|
|
2548
|
+
feature,
|
|
2549
|
+
position,
|
|
2550
|
+
model: feature.assetUrl ?? assetManifest?.features?.[feature.type],
|
|
2551
|
+
onFeatureClick: handleFeatureClick
|
|
2552
|
+
},
|
|
2553
|
+
key
|
|
2554
|
+
);
|
|
2257
2555
|
}),
|
|
2258
2556
|
units.map((unit) => {
|
|
2259
2557
|
const position = gridToWorld2(
|
|
2260
|
-
unit.x ?? 0,
|
|
2261
|
-
unit.z ?? unit.y ?? 0,
|
|
2558
|
+
unit.x ?? unit.position?.x ?? 0,
|
|
2559
|
+
unit.z ?? unit.y ?? unit.position?.y ?? 0,
|
|
2262
2560
|
(unit.elevation ?? 0) + 0.5
|
|
2263
2561
|
);
|
|
2264
|
-
|
|
2265
|
-
|
|
2562
|
+
return /* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: CustomUnitRenderer ? /* @__PURE__ */ jsxRuntime.jsx(CustomUnitRenderer, { unit, position: [0, 0, 0] }) : /* @__PURE__ */ jsxRuntime.jsx(
|
|
2563
|
+
DefaultUnit,
|
|
2564
|
+
{
|
|
2565
|
+
unit,
|
|
2566
|
+
position: [0, 0, 0],
|
|
2567
|
+
model: unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""],
|
|
2568
|
+
isSelected: selectedUnitId === unit.id,
|
|
2569
|
+
unitScale,
|
|
2570
|
+
cellSize: gridConfig.cellSize,
|
|
2571
|
+
resolveUnitFrame,
|
|
2572
|
+
onUnitClick: handleUnitClick
|
|
2573
|
+
}
|
|
2574
|
+
) }, unit.id);
|
|
2575
|
+
}),
|
|
2576
|
+
events.map((ev, i) => {
|
|
2577
|
+
const position = gridToWorld2(ev.x, ev.z ?? ev.y ?? 0, 0);
|
|
2578
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
2579
|
+
EventMarker,
|
|
2580
|
+
{
|
|
2581
|
+
event: ev,
|
|
2582
|
+
position: [position[0], position[1] + 1.4, position[2]]
|
|
2583
|
+
},
|
|
2584
|
+
ev.id ?? `ev-${i}`
|
|
2585
|
+
);
|
|
2266
2586
|
})
|
|
2267
2587
|
] }),
|
|
2268
2588
|
children,
|
|
@@ -2270,6 +2590,7 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
2270
2590
|
drei.OrbitControls,
|
|
2271
2591
|
{
|
|
2272
2592
|
ref: controlsRef,
|
|
2593
|
+
enabled: cameraMode !== "follow",
|
|
2273
2594
|
target: cameraTarget,
|
|
2274
2595
|
enableDamping: true,
|
|
2275
2596
|
dampingFactor: 0.05,
|
|
@@ -2302,9 +2623,6 @@ var GameCanvas3D = React6.forwardRef(
|
|
|
2302
2623
|
}
|
|
2303
2624
|
);
|
|
2304
2625
|
GameCanvas3D.displayName = "GameCanvas3D";
|
|
2305
|
-
function Canvas3D(props) {
|
|
2306
|
-
return /* @__PURE__ */ jsxRuntime.jsx(GameCanvas3D, { ...props });
|
|
2307
|
-
}
|
|
2308
2626
|
var DEFAULT_FAMILY = "lucide";
|
|
2309
2627
|
var VALID_FAMILIES = [
|
|
2310
2628
|
"lucide",
|
|
@@ -2355,7 +2673,7 @@ function getIconFamilyServerSnapshot() {
|
|
|
2355
2673
|
return DEFAULT_FAMILY;
|
|
2356
2674
|
}
|
|
2357
2675
|
function useIconFamily() {
|
|
2358
|
-
return
|
|
2676
|
+
return React8.useSyncExternalStore(
|
|
2359
2677
|
subscribeIconFamily,
|
|
2360
2678
|
getIconFamilySnapshot,
|
|
2361
2679
|
getIconFamilyServerSnapshot
|
|
@@ -2377,7 +2695,7 @@ function loadLib(key, importer) {
|
|
|
2377
2695
|
return p;
|
|
2378
2696
|
}
|
|
2379
2697
|
function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
|
|
2380
|
-
const Lazy =
|
|
2698
|
+
const Lazy = React8__namespace.default.lazy(async () => {
|
|
2381
2699
|
const lib = await loadLib(libKey, importer);
|
|
2382
2700
|
const Comp = pick(lib);
|
|
2383
2701
|
if (!Comp) {
|
|
@@ -2387,7 +2705,7 @@ function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
|
|
|
2387
2705
|
return { default: Comp };
|
|
2388
2706
|
});
|
|
2389
2707
|
const Wrapped = (props) => /* @__PURE__ */ jsxRuntime.jsx(
|
|
2390
|
-
|
|
2708
|
+
React8__namespace.default.Suspense,
|
|
2391
2709
|
{
|
|
2392
2710
|
fallback: /* @__PURE__ */ jsxRuntime.jsx(
|
|
2393
2711
|
"span",
|
|
@@ -3056,7 +3374,7 @@ var Icon = ({
|
|
|
3056
3374
|
const directIcon = typeof icon === "string" ? void 0 : icon;
|
|
3057
3375
|
const effectiveName = typeof icon === "string" ? icon : name;
|
|
3058
3376
|
const family = useIconFamily();
|
|
3059
|
-
const RenderedComponent =
|
|
3377
|
+
const RenderedComponent = React8__namespace.default.useMemo(() => {
|
|
3060
3378
|
if (directIcon) return null;
|
|
3061
3379
|
return effectiveName ? resolveIconForFamily(effectiveName, family) : null;
|
|
3062
3380
|
}, [directIcon, effectiveName, family]);
|
|
@@ -3104,6 +3422,143 @@ var Icon = ({
|
|
|
3104
3422
|
);
|
|
3105
3423
|
};
|
|
3106
3424
|
Icon.displayName = "Icon";
|
|
3425
|
+
|
|
3426
|
+
// lib/atlasSlice.ts
|
|
3427
|
+
var atlasCache = /* @__PURE__ */ new Map();
|
|
3428
|
+
function isTilesheet(a) {
|
|
3429
|
+
return typeof a.tileWidth === "number";
|
|
3430
|
+
}
|
|
3431
|
+
function getAtlas(url, onReady) {
|
|
3432
|
+
if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
|
|
3433
|
+
atlasCache.set(url, void 0);
|
|
3434
|
+
fetch(url).then((r) => r.json()).then((json) => {
|
|
3435
|
+
atlasCache.set(url, json);
|
|
3436
|
+
onReady();
|
|
3437
|
+
}).catch(() => {
|
|
3438
|
+
atlasCache.set(url, null);
|
|
3439
|
+
});
|
|
3440
|
+
return void 0;
|
|
3441
|
+
}
|
|
3442
|
+
function subRectFor(atlas, sprite) {
|
|
3443
|
+
if (isTilesheet(atlas)) {
|
|
3444
|
+
let col;
|
|
3445
|
+
let row;
|
|
3446
|
+
if (sprite.includes(",")) {
|
|
3447
|
+
const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
|
|
3448
|
+
col = c;
|
|
3449
|
+
row = r;
|
|
3450
|
+
} else {
|
|
3451
|
+
const i = Number(sprite);
|
|
3452
|
+
if (!Number.isFinite(i)) return null;
|
|
3453
|
+
col = i % atlas.columns;
|
|
3454
|
+
row = Math.floor(i / atlas.columns);
|
|
3455
|
+
}
|
|
3456
|
+
if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
|
|
3457
|
+
const margin = atlas.margin ?? 0;
|
|
3458
|
+
const spacing = atlas.spacing ?? 0;
|
|
3459
|
+
return {
|
|
3460
|
+
sx: margin + col * (atlas.tileWidth + spacing),
|
|
3461
|
+
sy: margin + row * (atlas.tileHeight + spacing),
|
|
3462
|
+
sw: atlas.tileWidth,
|
|
3463
|
+
sh: atlas.tileHeight
|
|
3464
|
+
};
|
|
3465
|
+
}
|
|
3466
|
+
const st = atlas.subTextures[sprite];
|
|
3467
|
+
if (!st) return null;
|
|
3468
|
+
return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
|
|
3469
|
+
}
|
|
3470
|
+
function isAtlasAsset(asset) {
|
|
3471
|
+
return !!asset && typeof asset.atlas === "string" && typeof asset.sprite === "string";
|
|
3472
|
+
}
|
|
3473
|
+
var imageCache = /* @__PURE__ */ new Map();
|
|
3474
|
+
var imageWaiters = /* @__PURE__ */ new Map();
|
|
3475
|
+
function getSheetImage(url, onReady) {
|
|
3476
|
+
const cached = imageCache.get(url);
|
|
3477
|
+
if (cached) return cached;
|
|
3478
|
+
(imageWaiters.get(url) ?? imageWaiters.set(url, /* @__PURE__ */ new Set()).get(url)).add(onReady);
|
|
3479
|
+
if (!imageCache.has(url)) {
|
|
3480
|
+
imageCache.set(url, null);
|
|
3481
|
+
const img = new Image();
|
|
3482
|
+
img.crossOrigin = "anonymous";
|
|
3483
|
+
img.onload = () => {
|
|
3484
|
+
imageCache.set(url, img);
|
|
3485
|
+
imageWaiters.get(url)?.forEach((fn) => fn());
|
|
3486
|
+
imageWaiters.delete(url);
|
|
3487
|
+
};
|
|
3488
|
+
img.src = url;
|
|
3489
|
+
}
|
|
3490
|
+
return null;
|
|
3491
|
+
}
|
|
3492
|
+
function AtlasImage({
|
|
3493
|
+
asset,
|
|
3494
|
+
size,
|
|
3495
|
+
width,
|
|
3496
|
+
height,
|
|
3497
|
+
fill = false,
|
|
3498
|
+
fit = "contain",
|
|
3499
|
+
alt,
|
|
3500
|
+
className,
|
|
3501
|
+
style,
|
|
3502
|
+
"aria-hidden": ariaHidden
|
|
3503
|
+
}) {
|
|
3504
|
+
const [, bump] = React8__namespace.useReducer((x) => x + 1, 0);
|
|
3505
|
+
const canvasRef = React8__namespace.useRef(null);
|
|
3506
|
+
const sliced = isAtlasAsset(asset);
|
|
3507
|
+
const atlas = sliced ? getAtlas(asset.atlas, bump) : void 0;
|
|
3508
|
+
const img = sliced && asset?.url ? getSheetImage(asset.url, bump) : null;
|
|
3509
|
+
const rect = sliced && atlas ? subRectFor(atlas, asset.sprite) : null;
|
|
3510
|
+
React8__namespace.useEffect(() => {
|
|
3511
|
+
const canvas = canvasRef.current;
|
|
3512
|
+
if (!canvas || !img || !rect) return;
|
|
3513
|
+
canvas.width = rect.sw;
|
|
3514
|
+
canvas.height = rect.sh;
|
|
3515
|
+
const ctx = canvas.getContext("2d");
|
|
3516
|
+
if (!ctx) return;
|
|
3517
|
+
ctx.imageSmoothingEnabled = false;
|
|
3518
|
+
ctx.clearRect(0, 0, rect.sw, rect.sh);
|
|
3519
|
+
ctx.drawImage(img, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
|
|
3520
|
+
}, [img, rect?.sx, rect?.sy, rect?.sw, rect?.sh]);
|
|
3521
|
+
const w = fill ? "100%" : width ?? size;
|
|
3522
|
+
const h = fill ? "100%" : height ?? size;
|
|
3523
|
+
const boxStyle = {
|
|
3524
|
+
...fill ? { position: "absolute", inset: 0 } : {},
|
|
3525
|
+
width: w,
|
|
3526
|
+
height: h,
|
|
3527
|
+
objectFit: fit,
|
|
3528
|
+
imageRendering: "pixelated",
|
|
3529
|
+
...style
|
|
3530
|
+
};
|
|
3531
|
+
if (!asset?.url) return null;
|
|
3532
|
+
if (sliced) {
|
|
3533
|
+
if (!rect || !img) {
|
|
3534
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: cn("inline-block flex-shrink-0", className), style: { ...boxStyle, objectFit: void 0 } });
|
|
3535
|
+
}
|
|
3536
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3537
|
+
"canvas",
|
|
3538
|
+
{
|
|
3539
|
+
ref: canvasRef,
|
|
3540
|
+
role: ariaHidden ? void 0 : "img",
|
|
3541
|
+
"aria-hidden": ariaHidden,
|
|
3542
|
+
"aria-label": ariaHidden ? void 0 : alt ?? asset.name ?? asset.category ?? "",
|
|
3543
|
+
className: cn("flex-shrink-0", className),
|
|
3544
|
+
style: boxStyle
|
|
3545
|
+
}
|
|
3546
|
+
);
|
|
3547
|
+
}
|
|
3548
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
3549
|
+
"img",
|
|
3550
|
+
{
|
|
3551
|
+
src: asset.url,
|
|
3552
|
+
alt: alt ?? asset.name ?? asset.category ?? "",
|
|
3553
|
+
"aria-hidden": ariaHidden,
|
|
3554
|
+
...typeof w === "number" ? { width: w } : {},
|
|
3555
|
+
...typeof h === "number" ? { height: h } : {},
|
|
3556
|
+
className: cn("flex-shrink-0", className),
|
|
3557
|
+
style: boxStyle
|
|
3558
|
+
}
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
AtlasImage.displayName = "AtlasImage";
|
|
3107
3562
|
var variantStyles = {
|
|
3108
3563
|
primary: [
|
|
3109
3564
|
"bg-primary text-primary-foreground",
|
|
@@ -3176,7 +3631,7 @@ function resolveIconProp(value, sizeClass) {
|
|
|
3176
3631
|
const IconComp = value;
|
|
3177
3632
|
return /* @__PURE__ */ jsxRuntime.jsx(IconComp, { className: sizeClass });
|
|
3178
3633
|
}
|
|
3179
|
-
if (
|
|
3634
|
+
if (React8__namespace.default.isValidElement(value)) {
|
|
3180
3635
|
return value;
|
|
3181
3636
|
}
|
|
3182
3637
|
if (typeof value === "object" && value !== null && isIconLike(value)) {
|
|
@@ -3185,7 +3640,7 @@ function resolveIconProp(value, sizeClass) {
|
|
|
3185
3640
|
}
|
|
3186
3641
|
return value;
|
|
3187
3642
|
}
|
|
3188
|
-
var Button =
|
|
3643
|
+
var Button = React8__namespace.default.forwardRef(
|
|
3189
3644
|
({
|
|
3190
3645
|
className,
|
|
3191
3646
|
variant = "primary",
|
|
@@ -3209,7 +3664,7 @@ var Button = React6__default.default.forwardRef(
|
|
|
3209
3664
|
const leftIconValue = leftIcon || iconProp;
|
|
3210
3665
|
const rightIconValue = rightIcon || iconRightProp;
|
|
3211
3666
|
const px = size === "sm" ? 16 : size === "lg" ? 20 : 16;
|
|
3212
|
-
const resolvedLeftIcon = iconAsset?.url ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
3667
|
+
const resolvedLeftIcon = iconAsset?.url ? /* @__PURE__ */ jsxRuntime.jsx(AtlasImage, { asset: iconAsset, size: px, alt: iconAsset.name ?? iconAsset.category ?? "", className: "flex-shrink-0" }) : resolveIconProp(leftIconValue, iconSizeStyles[size]);
|
|
3213
3668
|
const resolvedRightIcon = resolveIconProp(rightIconValue, iconSizeStyles[size]);
|
|
3214
3669
|
const handleClick = (e) => {
|
|
3215
3670
|
if (action) {
|
|
@@ -3333,7 +3788,7 @@ var Typography = ({
|
|
|
3333
3788
|
}) => {
|
|
3334
3789
|
const variant = variantProp ?? (level ? `h${level}` : "body1");
|
|
3335
3790
|
const Component2 = as || defaultElements[variant];
|
|
3336
|
-
return
|
|
3791
|
+
return React8__namespace.default.createElement(
|
|
3337
3792
|
Component2,
|
|
3338
3793
|
{
|
|
3339
3794
|
id,
|
|
@@ -3406,7 +3861,7 @@ var Stack = ({
|
|
|
3406
3861
|
};
|
|
3407
3862
|
const isHorizontal = direction === "horizontal";
|
|
3408
3863
|
const directionClass = responsive && isHorizontal ? reverse ? "flex-col-reverse md:flex-row-reverse" : "flex-col md:flex-row" : isHorizontal ? reverse ? "flex-row-reverse" : "flex-row" : reverse ? "flex-col-reverse" : "flex-col";
|
|
3409
|
-
return
|
|
3864
|
+
return React8__namespace.default.createElement(
|
|
3410
3865
|
Component2,
|
|
3411
3866
|
{
|
|
3412
3867
|
className: cn(
|
|
@@ -3499,8 +3954,8 @@ function GameBoard3D({
|
|
|
3499
3954
|
const turn = row ? num(row.turn) : 0;
|
|
3500
3955
|
const currentTeam = row ? str(row.currentTeam) : "player";
|
|
3501
3956
|
const isGameOver = result !== "none";
|
|
3502
|
-
const gameEndEmittedRef =
|
|
3503
|
-
|
|
3957
|
+
const gameEndEmittedRef = React8.useRef(false);
|
|
3958
|
+
React8.useEffect(() => {
|
|
3504
3959
|
if ((result === "victory" || result === "defeat") && gameEndEvent) {
|
|
3505
3960
|
if (!gameEndEmittedRef.current) {
|
|
3506
3961
|
gameEndEmittedRef.current = true;
|
|
@@ -3510,7 +3965,7 @@ function GameBoard3D({
|
|
|
3510
3965
|
gameEndEmittedRef.current = false;
|
|
3511
3966
|
}
|
|
3512
3967
|
}, [result, gameEndEvent, eventBus]);
|
|
3513
|
-
const checkGameEnd =
|
|
3968
|
+
const checkGameEnd = React8.useCallback(() => {
|
|
3514
3969
|
const alivePlayer = entityUnits.filter((u) => unitTeam(u) === "player" && unitHealth(u) > 0);
|
|
3515
3970
|
const aliveEnemy = entityUnits.filter((u) => unitTeam(u) === "enemy" && unitHealth(u) > 0);
|
|
3516
3971
|
if (alivePlayer.length === 0 && gameEndEvent) {
|
|
@@ -3519,7 +3974,7 @@ function GameBoard3D({
|
|
|
3519
3974
|
eventBus.emit(`UI:${gameEndEvent}`, { result: "victory" });
|
|
3520
3975
|
}
|
|
3521
3976
|
}, [entityUnits, gameEndEvent, eventBus]);
|
|
3522
|
-
const handleUnitClickCallback =
|
|
3977
|
+
const handleUnitClickCallback = React8.useCallback((isoUnit) => {
|
|
3523
3978
|
if (phase !== "action" || !selectedUnitId || !attackEvent) return;
|
|
3524
3979
|
const attackerRow = entityUnits.find((u) => str(u.id) === selectedUnitId);
|
|
3525
3980
|
const targetRow = entityUnits.find((u) => str(u.id) === isoUnit.id);
|
|
@@ -3539,13 +3994,13 @@ function GameBoard3D({
|
|
|
3539
3994
|
setTimeout(checkGameEnd, 100);
|
|
3540
3995
|
}
|
|
3541
3996
|
}, [phase, selectedUnitId, entityUnits, attackEvent, eventBus, checkGameEnd]);
|
|
3542
|
-
const handleEndTurn =
|
|
3997
|
+
const handleEndTurn = React8.useCallback(() => {
|
|
3543
3998
|
if (endTurnEvent) eventBus.emit(`UI:${endTurnEvent}`, {});
|
|
3544
3999
|
}, [endTurnEvent, eventBus]);
|
|
3545
|
-
const handleCancel =
|
|
4000
|
+
const handleCancel = React8.useCallback(() => {
|
|
3546
4001
|
if (cancelEvent) eventBus.emit(`UI:${cancelEvent}`, {});
|
|
3547
4002
|
}, [cancelEvent, eventBus]);
|
|
3548
|
-
const handlePlayAgain =
|
|
4003
|
+
const handlePlayAgain = React8.useCallback(() => {
|
|
3549
4004
|
if (playAgainEvent) eventBus.emit(`UI:${playAgainEvent}`, {});
|
|
3550
4005
|
}, [playAgainEvent, eventBus]);
|
|
3551
4006
|
return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: cn("game-board-3d block w-full min-h-[85vh] relative", className), children: [
|
|
@@ -3636,24 +4091,24 @@ function GameBoard3D({
|
|
|
3636
4091
|
GameBoard3D.displayName = "GameBoard3D";
|
|
3637
4092
|
function useTapReveal(options = {}) {
|
|
3638
4093
|
const { onReveal, onDismiss, refs, enabled = true } = options;
|
|
3639
|
-
const [revealed, setRevealed] =
|
|
3640
|
-
const onRevealRef =
|
|
3641
|
-
const onDismissRef =
|
|
4094
|
+
const [revealed, setRevealed] = React8.useState(false);
|
|
4095
|
+
const onRevealRef = React8.useRef(onReveal);
|
|
4096
|
+
const onDismissRef = React8.useRef(onDismiss);
|
|
3642
4097
|
onRevealRef.current = onReveal;
|
|
3643
4098
|
onDismissRef.current = onDismiss;
|
|
3644
|
-
const reveal =
|
|
4099
|
+
const reveal = React8.useCallback(() => {
|
|
3645
4100
|
setRevealed((wasRevealed) => {
|
|
3646
4101
|
if (!wasRevealed) onRevealRef.current?.();
|
|
3647
4102
|
return true;
|
|
3648
4103
|
});
|
|
3649
4104
|
}, []);
|
|
3650
|
-
const dismiss =
|
|
4105
|
+
const dismiss = React8.useCallback(() => {
|
|
3651
4106
|
setRevealed((wasRevealed) => {
|
|
3652
4107
|
if (wasRevealed) onDismissRef.current?.();
|
|
3653
4108
|
return false;
|
|
3654
4109
|
});
|
|
3655
4110
|
}, []);
|
|
3656
|
-
const onPointerDown =
|
|
4111
|
+
const onPointerDown = React8.useCallback(
|
|
3657
4112
|
(e) => {
|
|
3658
4113
|
if (!enabled || e.pointerType === "mouse") return;
|
|
3659
4114
|
if (revealed) dismiss();
|
|
@@ -3661,7 +4116,7 @@ function useTapReveal(options = {}) {
|
|
|
3661
4116
|
},
|
|
3662
4117
|
[enabled, revealed, reveal, dismiss]
|
|
3663
4118
|
);
|
|
3664
|
-
|
|
4119
|
+
React8.useEffect(() => {
|
|
3665
4120
|
if (!revealed || typeof document === "undefined") return;
|
|
3666
4121
|
const onDocDown = (ev) => {
|
|
3667
4122
|
const target = ev.target;
|
|
@@ -3781,7 +4236,7 @@ var positionStyles = {
|
|
|
3781
4236
|
fixed: "fixed",
|
|
3782
4237
|
sticky: "sticky"
|
|
3783
4238
|
};
|
|
3784
|
-
var Box =
|
|
4239
|
+
var Box = React8__namespace.default.forwardRef(
|
|
3785
4240
|
({
|
|
3786
4241
|
padding,
|
|
3787
4242
|
paddingX,
|
|
@@ -3813,20 +4268,20 @@ var Box = React6__default.default.forwardRef(
|
|
|
3813
4268
|
...rest
|
|
3814
4269
|
}, ref) => {
|
|
3815
4270
|
const eventBus = useEventBus();
|
|
3816
|
-
const handleClick =
|
|
4271
|
+
const handleClick = React8.useCallback((e) => {
|
|
3817
4272
|
if (action) {
|
|
3818
4273
|
e.stopPropagation();
|
|
3819
4274
|
eventBus.emit(`UI:${action}`, actionPayload ?? {});
|
|
3820
4275
|
}
|
|
3821
4276
|
onClick?.(e);
|
|
3822
4277
|
}, [action, actionPayload, eventBus, onClick]);
|
|
3823
|
-
const handleMouseEnter =
|
|
4278
|
+
const handleMouseEnter = React8.useCallback((e) => {
|
|
3824
4279
|
if (hoverEvent) {
|
|
3825
4280
|
eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
|
|
3826
4281
|
}
|
|
3827
4282
|
onMouseEnter?.(e);
|
|
3828
4283
|
}, [hoverEvent, eventBus, onMouseEnter]);
|
|
3829
|
-
const handleMouseLeave =
|
|
4284
|
+
const handleMouseLeave = React8.useCallback((e) => {
|
|
3830
4285
|
if (hoverEvent) {
|
|
3831
4286
|
eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
|
|
3832
4287
|
}
|
|
@@ -3834,19 +4289,19 @@ var Box = React6__default.default.forwardRef(
|
|
|
3834
4289
|
}, [hoverEvent, eventBus, onMouseLeave]);
|
|
3835
4290
|
const { triggerProps } = useTapReveal({
|
|
3836
4291
|
enabled: tapReveal && !!hoverEvent,
|
|
3837
|
-
onReveal:
|
|
4292
|
+
onReveal: React8.useCallback(() => {
|
|
3838
4293
|
if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
|
|
3839
4294
|
}, [hoverEvent, eventBus]),
|
|
3840
|
-
onDismiss:
|
|
4295
|
+
onDismiss: React8.useCallback(() => {
|
|
3841
4296
|
if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
|
|
3842
4297
|
}, [hoverEvent, eventBus])
|
|
3843
4298
|
});
|
|
3844
|
-
const handlePointerDown =
|
|
4299
|
+
const handlePointerDown = React8.useCallback((e) => {
|
|
3845
4300
|
if (hoverEvent && tapReveal) triggerProps.onPointerDown(e);
|
|
3846
4301
|
onPointerDown?.(e);
|
|
3847
4302
|
}, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
|
|
3848
4303
|
const isClickable = action || onClick;
|
|
3849
|
-
return
|
|
4304
|
+
return React8__namespace.default.createElement(
|
|
3850
4305
|
Component2,
|
|
3851
4306
|
{
|
|
3852
4307
|
ref,
|
|
@@ -5086,10 +5541,10 @@ var Avl3DOrbitalNode = ({
|
|
|
5086
5541
|
position,
|
|
5087
5542
|
onClick
|
|
5088
5543
|
}) => {
|
|
5089
|
-
const groupRef =
|
|
5090
|
-
const [hovered, setHovered] =
|
|
5091
|
-
const scale =
|
|
5092
|
-
const currentScale =
|
|
5544
|
+
const groupRef = React8.useRef(null);
|
|
5545
|
+
const [hovered, setHovered] = React8.useState(false);
|
|
5546
|
+
const scale = React8.useMemo(() => 0.6 + Math.min(traitCount, 8) * 0.3, [traitCount]);
|
|
5547
|
+
const currentScale = React8.useRef(scale);
|
|
5093
5548
|
const targetScale = hovered ? scale * 1.06 : scale;
|
|
5094
5549
|
fiber.useFrame((_, delta) => {
|
|
5095
5550
|
if (!groupRef.current) return;
|
|
@@ -5231,7 +5686,7 @@ var Avl3DCrossWire = ({
|
|
|
5231
5686
|
eventName,
|
|
5232
5687
|
color = AVL_3D_COLORS.crossWire
|
|
5233
5688
|
}) => {
|
|
5234
|
-
const { tubeArgs, midpoint } =
|
|
5689
|
+
const { tubeArgs, midpoint } = React8.useMemo(() => {
|
|
5235
5690
|
const curve = arcCurve3D(from, to, 2);
|
|
5236
5691
|
const mid = curve.getPoint(0.5);
|
|
5237
5692
|
return {
|
|
@@ -5270,9 +5725,9 @@ var Avl3DApplicationScene = ({
|
|
|
5270
5725
|
data,
|
|
5271
5726
|
onOrbitalClick
|
|
5272
5727
|
}) => {
|
|
5273
|
-
const groupRef =
|
|
5274
|
-
const [, setHoverTrigger] =
|
|
5275
|
-
const orbitalPositions =
|
|
5728
|
+
const groupRef = React8.useRef(null);
|
|
5729
|
+
const [, setHoverTrigger] = React8.useState(0);
|
|
5730
|
+
const orbitalPositions = React8.useMemo(() => {
|
|
5276
5731
|
const baseRadius = Math.max(4, data.orbitals.length * 1.5);
|
|
5277
5732
|
if (data.crossLinks.length === 0) {
|
|
5278
5733
|
return goldenSpiralPositions(data.orbitals.length, baseRadius);
|
|
@@ -5285,7 +5740,7 @@ var Avl3DApplicationScene = ({
|
|
|
5285
5740
|
})).filter((e) => e.from >= 0 && e.to >= 0);
|
|
5286
5741
|
return forceDirectedPositions(data.orbitals.length, edges, baseRadius);
|
|
5287
5742
|
}, [data.orbitals, data.crossLinks]);
|
|
5288
|
-
const positionByName =
|
|
5743
|
+
const positionByName = React8.useMemo(() => {
|
|
5289
5744
|
const map = /* @__PURE__ */ new Map();
|
|
5290
5745
|
data.orbitals.forEach((orb, i) => {
|
|
5291
5746
|
const pos = orbitalPositions[i];
|
|
@@ -5368,7 +5823,7 @@ var Avl3DEntityCore = ({
|
|
|
5368
5823
|
position,
|
|
5369
5824
|
fields
|
|
5370
5825
|
}) => {
|
|
5371
|
-
const coreRef =
|
|
5826
|
+
const coreRef = React8.useRef(null);
|
|
5372
5827
|
fiber.useFrame((_, delta) => {
|
|
5373
5828
|
if (!coreRef.current) return;
|
|
5374
5829
|
coreRef.current.rotation.y += delta * 0.3;
|
|
@@ -5521,8 +5976,8 @@ function TraitOrbit({
|
|
|
5521
5976
|
onClick,
|
|
5522
5977
|
onHover
|
|
5523
5978
|
}) {
|
|
5524
|
-
const groupRef =
|
|
5525
|
-
const angleRef =
|
|
5979
|
+
const groupRef = React8.useRef(null);
|
|
5980
|
+
const angleRef = React8.useRef(Math.random() * Math.PI * 2);
|
|
5526
5981
|
fiber.useFrame((_, delta) => {
|
|
5527
5982
|
angleRef.current += delta * speed;
|
|
5528
5983
|
if (!groupRef.current) return;
|
|
@@ -5653,9 +6108,9 @@ var Avl3DOrbitalScene = ({
|
|
|
5653
6108
|
highlightedTrait,
|
|
5654
6109
|
onTraitHighlight
|
|
5655
6110
|
}) => {
|
|
5656
|
-
const [localHighlight, setLocalHighlight] =
|
|
6111
|
+
const [localHighlight, setLocalHighlight] = React8.useState(null);
|
|
5657
6112
|
const activeHighlight = highlightedTrait ?? localHighlight;
|
|
5658
|
-
const traitOrbits =
|
|
6113
|
+
const traitOrbits = React8.useMemo(() => {
|
|
5659
6114
|
return data.traits.map((trait, i) => ({
|
|
5660
6115
|
name: trait.name,
|
|
5661
6116
|
radius: 3 + i * 1.2,
|
|
@@ -5666,7 +6121,7 @@ var Avl3DOrbitalScene = ({
|
|
|
5666
6121
|
transitionCount: trait.transitionCount
|
|
5667
6122
|
}));
|
|
5668
6123
|
}, [data.traits]);
|
|
5669
|
-
const pagePositions =
|
|
6124
|
+
const pagePositions = React8.useMemo(() => {
|
|
5670
6125
|
const outerRadius = 3 + data.traits.length * 1.2 + 1.5;
|
|
5671
6126
|
return data.pages.map((_, i) => {
|
|
5672
6127
|
const angle = 2 * Math.PI * i / data.pages.length - Math.PI / 2;
|
|
@@ -5730,12 +6185,12 @@ var Avl3DStateNode = ({
|
|
|
5730
6185
|
outgoingCount = 0,
|
|
5731
6186
|
onClick
|
|
5732
6187
|
}) => {
|
|
5733
|
-
const [hovered, setHovered] =
|
|
6188
|
+
const [hovered, setHovered] = React8.useState(false);
|
|
5734
6189
|
const baseColor = active ? AVL_3D_COLORS.stateActive : AVL_3D_COLORS.stateIdle;
|
|
5735
6190
|
const emissiveColor = active ? AVL_3D_COLORS.stateActive : AVL_3D_COLORS.stateEdge;
|
|
5736
6191
|
const emissiveIntensity = active ? 2 : hovered ? 0.8 : 0.3;
|
|
5737
6192
|
const targetScale = hovered ? 1.08 : 1;
|
|
5738
|
-
const currentScale =
|
|
6193
|
+
const currentScale = React8.useRef(1);
|
|
5739
6194
|
fiber.useFrame((_, delta) => {
|
|
5740
6195
|
currentScale.current = THREE6.MathUtils.damp(currentScale.current, targetScale, 6, delta);
|
|
5741
6196
|
});
|
|
@@ -5909,8 +6364,8 @@ var Avl3DTransitionArc = ({
|
|
|
5909
6364
|
onClick,
|
|
5910
6365
|
color = AVL_3D_COLORS.transitionArc
|
|
5911
6366
|
}) => {
|
|
5912
|
-
const [hovered, setHovered] =
|
|
5913
|
-
const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } =
|
|
6367
|
+
const [hovered, setHovered] = React8.useState(false);
|
|
6368
|
+
const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } = React8.useMemo(() => {
|
|
5914
6369
|
const offset = 1.5 + index * 0.8;
|
|
5915
6370
|
const curve = isSelf ? selfLoopCurve3D(from, 1.2) : arcCurve3D(from, to, offset);
|
|
5916
6371
|
const mid = curve.getPoint(0.5);
|
|
@@ -6043,7 +6498,7 @@ var Avl3DTraitScene = ({
|
|
|
6043
6498
|
data,
|
|
6044
6499
|
onTransitionClick
|
|
6045
6500
|
}) => {
|
|
6046
|
-
const statePositions =
|
|
6501
|
+
const statePositions = React8.useMemo(() => {
|
|
6047
6502
|
const radius = Math.max(3, data.states.length * 0.8);
|
|
6048
6503
|
const positions = fibonacciSpherePositions(data.states.length, radius);
|
|
6049
6504
|
const posMap = /* @__PURE__ */ new Map();
|
|
@@ -6053,7 +6508,7 @@ var Avl3DTraitScene = ({
|
|
|
6053
6508
|
});
|
|
6054
6509
|
return posMap;
|
|
6055
6510
|
}, [data.states]);
|
|
6056
|
-
const transitionIndices =
|
|
6511
|
+
const transitionIndices = React8.useMemo(() => {
|
|
6057
6512
|
const pairCount = /* @__PURE__ */ new Map();
|
|
6058
6513
|
return data.transitions.map((t) => {
|
|
6059
6514
|
const key = `${t.from}->${t.to}`;
|
|
@@ -6062,7 +6517,7 @@ var Avl3DTraitScene = ({
|
|
|
6062
6517
|
return idx;
|
|
6063
6518
|
});
|
|
6064
6519
|
}, [data.transitions]);
|
|
6065
|
-
const transitionCounts =
|
|
6520
|
+
const transitionCounts = React8.useMemo(() => {
|
|
6066
6521
|
const incoming = /* @__PURE__ */ new Map();
|
|
6067
6522
|
const outgoing = /* @__PURE__ */ new Map();
|
|
6068
6523
|
for (const t of data.transitions) {
|
|
@@ -6172,11 +6627,11 @@ var Avl3DExprTree = ({
|
|
|
6172
6627
|
expression,
|
|
6173
6628
|
position
|
|
6174
6629
|
}) => {
|
|
6175
|
-
const layoutResults =
|
|
6630
|
+
const layoutResults = React8.useMemo(() => {
|
|
6176
6631
|
const origin = { x: position[0], y: position[1], z: position[2] };
|
|
6177
6632
|
return treeLayout3D(expression, origin, 2.5, 1.5);
|
|
6178
6633
|
}, [expression, position]);
|
|
6179
|
-
const edges =
|
|
6634
|
+
const edges = React8.useMemo(() => {
|
|
6180
6635
|
const result = [];
|
|
6181
6636
|
function collectEdges(node, parentPos) {
|
|
6182
6637
|
const entry = layoutResults.find((r) => r.node === node);
|
|
@@ -6456,7 +6911,7 @@ var Avl3DEffects = ({
|
|
|
6456
6911
|
const dof = DOF_CONFIG[level];
|
|
6457
6912
|
const sparkles = SPARKLE_CONFIGS[level];
|
|
6458
6913
|
const stars = STARS_CONFIG[level];
|
|
6459
|
-
const starsKey =
|
|
6914
|
+
const starsKey = React8.useMemo(() => `stars-${level}`, [level]);
|
|
6460
6915
|
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
6461
6916
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
6462
6917
|
drei.Stars,
|
|
@@ -6520,16 +6975,16 @@ var DEFAULT_CONFIG2 = {
|
|
|
6520
6975
|
modelOverrides: {},
|
|
6521
6976
|
effectsEnabled: true
|
|
6522
6977
|
};
|
|
6523
|
-
var Avl3DContext =
|
|
6978
|
+
var Avl3DContext = React8.createContext(DEFAULT_CONFIG2);
|
|
6524
6979
|
function useAvl3DConfig() {
|
|
6525
|
-
return
|
|
6980
|
+
return React8.useContext(Avl3DContext);
|
|
6526
6981
|
}
|
|
6527
6982
|
function CameraController2({ targetPosition, targetLookAt, animated }) {
|
|
6528
6983
|
const { camera } = fiber.useThree();
|
|
6529
|
-
const targetPosVec =
|
|
6530
|
-
const targetLookVec =
|
|
6531
|
-
const isAnimating =
|
|
6532
|
-
|
|
6984
|
+
const targetPosVec = React8.useRef(new THREE6__namespace.Vector3(...targetPosition));
|
|
6985
|
+
const targetLookVec = React8.useRef(new THREE6__namespace.Vector3(...targetLookAt));
|
|
6986
|
+
const isAnimating = React8.useRef(false);
|
|
6987
|
+
React8.useEffect(() => {
|
|
6533
6988
|
const newTarget = new THREE6__namespace.Vector3(...targetPosition);
|
|
6534
6989
|
const newLookAt = new THREE6__namespace.Vector3(...targetLookAt);
|
|
6535
6990
|
if (!newTarget.equals(targetPosVec.current) || !newLookAt.equals(targetLookVec.current)) {
|
|
@@ -6558,8 +7013,8 @@ function CameraController2({ targetPosition, targetLookAt, animated }) {
|
|
|
6558
7013
|
return null;
|
|
6559
7014
|
}
|
|
6560
7015
|
function SceneFade({ animating, children }) {
|
|
6561
|
-
const groupRef =
|
|
6562
|
-
const opacityRef =
|
|
7016
|
+
const groupRef = React8.useRef(null);
|
|
7017
|
+
const opacityRef = React8.useRef(1);
|
|
6563
7018
|
fiber.useFrame((_, delta) => {
|
|
6564
7019
|
if (!groupRef.current) return;
|
|
6565
7020
|
const target = animating ? 0 : 1;
|
|
@@ -6583,7 +7038,7 @@ var Avl3DViewer = ({
|
|
|
6583
7038
|
modelOverrides = {}
|
|
6584
7039
|
}) => {
|
|
6585
7040
|
const { t } = hooks.useTranslate();
|
|
6586
|
-
const schema =
|
|
7041
|
+
const schema = React8.useMemo(() => {
|
|
6587
7042
|
if (typeof schemaProp === "string") {
|
|
6588
7043
|
try {
|
|
6589
7044
|
return JSON.parse(schemaProp);
|
|
@@ -6593,12 +7048,12 @@ var Avl3DViewer = ({
|
|
|
6593
7048
|
}
|
|
6594
7049
|
return schemaProp;
|
|
6595
7050
|
}, [schemaProp]);
|
|
6596
|
-
const configValue =
|
|
7051
|
+
const configValue = React8.useMemo(() => ({
|
|
6597
7052
|
modelOverrides,
|
|
6598
7053
|
effectsEnabled: effects
|
|
6599
7054
|
}), [modelOverrides, effects]);
|
|
6600
|
-
const [state, dispatch] =
|
|
6601
|
-
|
|
7055
|
+
const [state, dispatch] = React8.useReducer(zoomReducer, initialZoomState);
|
|
7056
|
+
React8.useEffect(() => {
|
|
6602
7057
|
if (initialOrbital) {
|
|
6603
7058
|
dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: initialOrbital, targetPosition: { x: 0, y: 0 } });
|
|
6604
7059
|
setTimeout(() => dispatch({ type: "ANIMATION_COMPLETE" }), 0);
|
|
@@ -6610,35 +7065,35 @@ var Avl3DViewer = ({
|
|
|
6610
7065
|
}
|
|
6611
7066
|
}
|
|
6612
7067
|
}, [initialOrbital, initialTrait]);
|
|
6613
|
-
|
|
7068
|
+
React8.useEffect(() => {
|
|
6614
7069
|
onZoomChange?.(state.level, {
|
|
6615
7070
|
orbital: state.selectedOrbital ?? void 0,
|
|
6616
7071
|
trait: state.selectedTrait ?? void 0
|
|
6617
7072
|
});
|
|
6618
7073
|
}, [state.level, state.selectedOrbital, state.selectedTrait, onZoomChange]);
|
|
6619
|
-
|
|
7074
|
+
React8.useEffect(() => {
|
|
6620
7075
|
if (!state.animating) return;
|
|
6621
7076
|
const timer = setTimeout(() => {
|
|
6622
7077
|
dispatch({ type: "ANIMATION_COMPLETE" });
|
|
6623
7078
|
}, 800);
|
|
6624
7079
|
return () => clearTimeout(timer);
|
|
6625
7080
|
}, [state.animating]);
|
|
6626
|
-
const handleKeyDown =
|
|
7081
|
+
const handleKeyDown = React8.useCallback((e) => {
|
|
6627
7082
|
if (e.key === "Escape") {
|
|
6628
7083
|
dispatch({ type: "ZOOM_OUT" });
|
|
6629
7084
|
}
|
|
6630
7085
|
}, []);
|
|
6631
|
-
const handleOrbitalClick =
|
|
7086
|
+
const handleOrbitalClick = React8.useCallback((name) => {
|
|
6632
7087
|
dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: name, targetPosition: { x: 0, y: 0 } });
|
|
6633
7088
|
}, []);
|
|
6634
|
-
const handleTraitClick =
|
|
7089
|
+
const handleTraitClick = React8.useCallback((name) => {
|
|
6635
7090
|
dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
|
|
6636
7091
|
}, []);
|
|
6637
|
-
const [highlightedTrait, setHighlightedTrait] =
|
|
6638
|
-
const handleTransitionClick =
|
|
7092
|
+
const [highlightedTrait, setHighlightedTrait] = React8__namespace.default.useState(null);
|
|
7093
|
+
const handleTransitionClick = React8.useCallback((index) => {
|
|
6639
7094
|
dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
|
|
6640
7095
|
}, []);
|
|
6641
|
-
const handleBreadcrumbClick =
|
|
7096
|
+
const handleBreadcrumbClick = React8.useCallback((targetLevel) => {
|
|
6642
7097
|
const levelOrder = ["application", "orbital", "trait", "transition"];
|
|
6643
7098
|
const currentIdx = levelOrder.indexOf(state.level);
|
|
6644
7099
|
const targetIdx = levelOrder.indexOf(targetLevel);
|
|
@@ -6647,7 +7102,7 @@ var Avl3DViewer = ({
|
|
|
6647
7102
|
}
|
|
6648
7103
|
}, [state.level]);
|
|
6649
7104
|
const cameraConfig = CAMERA_POSITIONS[state.level];
|
|
6650
|
-
const sceneContent =
|
|
7105
|
+
const sceneContent = React8.useMemo(() => {
|
|
6651
7106
|
switch (state.level) {
|
|
6652
7107
|
case "application": {
|
|
6653
7108
|
const data = parseApplicationLevel(schema);
|
|
@@ -6721,7 +7176,7 @@ var Avl3DViewer = ({
|
|
|
6721
7176
|
gap: "xs",
|
|
6722
7177
|
align: "center",
|
|
6723
7178
|
className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
|
|
6724
|
-
children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxRuntime.jsxs(
|
|
7179
|
+
children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxRuntime.jsxs(React8__namespace.default.Fragment, { children: [
|
|
6725
7180
|
i > 0 && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
|
|
6726
7181
|
i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
6727
7182
|
Box,
|
|
@@ -7008,7 +7463,6 @@ exports.Avl3DTransitionScene = Avl3DTransitionScene;
|
|
|
7008
7463
|
exports.Avl3DViewer = Avl3DViewer;
|
|
7009
7464
|
exports.CAMERA_POSITIONS = CAMERA_POSITIONS;
|
|
7010
7465
|
exports.Camera3D = Camera3D;
|
|
7011
|
-
exports.Canvas3D = Canvas3D;
|
|
7012
7466
|
exports.Canvas3DErrorBoundary = Canvas3DErrorBoundary;
|
|
7013
7467
|
exports.Canvas3DLoadingState = Canvas3DLoadingState;
|
|
7014
7468
|
exports.GameBoard3D = GameBoard3D;
|