@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.
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- var React6 = require('react');
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 React6__default = /*#__PURE__*/_interopDefault(React6);
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 = React6.useRef(false);
51
- React6.useEffect(() => {
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 = React6.forwardRef(
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 = React6.useRef(null);
86
- const initialPosition = React6.useRef(new THREE6__namespace.Vector3(...position));
87
- const initialTarget = React6.useRef(new THREE6__namespace.Vector3(...target));
88
- React6.useEffect(() => {
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
- React6.useImperativeHandle(ref, () => ({
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 React6.Component {
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] = React6.useState({
342
- model: null,
343
- isLoading: false,
344
- error: null
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
- log2.debug("Loading", { url });
352
- setState((prev) => ({ ...prev, isLoading: true, error: null }));
353
- const assetRoot = resourceBasePath || detectAssetRoot(url);
354
- const loader = new GLTFLoader.GLTFLoader();
355
- loader.setResourcePath(assetRoot);
356
- loader.load(
357
- url,
358
- (gltf) => {
359
- log2.debug("Loaded", { url });
360
- setState({
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 = React6.useMemo(() => {
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 normFactor = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useRef(null);
700
- const canvasRef = React6.useRef(null);
701
- const rendererRef = React6.useRef(null);
702
- const sceneRef = React6.useRef(null);
703
- const cameraRef = React6.useRef(null);
704
- const controlsRef = React6.useRef(null);
705
- const gridHelperRef = React6.useRef(null);
706
- const rafRef = React6.useRef(0);
707
- const [isReady, setIsReady] = React6.useState(false);
708
- const [dimensions, setDimensions] = React6.useState({ width: 0, height: 0 });
709
- const initialCameraPosition = React6.useMemo(
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
- React6.useEffect(() => {
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
- React6.useEffect(() => {
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 = React6.useCallback((x, y, z) => {
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 = React6.useCallback((x, y, z) => {
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 = React6.useCallback(() => {
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 = React6.useCallback(
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 = React6.useRef(customLoader || new AssetLoader());
896
- const [state, setState] = React6.useState({
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
- React6.useEffect(() => {
942
+ React8.useEffect(() => {
904
943
  if (preloadUrls.length > 0) {
905
944
  preload(preloadUrls);
906
945
  }
907
946
  }, []);
908
- const updateProgress = React6.useCallback((loaded, total) => {
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback((url) => {
1063
+ const hasModel = React8.useCallback((url) => {
1025
1064
  return loaderRef.current.hasModel(url);
1026
1065
  }, []);
1027
- const hasTexture = React6.useCallback((url) => {
1066
+ const hasTexture = React8.useCallback((url) => {
1028
1067
  return loaderRef.current.hasTexture(url);
1029
1068
  }, []);
1030
- const getModel = React6.useCallback((url) => {
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 = React6.useCallback((url) => {
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 = React6.useCallback(() => {
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 = React6.useRef(/* @__PURE__ */ new Map());
1069
- const addNode = React6.useCallback((node) => {
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 = React6.useCallback((id) => {
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 = React6.useCallback((id) => {
1122
+ const getNode = React8.useCallback((id) => {
1084
1123
  return nodesRef.current.get(id);
1085
1124
  }, []);
1086
- const updateNodePosition = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback((type) => {
1153
+ const getNodesByType = React8.useCallback((type) => {
1115
1154
  return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
1116
1155
  }, []);
1117
- const getNodesInBounds = React6.useCallback(
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 = React6.useCallback(() => {
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 = React6.useCallback((type) => {
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 = React6.useRef(new THREE6__namespace.Raycaster());
1155
- const mouse = React6.useRef(new THREE6__namespace.Vector2());
1156
- const clientToNDC = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useContext(providers.EventBusContext);
1399
+ const context = React8.useContext(providers.EventBusContext);
1361
1400
  const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
1362
1401
  const scope = providers.useTraitScope();
1363
- return React6.useMemo(() => {
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 React6.useCallback(
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 = React6.useRef(options);
1465
+ const optionsRef = React8.useRef(options);
1427
1466
  optionsRef.current = options;
1428
- const handleTileClick = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useRef(/* @__PURE__ */ new Map());
1582
- const loadingRef = React6.useRef(/* @__PURE__ */ new Set());
1583
- const [pendingCount, setPendingCount] = React6.useState(0);
1584
- const [, forceTick] = React6.useState(0);
1585
- const atlasUrls = React6.useMemo(() => {
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
- React6.useEffect(() => {
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 = React6.useMemo(() => {
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 = React6.useCallback((unitId) => {
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
- React6.useEffect(() => {
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 = React6.useRef(null);
1688
- const matRef = React6.useRef(null);
1689
- const [aspect, setAspect] = React6.useState(1);
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
- var GameCanvas3D = React6.forwardRef(
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 = React6.useRef(null);
1768
- const controlsRef = React6.useRef(null);
1769
- const [hoveredTile, setHoveredTile] = React6.useState(null);
1770
- const [internalError, setInternalError] = React6.useState(null);
1771
- React6.useEffect(() => {
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 = React6.useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
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 = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(
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 = React6.useCallback(
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
- React6.useImperativeHandle(ref, () => ({
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useCallback(
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 = React6.useMemo(() => {
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 DefaultTileRenderer = React6.useCallback(
1949
- ({ tile, position }) => {
1950
- const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
1951
- const isHovered = hoveredTile?.id === tile.id;
1952
- const isValidMove = validMoves.some(
1953
- (m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
1954
- );
1955
- const isAttackTarget = attackTargets.some(
1956
- (m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)
1957
- );
1958
- let color = 8421504;
1959
- if (tile.type === "water") color = 4491468;
1960
- else if (tile.type === "grass") color = 4500036;
1961
- else if (tile.type === "sand") color = 14535816;
1962
- else if (tile.type === "rock") color = 8947848;
1963
- else if (tile.type === "snow") color = 15658734;
1964
- let emissive = 0;
1965
- if (isSelected) emissive = 4473924;
1966
- else if (isAttackTarget) emissive = 4456448;
1967
- else if (isValidMove) emissive = 17408;
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
- /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
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 Renderer = CustomTileRenderer || DefaultTileRenderer;
2247
- return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { tile, position }, tile.id ?? `tile-${index}`);
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 Renderer = CustomFeatureRenderer || DefaultFeatureRenderer;
2256
- return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { feature, position }, feature.id ?? `feature-${index}`);
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
- const Renderer = CustomUnitRenderer || DefaultUnitRenderer;
2265
- return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { unit, position }, unit.id);
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 React6.useSyncExternalStore(
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 = React6__default.default.lazy(async () => {
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
- React6__default.default.Suspense,
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 = React6__default.default.useMemo(() => {
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 (React6__default.default.isValidElement(value)) {
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 = React6__default.default.forwardRef(
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("img", { src: iconAsset.url, alt: iconAsset.name ?? iconAsset.category ?? "", width: px, height: px, style: { imageRendering: "pixelated", objectFit: "contain", width: px, height: px }, className: "flex-shrink-0" }) : resolveIconProp(leftIconValue, iconSizeStyles[size]);
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 React6__default.default.createElement(
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 React6__default.default.createElement(
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 = React6.useRef(false);
3503
- React6.useEffect(() => {
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 = React6.useCallback(() => {
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 = React6.useCallback((isoUnit) => {
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 = React6.useCallback(() => {
3997
+ const handleEndTurn = React8.useCallback(() => {
3543
3998
  if (endTurnEvent) eventBus.emit(`UI:${endTurnEvent}`, {});
3544
3999
  }, [endTurnEvent, eventBus]);
3545
- const handleCancel = React6.useCallback(() => {
4000
+ const handleCancel = React8.useCallback(() => {
3546
4001
  if (cancelEvent) eventBus.emit(`UI:${cancelEvent}`, {});
3547
4002
  }, [cancelEvent, eventBus]);
3548
- const handlePlayAgain = React6.useCallback(() => {
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] = React6.useState(false);
3640
- const onRevealRef = React6.useRef(onReveal);
3641
- const onDismissRef = React6.useRef(onDismiss);
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 = React6.useCallback(() => {
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 = React6.useCallback(() => {
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 = React6.useCallback(
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
- React6.useEffect(() => {
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 = React6__default.default.forwardRef(
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 = React6.useCallback((e) => {
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 = React6.useCallback((e) => {
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 = React6.useCallback((e) => {
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: React6.useCallback(() => {
4292
+ onReveal: React8.useCallback(() => {
3838
4293
  if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
3839
4294
  }, [hoverEvent, eventBus]),
3840
- onDismiss: React6.useCallback(() => {
4295
+ onDismiss: React8.useCallback(() => {
3841
4296
  if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
3842
4297
  }, [hoverEvent, eventBus])
3843
4298
  });
3844
- const handlePointerDown = React6.useCallback((e) => {
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 React6__default.default.createElement(
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 = React6.useRef(null);
5090
- const [hovered, setHovered] = React6.useState(false);
5091
- const scale = React6.useMemo(() => 0.6 + Math.min(traitCount, 8) * 0.3, [traitCount]);
5092
- const currentScale = React6.useRef(scale);
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 } = React6.useMemo(() => {
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 = React6.useRef(null);
5274
- const [, setHoverTrigger] = React6.useState(0);
5275
- const orbitalPositions = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useRef(null);
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 = React6.useRef(null);
5525
- const angleRef = React6.useRef(Math.random() * Math.PI * 2);
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] = React6.useState(null);
6111
+ const [localHighlight, setLocalHighlight] = React8.useState(null);
5657
6112
  const activeHighlight = highlightedTrait ?? localHighlight;
5658
- const traitOrbits = React6.useMemo(() => {
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 = React6.useMemo(() => {
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] = React6.useState(false);
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 = React6.useRef(1);
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] = React6.useState(false);
5913
- const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(() => {
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 = React6.useMemo(() => `stars-${level}`, [level]);
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 = React6.createContext(DEFAULT_CONFIG2);
6978
+ var Avl3DContext = React8.createContext(DEFAULT_CONFIG2);
6524
6979
  function useAvl3DConfig() {
6525
- return React6.useContext(Avl3DContext);
6980
+ return React8.useContext(Avl3DContext);
6526
6981
  }
6527
6982
  function CameraController2({ targetPosition, targetLookAt, animated }) {
6528
6983
  const { camera } = fiber.useThree();
6529
- const targetPosVec = React6.useRef(new THREE6__namespace.Vector3(...targetPosition));
6530
- const targetLookVec = React6.useRef(new THREE6__namespace.Vector3(...targetLookAt));
6531
- const isAnimating = React6.useRef(false);
6532
- React6.useEffect(() => {
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 = React6.useRef(null);
6562
- const opacityRef = React6.useRef(1);
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 = React6.useMemo(() => {
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 = React6.useMemo(() => ({
7051
+ const configValue = React8.useMemo(() => ({
6597
7052
  modelOverrides,
6598
7053
  effectsEnabled: effects
6599
7054
  }), [modelOverrides, effects]);
6600
- const [state, dispatch] = React6.useReducer(zoomReducer, initialZoomState);
6601
- React6.useEffect(() => {
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
- React6.useEffect(() => {
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
- React6.useEffect(() => {
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 = React6.useCallback((e) => {
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 = React6.useCallback((name) => {
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 = React6.useCallback((name) => {
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] = React6__default.default.useState(null);
6638
- const handleTransitionClick = React6.useCallback((index) => {
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 = React6.useCallback((targetLevel) => {
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 = React6.useMemo(() => {
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(React6__default.default.Fragment, { children: [
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;