@almadar/ui 5.95.0 → 5.96.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", () => {
@@ -338,14 +336,15 @@ function detectAssetRoot(modelUrl) {
338
336
  return modelUrl.substring(0, modelUrl.lastIndexOf("/") + 1);
339
337
  }
340
338
  function useGLTFModel(url, resourceBasePath) {
341
- const [state, setState] = React6.useState({
339
+ const [state, setState] = React8.useState({
342
340
  model: null,
341
+ clips: [],
343
342
  isLoading: false,
344
343
  error: null
345
344
  });
346
- React6.useEffect(() => {
345
+ React8.useEffect(() => {
347
346
  if (!url) {
348
- setState({ model: null, isLoading: false, error: null });
347
+ setState({ model: null, clips: [], isLoading: false, error: null });
349
348
  return;
350
349
  }
351
350
  log2.debug("Loading", { url });
@@ -356,9 +355,10 @@ function useGLTFModel(url, resourceBasePath) {
356
355
  loader.load(
357
356
  url,
358
357
  (gltf) => {
359
- log2.debug("Loaded", { url });
358
+ log2.debug("Loaded", { url, clips: gltf.animations.length });
360
359
  setState({
361
360
  model: gltf.scene,
361
+ clips: gltf.animations,
362
362
  isLoading: false,
363
363
  error: null
364
364
  });
@@ -368,6 +368,7 @@ function useGLTFModel(url, resourceBasePath) {
368
368
  log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
369
369
  setState({
370
370
  model: null,
371
+ clips: [],
371
372
  isLoading: false,
372
373
  error: err instanceof Error ? err : new Error(String(err))
373
374
  });
@@ -388,10 +389,11 @@ function ModelLoader({
388
389
  fallbackGeometry = "box",
389
390
  castShadow = true,
390
391
  receiveShadow = true,
391
- resourceBasePath
392
+ resourceBasePath,
393
+ animation
392
394
  }) {
393
- const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
394
- const model = React6.useMemo(() => {
395
+ const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
396
+ const model = React8.useMemo(() => {
395
397
  if (!loadedModel) return null;
396
398
  const cloned = SkeletonUtils.clone(loadedModel);
397
399
  cloned.updateMatrixWorld(true);
@@ -403,7 +405,22 @@ function ModelLoader({
403
405
  });
404
406
  return cloned;
405
407
  }, [loadedModel, castShadow, receiveShadow]);
406
- const normFactor = React6.useMemo(() => {
408
+ const mixer = React8.useMemo(() => model ? new THREE6__namespace.AnimationMixer(model) : null, [model]);
409
+ React8.useEffect(() => {
410
+ if (!mixer || !animation || clips.length === 0) return;
411
+ const wanted = animation.toLowerCase();
412
+ const clip = clips.find((c) => c.name === animation) ?? clips.find((c) => c.name.toLowerCase() === wanted);
413
+ if (!clip) return;
414
+ const action = mixer.clipAction(clip);
415
+ action.reset().fadeIn(0.15).play();
416
+ return () => {
417
+ action.fadeOut(0.15);
418
+ };
419
+ }, [mixer, clips, animation]);
420
+ fiber.useFrame((_, delta) => {
421
+ mixer?.update(delta);
422
+ });
423
+ const normFactor = React8.useMemo(() => {
407
424
  if (!model) return 1;
408
425
  const box = new THREE6__namespace.Box3().setFromObject(model);
409
426
  const size = new THREE6__namespace.Vector3();
@@ -412,11 +429,11 @@ function ModelLoader({
412
429
  if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
413
430
  return 1 / maxDim;
414
431
  }, [model]);
415
- const scaleArray = React6.useMemo(() => {
432
+ const scaleArray = React8.useMemo(() => {
416
433
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;
417
434
  return [base[0] * normFactor, base[1] * normFactor, base[2] * normFactor];
418
435
  }, [scale, normFactor]);
419
- const rotationRad = React6.useMemo(() => {
436
+ const rotationRad = React8.useMemo(() => {
420
437
  return [
421
438
  rotation[0] * Math.PI / 180,
422
439
  rotation[1] * Math.PI / 180,
@@ -696,21 +713,21 @@ var DEFAULT_OPTIONS = {
696
713
  };
697
714
  function useThree3(options = {}) {
698
715
  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(
716
+ const containerRef = React8.useRef(null);
717
+ const canvasRef = React8.useRef(null);
718
+ const rendererRef = React8.useRef(null);
719
+ const sceneRef = React8.useRef(null);
720
+ const cameraRef = React8.useRef(null);
721
+ const controlsRef = React8.useRef(null);
722
+ const gridHelperRef = React8.useRef(null);
723
+ const rafRef = React8.useRef(0);
724
+ const [isReady, setIsReady] = React8.useState(false);
725
+ const [dimensions, setDimensions] = React8.useState({ width: 0, height: 0 });
726
+ const initialCameraPosition = React8.useMemo(
710
727
  () => new THREE6__namespace.Vector3(...opts.cameraPosition),
711
728
  []
712
729
  );
713
- React6.useEffect(() => {
730
+ React8.useEffect(() => {
714
731
  if (!containerRef.current) return;
715
732
  const container = containerRef.current;
716
733
  const { clientWidth, clientHeight } = container;
@@ -803,7 +820,7 @@ function useThree3(options = {}) {
803
820
  scene.clear();
804
821
  };
805
822
  }, []);
806
- React6.useEffect(() => {
823
+ React8.useEffect(() => {
807
824
  if (!cameraRef.current || !containerRef.current) return;
808
825
  const container = containerRef.current;
809
826
  const { clientWidth, clientHeight } = container;
@@ -838,20 +855,20 @@ function useThree3(options = {}) {
838
855
  animate();
839
856
  }
840
857
  }, [opts.cameraMode]);
841
- const setCameraPosition = React6.useCallback((x, y, z) => {
858
+ const setCameraPosition = React8.useCallback((x, y, z) => {
842
859
  if (cameraRef.current) {
843
860
  cameraRef.current.position.set(x, y, z);
844
861
  controlsRef.current?.update();
845
862
  }
846
863
  }, []);
847
- const lookAt = React6.useCallback((x, y, z) => {
864
+ const lookAt = React8.useCallback((x, y, z) => {
848
865
  if (cameraRef.current) {
849
866
  cameraRef.current.lookAt(x, y, z);
850
867
  controlsRef.current?.target.set(x, y, z);
851
868
  controlsRef.current?.update();
852
869
  }
853
870
  }, []);
854
- const resetCamera = React6.useCallback(() => {
871
+ const resetCamera = React8.useCallback(() => {
855
872
  if (cameraRef.current) {
856
873
  cameraRef.current.position.copy(initialCameraPosition);
857
874
  cameraRef.current.lookAt(0, 0, 0);
@@ -861,7 +878,7 @@ function useThree3(options = {}) {
861
878
  }
862
879
  }
863
880
  }, [initialCameraPosition]);
864
- const fitView = React6.useCallback(
881
+ const fitView = React8.useCallback(
865
882
  (bounds) => {
866
883
  if (!cameraRef.current) return;
867
884
  const centerX = (bounds.minX + bounds.maxX) / 2;
@@ -892,20 +909,20 @@ function useThree3(options = {}) {
892
909
  }
893
910
  function useAssetLoader(options = {}) {
894
911
  const { preloadUrls = [], loader: customLoader } = options;
895
- const loaderRef = React6.useRef(customLoader || new AssetLoader());
896
- const [state, setState] = React6.useState({
912
+ const loaderRef = React8.useRef(customLoader || new AssetLoader());
913
+ const [state, setState] = React8.useState({
897
914
  isLoading: false,
898
915
  progress: 0,
899
916
  loaded: 0,
900
917
  total: 0,
901
918
  errors: []
902
919
  });
903
- React6.useEffect(() => {
920
+ React8.useEffect(() => {
904
921
  if (preloadUrls.length > 0) {
905
922
  preload(preloadUrls);
906
923
  }
907
924
  }, []);
908
- const updateProgress = React6.useCallback((loaded, total) => {
925
+ const updateProgress = React8.useCallback((loaded, total) => {
909
926
  setState((prev) => ({
910
927
  ...prev,
911
928
  loaded,
@@ -913,7 +930,7 @@ function useAssetLoader(options = {}) {
913
930
  progress: total > 0 ? Math.round(loaded / total * 100) : 0
914
931
  }));
915
932
  }, []);
916
- const loadModel = React6.useCallback(
933
+ const loadModel = React8.useCallback(
917
934
  async (url) => {
918
935
  setState((prev) => ({ ...prev, isLoading: true }));
919
936
  try {
@@ -936,7 +953,7 @@ function useAssetLoader(options = {}) {
936
953
  },
937
954
  []
938
955
  );
939
- const loadOBJ = React6.useCallback(
956
+ const loadOBJ = React8.useCallback(
940
957
  async (url) => {
941
958
  setState((prev) => ({ ...prev, isLoading: true }));
942
959
  try {
@@ -959,7 +976,7 @@ function useAssetLoader(options = {}) {
959
976
  },
960
977
  []
961
978
  );
962
- const loadTexture = React6.useCallback(
979
+ const loadTexture = React8.useCallback(
963
980
  async (url) => {
964
981
  setState((prev) => ({ ...prev, isLoading: true }));
965
982
  try {
@@ -982,7 +999,7 @@ function useAssetLoader(options = {}) {
982
999
  },
983
1000
  []
984
1001
  );
985
- const preload = React6.useCallback(
1002
+ const preload = React8.useCallback(
986
1003
  async (urls) => {
987
1004
  setState((prev) => ({
988
1005
  ...prev,
@@ -1021,27 +1038,27 @@ function useAssetLoader(options = {}) {
1021
1038
  },
1022
1039
  [updateProgress]
1023
1040
  );
1024
- const hasModel = React6.useCallback((url) => {
1041
+ const hasModel = React8.useCallback((url) => {
1025
1042
  return loaderRef.current.hasModel(url);
1026
1043
  }, []);
1027
- const hasTexture = React6.useCallback((url) => {
1044
+ const hasTexture = React8.useCallback((url) => {
1028
1045
  return loaderRef.current.hasTexture(url);
1029
1046
  }, []);
1030
- const getModel = React6.useCallback((url) => {
1047
+ const getModel = React8.useCallback((url) => {
1031
1048
  try {
1032
1049
  return loaderRef.current.getModel(url);
1033
1050
  } catch {
1034
1051
  return void 0;
1035
1052
  }
1036
1053
  }, []);
1037
- const getTexture = React6.useCallback((url) => {
1054
+ const getTexture = React8.useCallback((url) => {
1038
1055
  try {
1039
1056
  return loaderRef.current.getTexture(url);
1040
1057
  } catch {
1041
1058
  return void 0;
1042
1059
  }
1043
1060
  }, []);
1044
- const clearCache = React6.useCallback(() => {
1061
+ const clearCache = React8.useCallback(() => {
1045
1062
  loaderRef.current.clearCache();
1046
1063
  setState({
1047
1064
  isLoading: false,
@@ -1065,25 +1082,25 @@ function useAssetLoader(options = {}) {
1065
1082
  };
1066
1083
  }
1067
1084
  function useSceneGraph() {
1068
- const nodesRef = React6.useRef(/* @__PURE__ */ new Map());
1069
- const addNode = React6.useCallback((node) => {
1085
+ const nodesRef = React8.useRef(/* @__PURE__ */ new Map());
1086
+ const addNode = React8.useCallback((node) => {
1070
1087
  const existing = nodesRef.current.get(node.id);
1071
1088
  if (existing) {
1072
1089
  existing.mesh.removeFromParent();
1073
1090
  }
1074
1091
  nodesRef.current.set(node.id, node);
1075
1092
  }, []);
1076
- const removeNode = React6.useCallback((id) => {
1093
+ const removeNode = React8.useCallback((id) => {
1077
1094
  const node = nodesRef.current.get(id);
1078
1095
  if (node) {
1079
1096
  node.mesh.removeFromParent();
1080
1097
  nodesRef.current.delete(id);
1081
1098
  }
1082
1099
  }, []);
1083
- const getNode = React6.useCallback((id) => {
1100
+ const getNode = React8.useCallback((id) => {
1084
1101
  return nodesRef.current.get(id);
1085
1102
  }, []);
1086
- const updateNodePosition = React6.useCallback(
1103
+ const updateNodePosition = React8.useCallback(
1087
1104
  (id, x, y, z) => {
1088
1105
  const node = nodesRef.current.get(id);
1089
1106
  if (node) {
@@ -1093,7 +1110,7 @@ function useSceneGraph() {
1093
1110
  },
1094
1111
  []
1095
1112
  );
1096
- const updateNodeGridPosition = React6.useCallback(
1113
+ const updateNodeGridPosition = React8.useCallback(
1097
1114
  (id, gridX, gridZ) => {
1098
1115
  const node = nodesRef.current.get(id);
1099
1116
  if (node) {
@@ -1102,7 +1119,7 @@ function useSceneGraph() {
1102
1119
  },
1103
1120
  []
1104
1121
  );
1105
- const getNodeAtGrid = React6.useCallback(
1122
+ const getNodeAtGrid = React8.useCallback(
1106
1123
  (x, z, type) => {
1107
1124
  return Array.from(nodesRef.current.values()).find((node) => {
1108
1125
  const matchesGrid = node.gridPosition.x === x && node.gridPosition.z === z;
@@ -1111,10 +1128,10 @@ function useSceneGraph() {
1111
1128
  },
1112
1129
  []
1113
1130
  );
1114
- const getNodesByType = React6.useCallback((type) => {
1131
+ const getNodesByType = React8.useCallback((type) => {
1115
1132
  return Array.from(nodesRef.current.values()).filter((node) => node.type === type);
1116
1133
  }, []);
1117
- const getNodesInBounds = React6.useCallback(
1134
+ const getNodesInBounds = React8.useCallback(
1118
1135
  (minX, maxX, minZ, maxZ) => {
1119
1136
  return Array.from(nodesRef.current.values()).filter((node) => {
1120
1137
  const { x, z } = node.gridPosition;
@@ -1123,13 +1140,13 @@ function useSceneGraph() {
1123
1140
  },
1124
1141
  []
1125
1142
  );
1126
- const clearNodes = React6.useCallback(() => {
1143
+ const clearNodes = React8.useCallback(() => {
1127
1144
  nodesRef.current.forEach((node) => {
1128
1145
  node.mesh.removeFromParent();
1129
1146
  });
1130
1147
  nodesRef.current.clear();
1131
1148
  }, []);
1132
- const countNodes = React6.useCallback((type) => {
1149
+ const countNodes = React8.useCallback((type) => {
1133
1150
  if (!type) {
1134
1151
  return nodesRef.current.size;
1135
1152
  }
@@ -1151,9 +1168,9 @@ function useSceneGraph() {
1151
1168
  }
1152
1169
  function useRaycaster(options) {
1153
1170
  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(
1171
+ const raycaster = React8.useRef(new THREE6__namespace.Raycaster());
1172
+ const mouse = React8.useRef(new THREE6__namespace.Vector2());
1173
+ const clientToNDC = React8.useCallback(
1157
1174
  (clientX, clientY) => {
1158
1175
  if (!canvas) {
1159
1176
  return { x: 0, y: 0 };
@@ -1166,7 +1183,7 @@ function useRaycaster(options) {
1166
1183
  },
1167
1184
  [canvas]
1168
1185
  );
1169
- const isWithinCanvas = React6.useCallback(
1186
+ const isWithinCanvas = React8.useCallback(
1170
1187
  (clientX, clientY) => {
1171
1188
  if (!canvas) return false;
1172
1189
  const rect = canvas.getBoundingClientRect();
@@ -1174,7 +1191,7 @@ function useRaycaster(options) {
1174
1191
  },
1175
1192
  [canvas]
1176
1193
  );
1177
- const getIntersection = React6.useCallback(
1194
+ const getIntersection = React8.useCallback(
1178
1195
  (clientX, clientY, objects) => {
1179
1196
  if (!camera || !canvas) return null;
1180
1197
  const ndc = clientToNDC(clientX, clientY);
@@ -1197,7 +1214,7 @@ function useRaycaster(options) {
1197
1214
  },
1198
1215
  [camera, canvas, clientToNDC]
1199
1216
  );
1200
- const getAllIntersections = React6.useCallback(
1217
+ const getAllIntersections = React8.useCallback(
1201
1218
  (clientX, clientY, objects) => {
1202
1219
  if (!camera || !canvas) return [];
1203
1220
  const ndc = clientToNDC(clientX, clientY);
@@ -1216,7 +1233,7 @@ function useRaycaster(options) {
1216
1233
  },
1217
1234
  [camera, canvas, clientToNDC]
1218
1235
  );
1219
- const getGridCoordinates = React6.useCallback(
1236
+ const getGridCoordinates = React8.useCallback(
1220
1237
  (clientX, clientY) => {
1221
1238
  if (!camera || !canvas) return null;
1222
1239
  const ndc = clientToNDC(clientX, clientY);
@@ -1234,7 +1251,7 @@ function useRaycaster(options) {
1234
1251
  },
1235
1252
  [camera, canvas, cellSize, offsetX, offsetZ, clientToNDC]
1236
1253
  );
1237
- const getTileAtPosition = React6.useCallback(
1254
+ const getTileAtPosition = React8.useCallback(
1238
1255
  (clientX, clientY, scene) => {
1239
1256
  if (!camera || !canvas) return null;
1240
1257
  const tileMeshes = [];
@@ -1357,10 +1374,10 @@ var fallbackEventBus = {
1357
1374
  }
1358
1375
  };
1359
1376
  function useEventBus() {
1360
- const context = React6.useContext(providers.EventBusContext);
1377
+ const context = React8.useContext(providers.EventBusContext);
1361
1378
  const baseBus = context ?? getGlobalEventBus() ?? fallbackEventBus;
1362
1379
  const scope = providers.useTraitScope();
1363
- return React6.useMemo(() => {
1380
+ return React8.useMemo(() => {
1364
1381
  if (!scope) {
1365
1382
  return {
1366
1383
  ...baseBus,
@@ -1396,7 +1413,7 @@ function useEventBus() {
1396
1413
  }
1397
1414
  function useEmitEvent() {
1398
1415
  const eventBus = useEventBus();
1399
- return React6.useCallback(
1416
+ return React8.useCallback(
1400
1417
  (type, payload, source) => {
1401
1418
  eventBus.emit(type, payload, source);
1402
1419
  },
@@ -1423,9 +1440,9 @@ function useGameCanvas3DEvents(options) {
1423
1440
  onUnitAnimation
1424
1441
  } = options;
1425
1442
  const emit = useEmitEvent();
1426
- const optionsRef = React6.useRef(options);
1443
+ const optionsRef = React8.useRef(options);
1427
1444
  optionsRef.current = options;
1428
- const handleTileClick = React6.useCallback(
1445
+ const handleTileClick = React8.useCallback(
1429
1446
  (tile, event) => {
1430
1447
  if (tileClickEvent) {
1431
1448
  emit(tileClickEvent, {
@@ -1441,7 +1458,7 @@ function useGameCanvas3DEvents(options) {
1441
1458
  },
1442
1459
  [tileClickEvent, emit]
1443
1460
  );
1444
- const handleUnitClick = React6.useCallback(
1461
+ const handleUnitClick = React8.useCallback(
1445
1462
  (unit, event) => {
1446
1463
  if (unitClickEvent) {
1447
1464
  emit(unitClickEvent, {
@@ -1460,7 +1477,7 @@ function useGameCanvas3DEvents(options) {
1460
1477
  },
1461
1478
  [unitClickEvent, emit]
1462
1479
  );
1463
- const handleFeatureClick = React6.useCallback(
1480
+ const handleFeatureClick = React8.useCallback(
1464
1481
  (feature, event) => {
1465
1482
  if (featureClickEvent) {
1466
1483
  emit(featureClickEvent, {
@@ -1475,7 +1492,7 @@ function useGameCanvas3DEvents(options) {
1475
1492
  },
1476
1493
  [featureClickEvent, emit]
1477
1494
  );
1478
- const handleCanvasClick = React6.useCallback(
1495
+ const handleCanvasClick = React8.useCallback(
1479
1496
  (event) => {
1480
1497
  if (canvasClickEvent) {
1481
1498
  emit(canvasClickEvent, {
@@ -1488,7 +1505,7 @@ function useGameCanvas3DEvents(options) {
1488
1505
  },
1489
1506
  [canvasClickEvent, emit]
1490
1507
  );
1491
- const handleTileHover = React6.useCallback(
1508
+ const handleTileHover = React8.useCallback(
1492
1509
  (tile, event) => {
1493
1510
  if (tile) {
1494
1511
  if (tileHoverEvent) {
@@ -1508,7 +1525,7 @@ function useGameCanvas3DEvents(options) {
1508
1525
  },
1509
1526
  [tileHoverEvent, tileLeaveEvent, emit]
1510
1527
  );
1511
- const handleUnitAnimation = React6.useCallback(
1528
+ const handleUnitAnimation = React8.useCallback(
1512
1529
  (unitId, state) => {
1513
1530
  if (unitAnimationEvent) {
1514
1531
  emit(unitAnimationEvent, {
@@ -1521,7 +1538,7 @@ function useGameCanvas3DEvents(options) {
1521
1538
  },
1522
1539
  [unitAnimationEvent, emit]
1523
1540
  );
1524
- const handleCameraChange = React6.useCallback(
1541
+ const handleCameraChange = React8.useCallback(
1525
1542
  (position) => {
1526
1543
  if (cameraChangeEvent) {
1527
1544
  emit(cameraChangeEvent, {
@@ -1578,11 +1595,11 @@ function resolveSheetUrl(atlasUrl, relativeSheetPath) {
1578
1595
  }
1579
1596
  }
1580
1597
  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(() => {
1598
+ const atlasCacheRef = React8.useRef(/* @__PURE__ */ new Map());
1599
+ const loadingRef = React8.useRef(/* @__PURE__ */ new Set());
1600
+ const [pendingCount, setPendingCount] = React8.useState(0);
1601
+ const [, forceTick] = React8.useState(0);
1602
+ const atlasUrls = React8.useMemo(() => {
1586
1603
  const set = /* @__PURE__ */ new Set();
1587
1604
  for (const unit of units) {
1588
1605
  const url = unitAtlasUrl(unit);
@@ -1590,7 +1607,7 @@ function useUnitSpriteAtlas(units) {
1590
1607
  }
1591
1608
  return [...set];
1592
1609
  }, [units]);
1593
- React6.useEffect(() => {
1610
+ React8.useEffect(() => {
1594
1611
  const cache = atlasCacheRef.current;
1595
1612
  const loading = loadingRef.current;
1596
1613
  const toLoad = atlasUrls.filter((url) => !cache.has(url) && !loading.has(url));
@@ -1614,7 +1631,7 @@ function useUnitSpriteAtlas(units) {
1614
1631
  cancelled = true;
1615
1632
  };
1616
1633
  }, [atlasUrls]);
1617
- const sheetUrls = React6.useMemo(() => {
1634
+ const sheetUrls = React8.useMemo(() => {
1618
1635
  const urls = /* @__PURE__ */ new Set();
1619
1636
  for (const unit of units) {
1620
1637
  const atlasUrl = unitAtlasUrl(unit);
@@ -1627,7 +1644,7 @@ function useUnitSpriteAtlas(units) {
1627
1644
  }
1628
1645
  return [...urls];
1629
1646
  }, [units, pendingCount]);
1630
- const resolveUnitFrame = React6.useCallback((unitId) => {
1647
+ const resolveUnitFrame = React8.useCallback((unitId) => {
1631
1648
  const unit = units.find((u) => u.id === unitId);
1632
1649
  if (!unit) return null;
1633
1650
  const atlasUrl = unitAtlasUrl(unit);
@@ -1667,7 +1684,7 @@ function CameraController({
1667
1684
  onCameraChange
1668
1685
  }) {
1669
1686
  const { camera } = fiber.useThree();
1670
- React6.useEffect(() => {
1687
+ React8.useEffect(() => {
1671
1688
  if (onCameraChange) {
1672
1689
  onCameraChange({
1673
1690
  x: camera.position.x,
@@ -1684,9 +1701,9 @@ function UnitSpriteBillboard({
1684
1701
  height = 1.2
1685
1702
  }) {
1686
1703
  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);
1704
+ const meshRef = React8.useRef(null);
1705
+ const matRef = React8.useRef(null);
1706
+ const [aspect, setAspect] = React8.useState(1);
1690
1707
  fiber.useFrame(() => {
1691
1708
  const frame = resolveFrame();
1692
1709
  if (!frame || !texture.image) return;
@@ -1719,7 +1736,100 @@ function UnitSpriteBillboard({
1719
1736
  )
1720
1737
  ] });
1721
1738
  }
1722
- var GameCanvas3D = React6.forwardRef(
1739
+ function FollowCamera({
1740
+ target,
1741
+ offset
1742
+ }) {
1743
+ const { camera } = fiber.useThree();
1744
+ const look = React8.useRef(new THREE6__namespace.Vector3(target[0], target[1], target[2]));
1745
+ const goal = React8.useRef(new THREE6__namespace.Vector3());
1746
+ fiber.useFrame((_, delta) => {
1747
+ const t = Math.min(1, delta * 5);
1748
+ goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
1749
+ camera.position.lerp(goal.current, t);
1750
+ look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
1751
+ camera.lookAt(look.current);
1752
+ });
1753
+ return null;
1754
+ }
1755
+ function LerpedGroup({
1756
+ target,
1757
+ enabled,
1758
+ children
1759
+ }) {
1760
+ const ref = React8.useRef(null);
1761
+ const goal = React8.useRef(new THREE6__namespace.Vector3());
1762
+ fiber.useFrame((_, delta) => {
1763
+ const g = ref.current;
1764
+ if (!g) return;
1765
+ goal.current.set(target[0], target[1], target[2]);
1766
+ if (enabled) g.position.lerp(goal.current, Math.min(1, delta * 10));
1767
+ else g.position.copy(goal.current);
1768
+ });
1769
+ return /* @__PURE__ */ jsxRuntime.jsx("group", { ref, position: target, children });
1770
+ }
1771
+ var SIDE_PLATFORM_COLORS = {
1772
+ ground: "#5b8c3e",
1773
+ platform: "#8b5a2b",
1774
+ hazard: "#cc4444",
1775
+ goal: "#4488cc"
1776
+ };
1777
+ function SideScene({
1778
+ player,
1779
+ platforms,
1780
+ worldHeight,
1781
+ ppu,
1782
+ playerSprite,
1783
+ tileSprites,
1784
+ interpolate
1785
+ }) {
1786
+ const pw = player.width ?? 32;
1787
+ const ph = player.height ?? 48;
1788
+ const playerPos = [
1789
+ (player.x + pw / 2) / ppu,
1790
+ (worldHeight - player.y - ph) / ppu,
1791
+ 0
1792
+ ];
1793
+ const playerH = Math.max(ph / ppu, 0.5);
1794
+ return /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
1795
+ platforms.map((p, i) => {
1796
+ const platformType = p.type ?? "platform";
1797
+ const model = tileSprites?.[platformType]?.url;
1798
+ const topY = (worldHeight - p.y) / ppu;
1799
+ if (model) {
1800
+ const cells = Math.max(1, Math.round(p.width / ppu));
1801
+ const x0 = p.x / ppu;
1802
+ 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}`);
1803
+ }
1804
+ return /* @__PURE__ */ jsxRuntime.jsxs(
1805
+ "mesh",
1806
+ {
1807
+ position: [(p.x + p.width / 2) / ppu, topY - p.height / 2 / ppu, 0],
1808
+ children: [
1809
+ /* @__PURE__ */ jsxRuntime.jsx("boxGeometry", { args: [p.width / ppu, p.height / ppu, 1] }),
1810
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: SIDE_PLATFORM_COLORS[platformType] ?? "#888888" })
1811
+ ]
1812
+ },
1813
+ `plat-${i}`
1814
+ );
1815
+ }),
1816
+ /* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: playerPos, enabled: interpolate, children: playerSprite?.url ? /* @__PURE__ */ jsxRuntime.jsx("group", { position: [0, playerH / 2, 0], children: /* @__PURE__ */ jsxRuntime.jsx(
1817
+ ModelLoader,
1818
+ {
1819
+ url: playerSprite.url,
1820
+ scale: playerH,
1821
+ rotation: [0, player.facingRight ? 90 : -90, 0],
1822
+ animation: player.animation ?? "idle",
1823
+ fallbackGeometry: "box",
1824
+ castShadow: true
1825
+ }
1826
+ ) }) : /* @__PURE__ */ jsxRuntime.jsxs("mesh", { position: [0, playerH / 2, 0], children: [
1827
+ /* @__PURE__ */ jsxRuntime.jsx("capsuleGeometry", { args: [playerH * 0.25, playerH * 0.5, 4, 8] }),
1828
+ /* @__PURE__ */ jsxRuntime.jsx("meshStandardMaterial", { color: "#4488ff" })
1829
+ ] }) })
1830
+ ] });
1831
+ }
1832
+ var GameCanvas3D = React8.forwardRef(
1723
1833
  ({
1724
1834
  tiles = [],
1725
1835
  units = [],
@@ -1762,13 +1872,48 @@ var GameCanvas3D = React6.forwardRef(
1762
1872
  selectedTileIds = [],
1763
1873
  selectedUnitId = null,
1764
1874
  unitScale = 1,
1875
+ keyMap,
1876
+ keyUpMap,
1877
+ player,
1878
+ platforms = [],
1879
+ worldHeight = 400,
1880
+ pixelsPerUnit = 32,
1881
+ playerSprite,
1882
+ tileSprites,
1883
+ assetManifest,
1884
+ interpolateUnits = false,
1765
1885
  children
1766
1886
  }, 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(() => {
1887
+ const containerRef = React8.useRef(null);
1888
+ const controlsRef = React8.useRef(null);
1889
+ const [hoveredTile, setHoveredTile] = React8.useState(null);
1890
+ const [internalError, setInternalError] = React8.useState(null);
1891
+ const eventBus = useEventBus();
1892
+ const keysRef = React8.useRef(/* @__PURE__ */ new Set());
1893
+ React8.useEffect(() => {
1894
+ if (!keyMap && !keyUpMap) return;
1895
+ const down = (e) => {
1896
+ if (keysRef.current.has(e.code)) return;
1897
+ keysRef.current.add(e.code);
1898
+ const ev = keyMap?.[e.code];
1899
+ if (ev) {
1900
+ eventBus.emit(`UI:${ev}`, {});
1901
+ e.preventDefault();
1902
+ }
1903
+ };
1904
+ const up = (e) => {
1905
+ keysRef.current.delete(e.code);
1906
+ const ev = keyUpMap?.[e.code];
1907
+ if (ev) eventBus.emit(`UI:${ev}`, {});
1908
+ };
1909
+ window.addEventListener("keydown", down);
1910
+ window.addEventListener("keyup", up);
1911
+ return () => {
1912
+ window.removeEventListener("keydown", down);
1913
+ window.removeEventListener("keyup", up);
1914
+ };
1915
+ }, [keyMap, keyUpMap, eventBus]);
1916
+ React8.useEffect(() => {
1772
1917
  const el = containerRef.current;
1773
1918
  if (!el) return;
1774
1919
  let node = el;
@@ -1792,7 +1937,7 @@ var GameCanvas3D = React6.forwardRef(
1792
1937
  }
1793
1938
  }, []);
1794
1939
  const { sheetUrls: atlasSheetUrls, resolveUnitFrame } = useUnitSpriteAtlas(units);
1795
- const preloadUrls = React6.useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
1940
+ const preloadUrls = React8.useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
1796
1941
  const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
1797
1942
  preloadUrls,
1798
1943
  loader: customAssetLoader
@@ -1813,7 +1958,7 @@ var GameCanvas3D = React6.forwardRef(
1813
1958
  onTileHover,
1814
1959
  onUnitAnimation
1815
1960
  });
1816
- const gridBounds = React6.useMemo(() => {
1961
+ const gridBounds = React8.useMemo(() => {
1817
1962
  if (tiles.length === 0) {
1818
1963
  return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
1819
1964
  }
@@ -1826,14 +1971,14 @@ var GameCanvas3D = React6.forwardRef(
1826
1971
  maxZ: Math.max(...zs)
1827
1972
  };
1828
1973
  }, [tiles]);
1829
- const cameraTarget = React6.useMemo(() => {
1974
+ const cameraTarget = React8.useMemo(() => {
1830
1975
  return [
1831
1976
  (gridBounds.minX + gridBounds.maxX) / 2,
1832
1977
  0,
1833
1978
  (gridBounds.minZ + gridBounds.maxZ) / 2
1834
1979
  ];
1835
1980
  }, [gridBounds]);
1836
- const gridConfig = React6.useMemo(
1981
+ const gridConfig = React8.useMemo(
1837
1982
  () => ({
1838
1983
  ...DEFAULT_GRID_CONFIG,
1839
1984
  offsetX: -(gridBounds.maxX - gridBounds.minX) / 2,
@@ -1841,7 +1986,7 @@ var GameCanvas3D = React6.forwardRef(
1841
1986
  }),
1842
1987
  [gridBounds]
1843
1988
  );
1844
- const gridToWorld2 = React6.useCallback(
1989
+ const gridToWorld2 = React8.useCallback(
1845
1990
  (x, z, y = 0) => {
1846
1991
  const worldX = (x - gridBounds.minX) * gridConfig.cellSize;
1847
1992
  const worldZ = (z - gridBounds.minZ) * gridConfig.cellSize;
@@ -1849,7 +1994,7 @@ var GameCanvas3D = React6.forwardRef(
1849
1994
  },
1850
1995
  [gridBounds, gridConfig]
1851
1996
  );
1852
- React6.useImperativeHandle(ref, () => ({
1997
+ React8.useImperativeHandle(ref, () => ({
1853
1998
  getCameraPosition: () => {
1854
1999
  if (controlsRef.current) {
1855
2000
  const pos = controlsRef.current.object.position;
@@ -1887,19 +2032,19 @@ var GameCanvas3D = React6.forwardRef(
1887
2032
  features
1888
2033
  })
1889
2034
  }));
1890
- const handleTileClick = React6.useCallback(
2035
+ const handleTileClick = React8.useCallback(
1891
2036
  (tile, event) => {
1892
2037
  eventHandlers.handleTileClick(tile, event);
1893
2038
  },
1894
2039
  [eventHandlers]
1895
2040
  );
1896
- const handleUnitClick = React6.useCallback(
2041
+ const handleUnitClick = React8.useCallback(
1897
2042
  (unit, event) => {
1898
2043
  eventHandlers.handleUnitClick(unit, event);
1899
2044
  },
1900
2045
  [eventHandlers]
1901
2046
  );
1902
- const handleFeatureClick = React6.useCallback(
2047
+ const handleFeatureClick = React8.useCallback(
1903
2048
  (feature, event) => {
1904
2049
  if (event) {
1905
2050
  eventHandlers.handleFeatureClick(feature, event);
@@ -1907,7 +2052,7 @@ var GameCanvas3D = React6.forwardRef(
1907
2052
  },
1908
2053
  [eventHandlers]
1909
2054
  );
1910
- const handleTileHover = React6.useCallback(
2055
+ const handleTileHover = React8.useCallback(
1911
2056
  (tile, event) => {
1912
2057
  setHoveredTile(tile);
1913
2058
  if (event) {
@@ -1916,7 +2061,7 @@ var GameCanvas3D = React6.forwardRef(
1916
2061
  },
1917
2062
  [eventHandlers]
1918
2063
  );
1919
- const cameraConfig = React6.useMemo(() => {
2064
+ const cameraConfig = React8.useMemo(() => {
1920
2065
  const size = Math.max(
1921
2066
  gridBounds.maxX - gridBounds.minX,
1922
2067
  gridBounds.maxZ - gridBounds.minZ,
@@ -1937,6 +2082,11 @@ var GameCanvas3D = React6.forwardRef(
1937
2082
  position: [cx, d * 2, cz],
1938
2083
  fov: 45
1939
2084
  };
2085
+ case "follow":
2086
+ return {
2087
+ position: [cx, d * 0.5, cz + d],
2088
+ fov: 45
2089
+ };
1940
2090
  case "perspective":
1941
2091
  default:
1942
2092
  return {
@@ -1945,7 +2095,27 @@ var GameCanvas3D = React6.forwardRef(
1945
2095
  };
1946
2096
  }
1947
2097
  }, [cameraMode, gridBounds]);
1948
- const DefaultTileRenderer = React6.useCallback(
2098
+ const followTarget = React8.useMemo(() => {
2099
+ if (player) {
2100
+ return [
2101
+ (player.x + (player.width ?? 32) / 2) / pixelsPerUnit,
2102
+ (worldHeight - player.y - (player.height ?? 48) / 2) / pixelsPerUnit,
2103
+ 0
2104
+ ];
2105
+ }
2106
+ const selected = units.find((u) => u.id === selectedUnitId);
2107
+ if (selected) {
2108
+ const sx = selected.x ?? selected.position?.x ?? 0;
2109
+ const sz = selected.z ?? selected.y ?? selected.position?.y ?? 0;
2110
+ return [
2111
+ (sx - gridBounds.minX) * gridConfig.cellSize,
2112
+ 0,
2113
+ (sz - gridBounds.minZ) * gridConfig.cellSize
2114
+ ];
2115
+ }
2116
+ return cameraTarget;
2117
+ }, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
2118
+ const DefaultTileRenderer = React8.useCallback(
1949
2119
  ({ tile, position }) => {
1950
2120
  const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
1951
2121
  const isHovered = hoveredTile?.id === tile.id;
@@ -1966,7 +2136,8 @@ var GameCanvas3D = React6.forwardRef(
1966
2136
  else if (isAttackTarget) emissive = 4456448;
1967
2137
  else if (isValidMove) emissive = 17408;
1968
2138
  else if (isHovered) emissive = 2236962;
1969
- if (tile.modelUrl) {
2139
+ const tileModel = tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""];
2140
+ if (tileModel?.url) {
1970
2141
  return /* @__PURE__ */ jsxRuntime.jsx(
1971
2142
  "group",
1972
2143
  {
@@ -1978,7 +2149,7 @@ var GameCanvas3D = React6.forwardRef(
1978
2149
  children: /* @__PURE__ */ jsxRuntime.jsx(
1979
2150
  ModelLoader,
1980
2151
  {
1981
- url: tile.modelUrl.url,
2152
+ url: tileModel.url,
1982
2153
  scale: 0.95,
1983
2154
  fallbackGeometry: "box",
1984
2155
  castShadow: true,
@@ -2003,17 +2174,18 @@ var GameCanvas3D = React6.forwardRef(
2003
2174
  }
2004
2175
  );
2005
2176
  },
2006
- [selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover]
2177
+ [selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover, assetManifest]
2007
2178
  );
2008
2179
  const UNIT_BASE_MODEL_SCALE = 0.5;
2009
2180
  const UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
2010
2181
  const UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
2011
- const DefaultUnitRenderer = React6.useCallback(
2182
+ const DefaultUnitRenderer = React8.useCallback(
2012
2183
  ({ unit, position }) => {
2013
2184
  const isSelected = selectedUnitId === unit.id;
2014
2185
  const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
2015
2186
  const hasAtlas = unitAtlasUrl(unit) !== null;
2016
2187
  const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
2188
+ const unitModel = unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""];
2017
2189
  const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * gridConfig.cellSize;
2018
2190
  const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * gridConfig.cellSize;
2019
2191
  const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * gridConfig.cellSize;
@@ -2038,13 +2210,14 @@ var GameCanvas3D = React6.forwardRef(
2038
2210
  height: billboardHeight
2039
2211
  }
2040
2212
  ) })
2041
- ) : unit.modelUrl ? (
2042
- /* GLB unit model (box fallback while loading / on error) */
2213
+ ) : unitModel?.url ? (
2214
+ /* GLB unit model LOLO's `animation` field drives the named clip */
2043
2215
  /* @__PURE__ */ jsxRuntime.jsx(
2044
2216
  ModelLoader,
2045
2217
  {
2046
- url: unit.modelUrl.url,
2218
+ url: unitModel.url,
2047
2219
  scale: modelScale,
2220
+ animation: unit.animation ?? "idle",
2048
2221
  fallbackGeometry: "box",
2049
2222
  castShadow: true
2050
2223
  }
@@ -2092,18 +2265,19 @@ var GameCanvas3D = React6.forwardRef(
2092
2265
  }
2093
2266
  );
2094
2267
  },
2095
- [selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig]
2268
+ [selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig, assetManifest]
2096
2269
  );
2097
- const DefaultFeatureRenderer = React6.useCallback(
2270
+ const DefaultFeatureRenderer = React8.useCallback(
2098
2271
  ({
2099
2272
  feature,
2100
2273
  position
2101
2274
  }) => {
2102
- if (feature.assetUrl) {
2275
+ const featureModel = feature.assetUrl ?? assetManifest?.features?.[feature.type];
2276
+ if (featureModel?.url) {
2103
2277
  return /* @__PURE__ */ jsxRuntime.jsx(
2104
2278
  ModelLoader,
2105
2279
  {
2106
- url: feature.assetUrl.url,
2280
+ url: featureModel.url,
2107
2281
  position,
2108
2282
  scale: 0.5,
2109
2283
  rotation: [0, feature.rotation ?? 0, 0],
@@ -2149,7 +2323,7 @@ var GameCanvas3D = React6.forwardRef(
2149
2323
  }
2150
2324
  return null;
2151
2325
  },
2152
- [handleFeatureClick]
2326
+ [handleFeatureClick, assetManifest]
2153
2327
  );
2154
2328
  if (externalLoading || assetsLoading && preloadAssets.length > 0) {
2155
2329
  return /* @__PURE__ */ jsxRuntime.jsx(
@@ -2203,6 +2377,13 @@ var GameCanvas3D = React6.forwardRef(
2203
2377
  },
2204
2378
  children: [
2205
2379
  /* @__PURE__ */ jsxRuntime.jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
2380
+ cameraMode === "follow" && /* @__PURE__ */ jsxRuntime.jsx(
2381
+ FollowCamera,
2382
+ {
2383
+ target: followTarget,
2384
+ offset: player ? [0, 2, 9] : [5, 7, 5]
2385
+ }
2386
+ ),
2206
2387
  /* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: 0.6 }),
2207
2388
  /* @__PURE__ */ jsxRuntime.jsx(
2208
2389
  "directionalLight",
@@ -2210,7 +2391,9 @@ var GameCanvas3D = React6.forwardRef(
2210
2391
  position: [10, 20, 10],
2211
2392
  intensity: 0.8,
2212
2393
  castShadow: shadows,
2213
- "shadow-mapSize": [2048, 2048]
2394
+ "shadow-mapSize": [2048, 2048],
2395
+ "shadow-bias": -4e-4,
2396
+ "shadow-normalBias": 0.04
2214
2397
  }
2215
2398
  ),
2216
2399
  /* @__PURE__ */ jsxRuntime.jsx("hemisphereLight", { intensity: 0.3, color: "#87ceeb", groundColor: "#362d1d" }),
@@ -2236,7 +2419,21 @@ var GameCanvas3D = React6.forwardRef(
2236
2419
  fadeStrength: 1
2237
2420
  }
2238
2421
  ),
2239
- /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
2422
+ player ? (
2423
+ /* Side-scroller mode — LOLO-owned pixel space mapped to world units */
2424
+ /* @__PURE__ */ jsxRuntime.jsx(
2425
+ SideScene,
2426
+ {
2427
+ player,
2428
+ platforms,
2429
+ worldHeight,
2430
+ ppu: pixelsPerUnit,
2431
+ playerSprite,
2432
+ tileSprites,
2433
+ interpolate: interpolateUnits
2434
+ }
2435
+ )
2436
+ ) : /* @__PURE__ */ jsxRuntime.jsxs("group", { children: [
2240
2437
  tiles.map((tile, index) => {
2241
2438
  const position = gridToWorld2(
2242
2439
  tile.x,
@@ -2257,12 +2454,12 @@ var GameCanvas3D = React6.forwardRef(
2257
2454
  }),
2258
2455
  units.map((unit) => {
2259
2456
  const position = gridToWorld2(
2260
- unit.x ?? 0,
2261
- unit.z ?? unit.y ?? 0,
2457
+ unit.x ?? unit.position?.x ?? 0,
2458
+ unit.z ?? unit.y ?? unit.position?.y ?? 0,
2262
2459
  (unit.elevation ?? 0) + 0.5
2263
2460
  );
2264
2461
  const Renderer = CustomUnitRenderer || DefaultUnitRenderer;
2265
- return /* @__PURE__ */ jsxRuntime.jsx(Renderer, { unit, position }, unit.id);
2462
+ return /* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: /* @__PURE__ */ jsxRuntime.jsx(Renderer, { unit, position: [0, 0, 0] }) }, unit.id);
2266
2463
  })
2267
2464
  ] }),
2268
2465
  children,
@@ -2270,6 +2467,7 @@ var GameCanvas3D = React6.forwardRef(
2270
2467
  drei.OrbitControls,
2271
2468
  {
2272
2469
  ref: controlsRef,
2470
+ enabled: cameraMode !== "follow",
2273
2471
  target: cameraTarget,
2274
2472
  enableDamping: true,
2275
2473
  dampingFactor: 0.05,
@@ -2302,9 +2500,6 @@ var GameCanvas3D = React6.forwardRef(
2302
2500
  }
2303
2501
  );
2304
2502
  GameCanvas3D.displayName = "GameCanvas3D";
2305
- function Canvas3D(props) {
2306
- return /* @__PURE__ */ jsxRuntime.jsx(GameCanvas3D, { ...props });
2307
- }
2308
2503
  var DEFAULT_FAMILY = "lucide";
2309
2504
  var VALID_FAMILIES = [
2310
2505
  "lucide",
@@ -2355,7 +2550,7 @@ function getIconFamilyServerSnapshot() {
2355
2550
  return DEFAULT_FAMILY;
2356
2551
  }
2357
2552
  function useIconFamily() {
2358
- return React6.useSyncExternalStore(
2553
+ return React8.useSyncExternalStore(
2359
2554
  subscribeIconFamily,
2360
2555
  getIconFamilySnapshot,
2361
2556
  getIconFamilyServerSnapshot
@@ -2377,7 +2572,7 @@ function loadLib(key, importer) {
2377
2572
  return p;
2378
2573
  }
2379
2574
  function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
2380
- const Lazy = React6__default.default.lazy(async () => {
2575
+ const Lazy = React8__namespace.default.lazy(async () => {
2381
2576
  const lib = await loadLib(libKey, importer);
2382
2577
  const Comp = pick(lib);
2383
2578
  if (!Comp) {
@@ -2387,7 +2582,7 @@ function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
2387
2582
  return { default: Comp };
2388
2583
  });
2389
2584
  const Wrapped = (props) => /* @__PURE__ */ jsxRuntime.jsx(
2390
- React6__default.default.Suspense,
2585
+ React8__namespace.default.Suspense,
2391
2586
  {
2392
2587
  fallback: /* @__PURE__ */ jsxRuntime.jsx(
2393
2588
  "span",
@@ -3056,7 +3251,7 @@ var Icon = ({
3056
3251
  const directIcon = typeof icon === "string" ? void 0 : icon;
3057
3252
  const effectiveName = typeof icon === "string" ? icon : name;
3058
3253
  const family = useIconFamily();
3059
- const RenderedComponent = React6__default.default.useMemo(() => {
3254
+ const RenderedComponent = React8__namespace.default.useMemo(() => {
3060
3255
  if (directIcon) return null;
3061
3256
  return effectiveName ? resolveIconForFamily(effectiveName, family) : null;
3062
3257
  }, [directIcon, effectiveName, family]);
@@ -3104,6 +3299,143 @@ var Icon = ({
3104
3299
  );
3105
3300
  };
3106
3301
  Icon.displayName = "Icon";
3302
+
3303
+ // lib/atlasSlice.ts
3304
+ var atlasCache = /* @__PURE__ */ new Map();
3305
+ function isTilesheet(a) {
3306
+ return typeof a.tileWidth === "number";
3307
+ }
3308
+ function getAtlas(url, onReady) {
3309
+ if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3310
+ atlasCache.set(url, void 0);
3311
+ fetch(url).then((r) => r.json()).then((json) => {
3312
+ atlasCache.set(url, json);
3313
+ onReady();
3314
+ }).catch(() => {
3315
+ atlasCache.set(url, null);
3316
+ });
3317
+ return void 0;
3318
+ }
3319
+ function subRectFor(atlas, sprite) {
3320
+ if (isTilesheet(atlas)) {
3321
+ let col;
3322
+ let row;
3323
+ if (sprite.includes(",")) {
3324
+ const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
3325
+ col = c;
3326
+ row = r;
3327
+ } else {
3328
+ const i = Number(sprite);
3329
+ if (!Number.isFinite(i)) return null;
3330
+ col = i % atlas.columns;
3331
+ row = Math.floor(i / atlas.columns);
3332
+ }
3333
+ if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
3334
+ const margin = atlas.margin ?? 0;
3335
+ const spacing = atlas.spacing ?? 0;
3336
+ return {
3337
+ sx: margin + col * (atlas.tileWidth + spacing),
3338
+ sy: margin + row * (atlas.tileHeight + spacing),
3339
+ sw: atlas.tileWidth,
3340
+ sh: atlas.tileHeight
3341
+ };
3342
+ }
3343
+ const st = atlas.subTextures[sprite];
3344
+ if (!st) return null;
3345
+ return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
3346
+ }
3347
+ function isAtlasAsset(asset) {
3348
+ return !!asset && typeof asset.atlas === "string" && typeof asset.sprite === "string";
3349
+ }
3350
+ var imageCache = /* @__PURE__ */ new Map();
3351
+ var imageWaiters = /* @__PURE__ */ new Map();
3352
+ function getSheetImage(url, onReady) {
3353
+ const cached = imageCache.get(url);
3354
+ if (cached) return cached;
3355
+ (imageWaiters.get(url) ?? imageWaiters.set(url, /* @__PURE__ */ new Set()).get(url)).add(onReady);
3356
+ if (!imageCache.has(url)) {
3357
+ imageCache.set(url, null);
3358
+ const img = new Image();
3359
+ img.crossOrigin = "anonymous";
3360
+ img.onload = () => {
3361
+ imageCache.set(url, img);
3362
+ imageWaiters.get(url)?.forEach((fn) => fn());
3363
+ imageWaiters.delete(url);
3364
+ };
3365
+ img.src = url;
3366
+ }
3367
+ return null;
3368
+ }
3369
+ function AtlasImage({
3370
+ asset,
3371
+ size,
3372
+ width,
3373
+ height,
3374
+ fill = false,
3375
+ fit = "contain",
3376
+ alt,
3377
+ className,
3378
+ style,
3379
+ "aria-hidden": ariaHidden
3380
+ }) {
3381
+ const [, bump] = React8__namespace.useReducer((x) => x + 1, 0);
3382
+ const canvasRef = React8__namespace.useRef(null);
3383
+ const sliced = isAtlasAsset(asset);
3384
+ const atlas = sliced ? getAtlas(asset.atlas, bump) : void 0;
3385
+ const img = sliced && asset?.url ? getSheetImage(asset.url, bump) : null;
3386
+ const rect = sliced && atlas ? subRectFor(atlas, asset.sprite) : null;
3387
+ React8__namespace.useEffect(() => {
3388
+ const canvas = canvasRef.current;
3389
+ if (!canvas || !img || !rect) return;
3390
+ canvas.width = rect.sw;
3391
+ canvas.height = rect.sh;
3392
+ const ctx = canvas.getContext("2d");
3393
+ if (!ctx) return;
3394
+ ctx.imageSmoothingEnabled = false;
3395
+ ctx.clearRect(0, 0, rect.sw, rect.sh);
3396
+ ctx.drawImage(img, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
3397
+ }, [img, rect?.sx, rect?.sy, rect?.sw, rect?.sh]);
3398
+ const w = fill ? "100%" : width ?? size;
3399
+ const h = fill ? "100%" : height ?? size;
3400
+ const boxStyle = {
3401
+ ...fill ? { position: "absolute", inset: 0 } : {},
3402
+ width: w,
3403
+ height: h,
3404
+ objectFit: fit,
3405
+ imageRendering: "pixelated",
3406
+ ...style
3407
+ };
3408
+ if (!asset?.url) return null;
3409
+ if (sliced) {
3410
+ if (!rect || !img) {
3411
+ return /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": true, className: cn("inline-block flex-shrink-0", className), style: { ...boxStyle, objectFit: void 0 } });
3412
+ }
3413
+ return /* @__PURE__ */ jsxRuntime.jsx(
3414
+ "canvas",
3415
+ {
3416
+ ref: canvasRef,
3417
+ role: ariaHidden ? void 0 : "img",
3418
+ "aria-hidden": ariaHidden,
3419
+ "aria-label": ariaHidden ? void 0 : alt ?? asset.name ?? asset.category ?? "",
3420
+ className: cn("flex-shrink-0", className),
3421
+ style: boxStyle
3422
+ }
3423
+ );
3424
+ }
3425
+ return /* @__PURE__ */ jsxRuntime.jsx(
3426
+ "img",
3427
+ {
3428
+ src: asset.url,
3429
+ alt: alt ?? asset.name ?? asset.category ?? "",
3430
+ "aria-hidden": ariaHidden,
3431
+ ...typeof w === "number" ? { width: w } : {},
3432
+ ...typeof h === "number" ? { height: h } : {},
3433
+ className: cn("flex-shrink-0", className),
3434
+ style: boxStyle
3435
+ }
3436
+ );
3437
+ }
3438
+ AtlasImage.displayName = "AtlasImage";
3107
3439
  var variantStyles = {
3108
3440
  primary: [
3109
3441
  "bg-primary text-primary-foreground",
@@ -3176,7 +3508,7 @@ function resolveIconProp(value, sizeClass) {
3176
3508
  const IconComp = value;
3177
3509
  return /* @__PURE__ */ jsxRuntime.jsx(IconComp, { className: sizeClass });
3178
3510
  }
3179
- if (React6__default.default.isValidElement(value)) {
3511
+ if (React8__namespace.default.isValidElement(value)) {
3180
3512
  return value;
3181
3513
  }
3182
3514
  if (typeof value === "object" && value !== null && isIconLike(value)) {
@@ -3185,7 +3517,7 @@ function resolveIconProp(value, sizeClass) {
3185
3517
  }
3186
3518
  return value;
3187
3519
  }
3188
- var Button = React6__default.default.forwardRef(
3520
+ var Button = React8__namespace.default.forwardRef(
3189
3521
  ({
3190
3522
  className,
3191
3523
  variant = "primary",
@@ -3209,7 +3541,7 @@ var Button = React6__default.default.forwardRef(
3209
3541
  const leftIconValue = leftIcon || iconProp;
3210
3542
  const rightIconValue = rightIcon || iconRightProp;
3211
3543
  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]);
3544
+ 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
3545
  const resolvedRightIcon = resolveIconProp(rightIconValue, iconSizeStyles[size]);
3214
3546
  const handleClick = (e) => {
3215
3547
  if (action) {
@@ -3333,7 +3665,7 @@ var Typography = ({
3333
3665
  }) => {
3334
3666
  const variant = variantProp ?? (level ? `h${level}` : "body1");
3335
3667
  const Component2 = as || defaultElements[variant];
3336
- return React6__default.default.createElement(
3668
+ return React8__namespace.default.createElement(
3337
3669
  Component2,
3338
3670
  {
3339
3671
  id,
@@ -3406,7 +3738,7 @@ var Stack = ({
3406
3738
  };
3407
3739
  const isHorizontal = direction === "horizontal";
3408
3740
  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(
3741
+ return React8__namespace.default.createElement(
3410
3742
  Component2,
3411
3743
  {
3412
3744
  className: cn(
@@ -3499,8 +3831,8 @@ function GameBoard3D({
3499
3831
  const turn = row ? num(row.turn) : 0;
3500
3832
  const currentTeam = row ? str(row.currentTeam) : "player";
3501
3833
  const isGameOver = result !== "none";
3502
- const gameEndEmittedRef = React6.useRef(false);
3503
- React6.useEffect(() => {
3834
+ const gameEndEmittedRef = React8.useRef(false);
3835
+ React8.useEffect(() => {
3504
3836
  if ((result === "victory" || result === "defeat") && gameEndEvent) {
3505
3837
  if (!gameEndEmittedRef.current) {
3506
3838
  gameEndEmittedRef.current = true;
@@ -3510,7 +3842,7 @@ function GameBoard3D({
3510
3842
  gameEndEmittedRef.current = false;
3511
3843
  }
3512
3844
  }, [result, gameEndEvent, eventBus]);
3513
- const checkGameEnd = React6.useCallback(() => {
3845
+ const checkGameEnd = React8.useCallback(() => {
3514
3846
  const alivePlayer = entityUnits.filter((u) => unitTeam(u) === "player" && unitHealth(u) > 0);
3515
3847
  const aliveEnemy = entityUnits.filter((u) => unitTeam(u) === "enemy" && unitHealth(u) > 0);
3516
3848
  if (alivePlayer.length === 0 && gameEndEvent) {
@@ -3519,7 +3851,7 @@ function GameBoard3D({
3519
3851
  eventBus.emit(`UI:${gameEndEvent}`, { result: "victory" });
3520
3852
  }
3521
3853
  }, [entityUnits, gameEndEvent, eventBus]);
3522
- const handleUnitClickCallback = React6.useCallback((isoUnit) => {
3854
+ const handleUnitClickCallback = React8.useCallback((isoUnit) => {
3523
3855
  if (phase !== "action" || !selectedUnitId || !attackEvent) return;
3524
3856
  const attackerRow = entityUnits.find((u) => str(u.id) === selectedUnitId);
3525
3857
  const targetRow = entityUnits.find((u) => str(u.id) === isoUnit.id);
@@ -3539,13 +3871,13 @@ function GameBoard3D({
3539
3871
  setTimeout(checkGameEnd, 100);
3540
3872
  }
3541
3873
  }, [phase, selectedUnitId, entityUnits, attackEvent, eventBus, checkGameEnd]);
3542
- const handleEndTurn = React6.useCallback(() => {
3874
+ const handleEndTurn = React8.useCallback(() => {
3543
3875
  if (endTurnEvent) eventBus.emit(`UI:${endTurnEvent}`, {});
3544
3876
  }, [endTurnEvent, eventBus]);
3545
- const handleCancel = React6.useCallback(() => {
3877
+ const handleCancel = React8.useCallback(() => {
3546
3878
  if (cancelEvent) eventBus.emit(`UI:${cancelEvent}`, {});
3547
3879
  }, [cancelEvent, eventBus]);
3548
- const handlePlayAgain = React6.useCallback(() => {
3880
+ const handlePlayAgain = React8.useCallback(() => {
3549
3881
  if (playAgainEvent) eventBus.emit(`UI:${playAgainEvent}`, {});
3550
3882
  }, [playAgainEvent, eventBus]);
3551
3883
  return /* @__PURE__ */ jsxRuntime.jsxs(VStack, { className: cn("game-board-3d block w-full min-h-[85vh] relative", className), children: [
@@ -3636,24 +3968,24 @@ function GameBoard3D({
3636
3968
  GameBoard3D.displayName = "GameBoard3D";
3637
3969
  function useTapReveal(options = {}) {
3638
3970
  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);
3971
+ const [revealed, setRevealed] = React8.useState(false);
3972
+ const onRevealRef = React8.useRef(onReveal);
3973
+ const onDismissRef = React8.useRef(onDismiss);
3642
3974
  onRevealRef.current = onReveal;
3643
3975
  onDismissRef.current = onDismiss;
3644
- const reveal = React6.useCallback(() => {
3976
+ const reveal = React8.useCallback(() => {
3645
3977
  setRevealed((wasRevealed) => {
3646
3978
  if (!wasRevealed) onRevealRef.current?.();
3647
3979
  return true;
3648
3980
  });
3649
3981
  }, []);
3650
- const dismiss = React6.useCallback(() => {
3982
+ const dismiss = React8.useCallback(() => {
3651
3983
  setRevealed((wasRevealed) => {
3652
3984
  if (wasRevealed) onDismissRef.current?.();
3653
3985
  return false;
3654
3986
  });
3655
3987
  }, []);
3656
- const onPointerDown = React6.useCallback(
3988
+ const onPointerDown = React8.useCallback(
3657
3989
  (e) => {
3658
3990
  if (!enabled || e.pointerType === "mouse") return;
3659
3991
  if (revealed) dismiss();
@@ -3661,7 +3993,7 @@ function useTapReveal(options = {}) {
3661
3993
  },
3662
3994
  [enabled, revealed, reveal, dismiss]
3663
3995
  );
3664
- React6.useEffect(() => {
3996
+ React8.useEffect(() => {
3665
3997
  if (!revealed || typeof document === "undefined") return;
3666
3998
  const onDocDown = (ev) => {
3667
3999
  const target = ev.target;
@@ -3781,7 +4113,7 @@ var positionStyles = {
3781
4113
  fixed: "fixed",
3782
4114
  sticky: "sticky"
3783
4115
  };
3784
- var Box = React6__default.default.forwardRef(
4116
+ var Box = React8__namespace.default.forwardRef(
3785
4117
  ({
3786
4118
  padding,
3787
4119
  paddingX,
@@ -3813,20 +4145,20 @@ var Box = React6__default.default.forwardRef(
3813
4145
  ...rest
3814
4146
  }, ref) => {
3815
4147
  const eventBus = useEventBus();
3816
- const handleClick = React6.useCallback((e) => {
4148
+ const handleClick = React8.useCallback((e) => {
3817
4149
  if (action) {
3818
4150
  e.stopPropagation();
3819
4151
  eventBus.emit(`UI:${action}`, actionPayload ?? {});
3820
4152
  }
3821
4153
  onClick?.(e);
3822
4154
  }, [action, actionPayload, eventBus, onClick]);
3823
- const handleMouseEnter = React6.useCallback((e) => {
4155
+ const handleMouseEnter = React8.useCallback((e) => {
3824
4156
  if (hoverEvent) {
3825
4157
  eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
3826
4158
  }
3827
4159
  onMouseEnter?.(e);
3828
4160
  }, [hoverEvent, eventBus, onMouseEnter]);
3829
- const handleMouseLeave = React6.useCallback((e) => {
4161
+ const handleMouseLeave = React8.useCallback((e) => {
3830
4162
  if (hoverEvent) {
3831
4163
  eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
3832
4164
  }
@@ -3834,19 +4166,19 @@ var Box = React6__default.default.forwardRef(
3834
4166
  }, [hoverEvent, eventBus, onMouseLeave]);
3835
4167
  const { triggerProps } = useTapReveal({
3836
4168
  enabled: tapReveal && !!hoverEvent,
3837
- onReveal: React6.useCallback(() => {
4169
+ onReveal: React8.useCallback(() => {
3838
4170
  if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
3839
4171
  }, [hoverEvent, eventBus]),
3840
- onDismiss: React6.useCallback(() => {
4172
+ onDismiss: React8.useCallback(() => {
3841
4173
  if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
3842
4174
  }, [hoverEvent, eventBus])
3843
4175
  });
3844
- const handlePointerDown = React6.useCallback((e) => {
4176
+ const handlePointerDown = React8.useCallback((e) => {
3845
4177
  if (hoverEvent && tapReveal) triggerProps.onPointerDown(e);
3846
4178
  onPointerDown?.(e);
3847
4179
  }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
3848
4180
  const isClickable = action || onClick;
3849
- return React6__default.default.createElement(
4181
+ return React8__namespace.default.createElement(
3850
4182
  Component2,
3851
4183
  {
3852
4184
  ref,
@@ -5086,10 +5418,10 @@ var Avl3DOrbitalNode = ({
5086
5418
  position,
5087
5419
  onClick
5088
5420
  }) => {
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);
5421
+ const groupRef = React8.useRef(null);
5422
+ const [hovered, setHovered] = React8.useState(false);
5423
+ const scale = React8.useMemo(() => 0.6 + Math.min(traitCount, 8) * 0.3, [traitCount]);
5424
+ const currentScale = React8.useRef(scale);
5093
5425
  const targetScale = hovered ? scale * 1.06 : scale;
5094
5426
  fiber.useFrame((_, delta) => {
5095
5427
  if (!groupRef.current) return;
@@ -5231,7 +5563,7 @@ var Avl3DCrossWire = ({
5231
5563
  eventName,
5232
5564
  color = AVL_3D_COLORS.crossWire
5233
5565
  }) => {
5234
- const { tubeArgs, midpoint } = React6.useMemo(() => {
5566
+ const { tubeArgs, midpoint } = React8.useMemo(() => {
5235
5567
  const curve = arcCurve3D(from, to, 2);
5236
5568
  const mid = curve.getPoint(0.5);
5237
5569
  return {
@@ -5270,9 +5602,9 @@ var Avl3DApplicationScene = ({
5270
5602
  data,
5271
5603
  onOrbitalClick
5272
5604
  }) => {
5273
- const groupRef = React6.useRef(null);
5274
- const [, setHoverTrigger] = React6.useState(0);
5275
- const orbitalPositions = React6.useMemo(() => {
5605
+ const groupRef = React8.useRef(null);
5606
+ const [, setHoverTrigger] = React8.useState(0);
5607
+ const orbitalPositions = React8.useMemo(() => {
5276
5608
  const baseRadius = Math.max(4, data.orbitals.length * 1.5);
5277
5609
  if (data.crossLinks.length === 0) {
5278
5610
  return goldenSpiralPositions(data.orbitals.length, baseRadius);
@@ -5285,7 +5617,7 @@ var Avl3DApplicationScene = ({
5285
5617
  })).filter((e) => e.from >= 0 && e.to >= 0);
5286
5618
  return forceDirectedPositions(data.orbitals.length, edges, baseRadius);
5287
5619
  }, [data.orbitals, data.crossLinks]);
5288
- const positionByName = React6.useMemo(() => {
5620
+ const positionByName = React8.useMemo(() => {
5289
5621
  const map = /* @__PURE__ */ new Map();
5290
5622
  data.orbitals.forEach((orb, i) => {
5291
5623
  const pos = orbitalPositions[i];
@@ -5368,7 +5700,7 @@ var Avl3DEntityCore = ({
5368
5700
  position,
5369
5701
  fields
5370
5702
  }) => {
5371
- const coreRef = React6.useRef(null);
5703
+ const coreRef = React8.useRef(null);
5372
5704
  fiber.useFrame((_, delta) => {
5373
5705
  if (!coreRef.current) return;
5374
5706
  coreRef.current.rotation.y += delta * 0.3;
@@ -5521,8 +5853,8 @@ function TraitOrbit({
5521
5853
  onClick,
5522
5854
  onHover
5523
5855
  }) {
5524
- const groupRef = React6.useRef(null);
5525
- const angleRef = React6.useRef(Math.random() * Math.PI * 2);
5856
+ const groupRef = React8.useRef(null);
5857
+ const angleRef = React8.useRef(Math.random() * Math.PI * 2);
5526
5858
  fiber.useFrame((_, delta) => {
5527
5859
  angleRef.current += delta * speed;
5528
5860
  if (!groupRef.current) return;
@@ -5653,9 +5985,9 @@ var Avl3DOrbitalScene = ({
5653
5985
  highlightedTrait,
5654
5986
  onTraitHighlight
5655
5987
  }) => {
5656
- const [localHighlight, setLocalHighlight] = React6.useState(null);
5988
+ const [localHighlight, setLocalHighlight] = React8.useState(null);
5657
5989
  const activeHighlight = highlightedTrait ?? localHighlight;
5658
- const traitOrbits = React6.useMemo(() => {
5990
+ const traitOrbits = React8.useMemo(() => {
5659
5991
  return data.traits.map((trait, i) => ({
5660
5992
  name: trait.name,
5661
5993
  radius: 3 + i * 1.2,
@@ -5666,7 +5998,7 @@ var Avl3DOrbitalScene = ({
5666
5998
  transitionCount: trait.transitionCount
5667
5999
  }));
5668
6000
  }, [data.traits]);
5669
- const pagePositions = React6.useMemo(() => {
6001
+ const pagePositions = React8.useMemo(() => {
5670
6002
  const outerRadius = 3 + data.traits.length * 1.2 + 1.5;
5671
6003
  return data.pages.map((_, i) => {
5672
6004
  const angle = 2 * Math.PI * i / data.pages.length - Math.PI / 2;
@@ -5730,12 +6062,12 @@ var Avl3DStateNode = ({
5730
6062
  outgoingCount = 0,
5731
6063
  onClick
5732
6064
  }) => {
5733
- const [hovered, setHovered] = React6.useState(false);
6065
+ const [hovered, setHovered] = React8.useState(false);
5734
6066
  const baseColor = active ? AVL_3D_COLORS.stateActive : AVL_3D_COLORS.stateIdle;
5735
6067
  const emissiveColor = active ? AVL_3D_COLORS.stateActive : AVL_3D_COLORS.stateEdge;
5736
6068
  const emissiveIntensity = active ? 2 : hovered ? 0.8 : 0.3;
5737
6069
  const targetScale = hovered ? 1.08 : 1;
5738
- const currentScale = React6.useRef(1);
6070
+ const currentScale = React8.useRef(1);
5739
6071
  fiber.useFrame((_, delta) => {
5740
6072
  currentScale.current = THREE6.MathUtils.damp(currentScale.current, targetScale, 6, delta);
5741
6073
  });
@@ -5909,8 +6241,8 @@ var Avl3DTransitionArc = ({
5909
6241
  onClick,
5910
6242
  color = AVL_3D_COLORS.transitionArc
5911
6243
  }) => {
5912
- const [hovered, setHovered] = React6.useState(false);
5913
- const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } = React6.useMemo(() => {
6244
+ const [hovered, setHovered] = React8.useState(false);
6245
+ const { tubeArgs, labelPos, guardPos, arrowPos, arrowQuat, effectPositions } = React8.useMemo(() => {
5914
6246
  const offset = 1.5 + index * 0.8;
5915
6247
  const curve = isSelf ? selfLoopCurve3D(from, 1.2) : arcCurve3D(from, to, offset);
5916
6248
  const mid = curve.getPoint(0.5);
@@ -6043,7 +6375,7 @@ var Avl3DTraitScene = ({
6043
6375
  data,
6044
6376
  onTransitionClick
6045
6377
  }) => {
6046
- const statePositions = React6.useMemo(() => {
6378
+ const statePositions = React8.useMemo(() => {
6047
6379
  const radius = Math.max(3, data.states.length * 0.8);
6048
6380
  const positions = fibonacciSpherePositions(data.states.length, radius);
6049
6381
  const posMap = /* @__PURE__ */ new Map();
@@ -6053,7 +6385,7 @@ var Avl3DTraitScene = ({
6053
6385
  });
6054
6386
  return posMap;
6055
6387
  }, [data.states]);
6056
- const transitionIndices = React6.useMemo(() => {
6388
+ const transitionIndices = React8.useMemo(() => {
6057
6389
  const pairCount = /* @__PURE__ */ new Map();
6058
6390
  return data.transitions.map((t) => {
6059
6391
  const key = `${t.from}->${t.to}`;
@@ -6062,7 +6394,7 @@ var Avl3DTraitScene = ({
6062
6394
  return idx;
6063
6395
  });
6064
6396
  }, [data.transitions]);
6065
- const transitionCounts = React6.useMemo(() => {
6397
+ const transitionCounts = React8.useMemo(() => {
6066
6398
  const incoming = /* @__PURE__ */ new Map();
6067
6399
  const outgoing = /* @__PURE__ */ new Map();
6068
6400
  for (const t of data.transitions) {
@@ -6172,11 +6504,11 @@ var Avl3DExprTree = ({
6172
6504
  expression,
6173
6505
  position
6174
6506
  }) => {
6175
- const layoutResults = React6.useMemo(() => {
6507
+ const layoutResults = React8.useMemo(() => {
6176
6508
  const origin = { x: position[0], y: position[1], z: position[2] };
6177
6509
  return treeLayout3D(expression, origin, 2.5, 1.5);
6178
6510
  }, [expression, position]);
6179
- const edges = React6.useMemo(() => {
6511
+ const edges = React8.useMemo(() => {
6180
6512
  const result = [];
6181
6513
  function collectEdges(node, parentPos) {
6182
6514
  const entry = layoutResults.find((r) => r.node === node);
@@ -6456,7 +6788,7 @@ var Avl3DEffects = ({
6456
6788
  const dof = DOF_CONFIG[level];
6457
6789
  const sparkles = SPARKLE_CONFIGS[level];
6458
6790
  const stars = STARS_CONFIG[level];
6459
- const starsKey = React6.useMemo(() => `stars-${level}`, [level]);
6791
+ const starsKey = React8.useMemo(() => `stars-${level}`, [level]);
6460
6792
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
6461
6793
  /* @__PURE__ */ jsxRuntime.jsx(
6462
6794
  drei.Stars,
@@ -6520,16 +6852,16 @@ var DEFAULT_CONFIG2 = {
6520
6852
  modelOverrides: {},
6521
6853
  effectsEnabled: true
6522
6854
  };
6523
- var Avl3DContext = React6.createContext(DEFAULT_CONFIG2);
6855
+ var Avl3DContext = React8.createContext(DEFAULT_CONFIG2);
6524
6856
  function useAvl3DConfig() {
6525
- return React6.useContext(Avl3DContext);
6857
+ return React8.useContext(Avl3DContext);
6526
6858
  }
6527
6859
  function CameraController2({ targetPosition, targetLookAt, animated }) {
6528
6860
  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(() => {
6861
+ const targetPosVec = React8.useRef(new THREE6__namespace.Vector3(...targetPosition));
6862
+ const targetLookVec = React8.useRef(new THREE6__namespace.Vector3(...targetLookAt));
6863
+ const isAnimating = React8.useRef(false);
6864
+ React8.useEffect(() => {
6533
6865
  const newTarget = new THREE6__namespace.Vector3(...targetPosition);
6534
6866
  const newLookAt = new THREE6__namespace.Vector3(...targetLookAt);
6535
6867
  if (!newTarget.equals(targetPosVec.current) || !newLookAt.equals(targetLookVec.current)) {
@@ -6558,8 +6890,8 @@ function CameraController2({ targetPosition, targetLookAt, animated }) {
6558
6890
  return null;
6559
6891
  }
6560
6892
  function SceneFade({ animating, children }) {
6561
- const groupRef = React6.useRef(null);
6562
- const opacityRef = React6.useRef(1);
6893
+ const groupRef = React8.useRef(null);
6894
+ const opacityRef = React8.useRef(1);
6563
6895
  fiber.useFrame((_, delta) => {
6564
6896
  if (!groupRef.current) return;
6565
6897
  const target = animating ? 0 : 1;
@@ -6583,7 +6915,7 @@ var Avl3DViewer = ({
6583
6915
  modelOverrides = {}
6584
6916
  }) => {
6585
6917
  const { t } = hooks.useTranslate();
6586
- const schema = React6.useMemo(() => {
6918
+ const schema = React8.useMemo(() => {
6587
6919
  if (typeof schemaProp === "string") {
6588
6920
  try {
6589
6921
  return JSON.parse(schemaProp);
@@ -6593,12 +6925,12 @@ var Avl3DViewer = ({
6593
6925
  }
6594
6926
  return schemaProp;
6595
6927
  }, [schemaProp]);
6596
- const configValue = React6.useMemo(() => ({
6928
+ const configValue = React8.useMemo(() => ({
6597
6929
  modelOverrides,
6598
6930
  effectsEnabled: effects
6599
6931
  }), [modelOverrides, effects]);
6600
- const [state, dispatch] = React6.useReducer(zoomReducer, initialZoomState);
6601
- React6.useEffect(() => {
6932
+ const [state, dispatch] = React8.useReducer(zoomReducer, initialZoomState);
6933
+ React8.useEffect(() => {
6602
6934
  if (initialOrbital) {
6603
6935
  dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: initialOrbital, targetPosition: { x: 0, y: 0 } });
6604
6936
  setTimeout(() => dispatch({ type: "ANIMATION_COMPLETE" }), 0);
@@ -6610,35 +6942,35 @@ var Avl3DViewer = ({
6610
6942
  }
6611
6943
  }
6612
6944
  }, [initialOrbital, initialTrait]);
6613
- React6.useEffect(() => {
6945
+ React8.useEffect(() => {
6614
6946
  onZoomChange?.(state.level, {
6615
6947
  orbital: state.selectedOrbital ?? void 0,
6616
6948
  trait: state.selectedTrait ?? void 0
6617
6949
  });
6618
6950
  }, [state.level, state.selectedOrbital, state.selectedTrait, onZoomChange]);
6619
- React6.useEffect(() => {
6951
+ React8.useEffect(() => {
6620
6952
  if (!state.animating) return;
6621
6953
  const timer = setTimeout(() => {
6622
6954
  dispatch({ type: "ANIMATION_COMPLETE" });
6623
6955
  }, 800);
6624
6956
  return () => clearTimeout(timer);
6625
6957
  }, [state.animating]);
6626
- const handleKeyDown = React6.useCallback((e) => {
6958
+ const handleKeyDown = React8.useCallback((e) => {
6627
6959
  if (e.key === "Escape") {
6628
6960
  dispatch({ type: "ZOOM_OUT" });
6629
6961
  }
6630
6962
  }, []);
6631
- const handleOrbitalClick = React6.useCallback((name) => {
6963
+ const handleOrbitalClick = React8.useCallback((name) => {
6632
6964
  dispatch({ type: "ZOOM_INTO_ORBITAL", orbital: name, targetPosition: { x: 0, y: 0 } });
6633
6965
  }, []);
6634
- const handleTraitClick = React6.useCallback((name) => {
6966
+ const handleTraitClick = React8.useCallback((name) => {
6635
6967
  dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
6636
6968
  }, []);
6637
- const [highlightedTrait, setHighlightedTrait] = React6__default.default.useState(null);
6638
- const handleTransitionClick = React6.useCallback((index) => {
6969
+ const [highlightedTrait, setHighlightedTrait] = React8__namespace.default.useState(null);
6970
+ const handleTransitionClick = React8.useCallback((index) => {
6639
6971
  dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
6640
6972
  }, []);
6641
- const handleBreadcrumbClick = React6.useCallback((targetLevel) => {
6973
+ const handleBreadcrumbClick = React8.useCallback((targetLevel) => {
6642
6974
  const levelOrder = ["application", "orbital", "trait", "transition"];
6643
6975
  const currentIdx = levelOrder.indexOf(state.level);
6644
6976
  const targetIdx = levelOrder.indexOf(targetLevel);
@@ -6647,7 +6979,7 @@ var Avl3DViewer = ({
6647
6979
  }
6648
6980
  }, [state.level]);
6649
6981
  const cameraConfig = CAMERA_POSITIONS[state.level];
6650
- const sceneContent = React6.useMemo(() => {
6982
+ const sceneContent = React8.useMemo(() => {
6651
6983
  switch (state.level) {
6652
6984
  case "application": {
6653
6985
  const data = parseApplicationLevel(schema);
@@ -6721,7 +7053,7 @@ var Avl3DViewer = ({
6721
7053
  gap: "xs",
6722
7054
  align: "center",
6723
7055
  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: [
7056
+ children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxRuntime.jsxs(React8__namespace.default.Fragment, { children: [
6725
7057
  i > 0 && /* @__PURE__ */ jsxRuntime.jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
6726
7058
  i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsxRuntime.jsx(
6727
7059
  Box,
@@ -7008,7 +7340,6 @@ exports.Avl3DTransitionScene = Avl3DTransitionScene;
7008
7340
  exports.Avl3DViewer = Avl3DViewer;
7009
7341
  exports.CAMERA_POSITIONS = CAMERA_POSITIONS;
7010
7342
  exports.Camera3D = Camera3D;
7011
- exports.Canvas3D = Canvas3D;
7012
7343
  exports.Canvas3DErrorBoundary = Canvas3DErrorBoundary;
7013
7344
  exports.Canvas3DLoadingState = Canvas3DLoadingState;
7014
7345
  exports.GameBoard3D = GameBoard3D;