@almadar/ui 5.94.1 → 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,4 +1,5 @@
1
- import React6, { forwardRef, useRef, useEffect, useImperativeHandle, useState, useMemo, useCallback, createContext, Component, useContext, useSyncExternalStore, useReducer } from 'react';
1
+ import * as React8 from 'react';
2
+ import React8__default, { forwardRef, useRef, useEffect, useImperativeHandle, useState, useMemo, useCallback, createContext, useContext, Component, useSyncExternalStore, useReducer } from 'react';
2
3
  import { useThree, useFrame, Canvas, useLoader } from '@react-three/fiber';
3
4
  import * as THREE6 from 'three';
4
5
  import { Vector3, QuadraticBezierCurve3, MathUtils, Quaternion } from 'three';
@@ -316,12 +317,13 @@ function detectAssetRoot(modelUrl) {
316
317
  function useGLTFModel(url, resourceBasePath) {
317
318
  const [state, setState] = useState({
318
319
  model: null,
320
+ clips: [],
319
321
  isLoading: false,
320
322
  error: null
321
323
  });
322
324
  useEffect(() => {
323
325
  if (!url) {
324
- setState({ model: null, isLoading: false, error: null });
326
+ setState({ model: null, clips: [], isLoading: false, error: null });
325
327
  return;
326
328
  }
327
329
  log2.debug("Loading", { url });
@@ -332,9 +334,10 @@ function useGLTFModel(url, resourceBasePath) {
332
334
  loader.load(
333
335
  url,
334
336
  (gltf) => {
335
- log2.debug("Loaded", { url });
337
+ log2.debug("Loaded", { url, clips: gltf.animations.length });
336
338
  setState({
337
339
  model: gltf.scene,
340
+ clips: gltf.animations,
338
341
  isLoading: false,
339
342
  error: null
340
343
  });
@@ -344,6 +347,7 @@ function useGLTFModel(url, resourceBasePath) {
344
347
  log2.warn("Failed", { url, error: err instanceof Error ? err : String(err) });
345
348
  setState({
346
349
  model: null,
350
+ clips: [],
347
351
  isLoading: false,
348
352
  error: err instanceof Error ? err : new Error(String(err))
349
353
  });
@@ -364,9 +368,10 @@ function ModelLoader({
364
368
  fallbackGeometry = "box",
365
369
  castShadow = true,
366
370
  receiveShadow = true,
367
- resourceBasePath
371
+ resourceBasePath,
372
+ animation
368
373
  }) {
369
- const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
374
+ const { model: loadedModel, clips, isLoading, error } = useGLTFModel(url, resourceBasePath);
370
375
  const model = useMemo(() => {
371
376
  if (!loadedModel) return null;
372
377
  const cloned = clone(loadedModel);
@@ -379,6 +384,21 @@ function ModelLoader({
379
384
  });
380
385
  return cloned;
381
386
  }, [loadedModel, castShadow, receiveShadow]);
387
+ const mixer = useMemo(() => model ? new THREE6.AnimationMixer(model) : null, [model]);
388
+ useEffect(() => {
389
+ if (!mixer || !animation || clips.length === 0) return;
390
+ const wanted = animation.toLowerCase();
391
+ const clip = clips.find((c) => c.name === animation) ?? clips.find((c) => c.name.toLowerCase() === wanted);
392
+ if (!clip) return;
393
+ const action = mixer.clipAction(clip);
394
+ action.reset().fadeIn(0.15).play();
395
+ return () => {
396
+ action.fadeOut(0.15);
397
+ };
398
+ }, [mixer, clips, animation]);
399
+ useFrame((_, delta) => {
400
+ mixer?.update(delta);
401
+ });
382
402
  const normFactor = useMemo(() => {
383
403
  if (!model) return 1;
384
404
  const box = new THREE6.Box3().setFromObject(model);
@@ -1695,6 +1715,99 @@ function UnitSpriteBillboard({
1695
1715
  )
1696
1716
  ] });
1697
1717
  }
1718
+ function FollowCamera({
1719
+ target,
1720
+ offset
1721
+ }) {
1722
+ const { camera } = useThree();
1723
+ const look = useRef(new THREE6.Vector3(target[0], target[1], target[2]));
1724
+ const goal = useRef(new THREE6.Vector3());
1725
+ useFrame((_, delta) => {
1726
+ const t = Math.min(1, delta * 5);
1727
+ goal.current.set(target[0] + offset[0], target[1] + offset[1], target[2] + offset[2]);
1728
+ camera.position.lerp(goal.current, t);
1729
+ look.current.lerp(goal.current.set(target[0], target[1], target[2]), t);
1730
+ camera.lookAt(look.current);
1731
+ });
1732
+ return null;
1733
+ }
1734
+ function LerpedGroup({
1735
+ target,
1736
+ enabled,
1737
+ children
1738
+ }) {
1739
+ const ref = useRef(null);
1740
+ const goal = useRef(new THREE6.Vector3());
1741
+ useFrame((_, delta) => {
1742
+ const g = ref.current;
1743
+ if (!g) return;
1744
+ goal.current.set(target[0], target[1], target[2]);
1745
+ if (enabled) g.position.lerp(goal.current, Math.min(1, delta * 10));
1746
+ else g.position.copy(goal.current);
1747
+ });
1748
+ return /* @__PURE__ */ jsx("group", { ref, position: target, children });
1749
+ }
1750
+ var SIDE_PLATFORM_COLORS = {
1751
+ ground: "#5b8c3e",
1752
+ platform: "#8b5a2b",
1753
+ hazard: "#cc4444",
1754
+ goal: "#4488cc"
1755
+ };
1756
+ function SideScene({
1757
+ player,
1758
+ platforms,
1759
+ worldHeight,
1760
+ ppu,
1761
+ playerSprite,
1762
+ tileSprites,
1763
+ interpolate
1764
+ }) {
1765
+ const pw = player.width ?? 32;
1766
+ const ph = player.height ?? 48;
1767
+ const playerPos = [
1768
+ (player.x + pw / 2) / ppu,
1769
+ (worldHeight - player.y - ph) / ppu,
1770
+ 0
1771
+ ];
1772
+ const playerH = Math.max(ph / ppu, 0.5);
1773
+ return /* @__PURE__ */ jsxs("group", { children: [
1774
+ platforms.map((p, i) => {
1775
+ const platformType = p.type ?? "platform";
1776
+ const model = tileSprites?.[platformType]?.url;
1777
+ const topY = (worldHeight - p.y) / ppu;
1778
+ if (model) {
1779
+ const cells = Math.max(1, Math.round(p.width / ppu));
1780
+ const x0 = p.x / ppu;
1781
+ return /* @__PURE__ */ jsx("group", { children: Array.from({ length: cells }, (_, c) => /* @__PURE__ */ jsx("group", { position: [x0 + c + 0.5, topY - 0.475, 0], children: /* @__PURE__ */ jsx(ModelLoader, { url: model, scale: 0.95, fallbackGeometry: "box", castShadow: true, receiveShadow: true }) }, c)) }, `plat-${i}`);
1782
+ }
1783
+ return /* @__PURE__ */ jsxs(
1784
+ "mesh",
1785
+ {
1786
+ position: [(p.x + p.width / 2) / ppu, topY - p.height / 2 / ppu, 0],
1787
+ children: [
1788
+ /* @__PURE__ */ jsx("boxGeometry", { args: [p.width / ppu, p.height / ppu, 1] }),
1789
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: SIDE_PLATFORM_COLORS[platformType] ?? "#888888" })
1790
+ ]
1791
+ },
1792
+ `plat-${i}`
1793
+ );
1794
+ }),
1795
+ /* @__PURE__ */ jsx(LerpedGroup, { target: playerPos, enabled: interpolate, children: playerSprite?.url ? /* @__PURE__ */ jsx("group", { position: [0, playerH / 2, 0], children: /* @__PURE__ */ jsx(
1796
+ ModelLoader,
1797
+ {
1798
+ url: playerSprite.url,
1799
+ scale: playerH,
1800
+ rotation: [0, player.facingRight ? 90 : -90, 0],
1801
+ animation: player.animation ?? "idle",
1802
+ fallbackGeometry: "box",
1803
+ castShadow: true
1804
+ }
1805
+ ) }) : /* @__PURE__ */ jsxs("mesh", { position: [0, playerH / 2, 0], children: [
1806
+ /* @__PURE__ */ jsx("capsuleGeometry", { args: [playerH * 0.25, playerH * 0.5, 4, 8] }),
1807
+ /* @__PURE__ */ jsx("meshStandardMaterial", { color: "#4488ff" })
1808
+ ] }) })
1809
+ ] });
1810
+ }
1698
1811
  var GameCanvas3D = forwardRef(
1699
1812
  ({
1700
1813
  tiles = [],
@@ -1738,12 +1851,47 @@ var GameCanvas3D = forwardRef(
1738
1851
  selectedTileIds = [],
1739
1852
  selectedUnitId = null,
1740
1853
  unitScale = 1,
1854
+ keyMap,
1855
+ keyUpMap,
1856
+ player,
1857
+ platforms = [],
1858
+ worldHeight = 400,
1859
+ pixelsPerUnit = 32,
1860
+ playerSprite,
1861
+ tileSprites,
1862
+ assetManifest,
1863
+ interpolateUnits = false,
1741
1864
  children
1742
1865
  }, ref) => {
1743
1866
  const containerRef = useRef(null);
1744
1867
  const controlsRef = useRef(null);
1745
1868
  const [hoveredTile, setHoveredTile] = useState(null);
1746
1869
  const [internalError, setInternalError] = useState(null);
1870
+ const eventBus = useEventBus();
1871
+ const keysRef = useRef(/* @__PURE__ */ new Set());
1872
+ useEffect(() => {
1873
+ if (!keyMap && !keyUpMap) return;
1874
+ const down = (e) => {
1875
+ if (keysRef.current.has(e.code)) return;
1876
+ keysRef.current.add(e.code);
1877
+ const ev = keyMap?.[e.code];
1878
+ if (ev) {
1879
+ eventBus.emit(`UI:${ev}`, {});
1880
+ e.preventDefault();
1881
+ }
1882
+ };
1883
+ const up = (e) => {
1884
+ keysRef.current.delete(e.code);
1885
+ const ev = keyUpMap?.[e.code];
1886
+ if (ev) eventBus.emit(`UI:${ev}`, {});
1887
+ };
1888
+ window.addEventListener("keydown", down);
1889
+ window.addEventListener("keyup", up);
1890
+ return () => {
1891
+ window.removeEventListener("keydown", down);
1892
+ window.removeEventListener("keyup", up);
1893
+ };
1894
+ }, [keyMap, keyUpMap, eventBus]);
1747
1895
  useEffect(() => {
1748
1896
  const el = containerRef.current;
1749
1897
  if (!el) return;
@@ -1913,6 +2061,11 @@ var GameCanvas3D = forwardRef(
1913
2061
  position: [cx, d * 2, cz],
1914
2062
  fov: 45
1915
2063
  };
2064
+ case "follow":
2065
+ return {
2066
+ position: [cx, d * 0.5, cz + d],
2067
+ fov: 45
2068
+ };
1916
2069
  case "perspective":
1917
2070
  default:
1918
2071
  return {
@@ -1921,6 +2074,26 @@ var GameCanvas3D = forwardRef(
1921
2074
  };
1922
2075
  }
1923
2076
  }, [cameraMode, gridBounds]);
2077
+ const followTarget = useMemo(() => {
2078
+ if (player) {
2079
+ return [
2080
+ (player.x + (player.width ?? 32) / 2) / pixelsPerUnit,
2081
+ (worldHeight - player.y - (player.height ?? 48) / 2) / pixelsPerUnit,
2082
+ 0
2083
+ ];
2084
+ }
2085
+ const selected = units.find((u) => u.id === selectedUnitId);
2086
+ if (selected) {
2087
+ const sx = selected.x ?? selected.position?.x ?? 0;
2088
+ const sz = selected.z ?? selected.y ?? selected.position?.y ?? 0;
2089
+ return [
2090
+ (sx - gridBounds.minX) * gridConfig.cellSize,
2091
+ 0,
2092
+ (sz - gridBounds.minZ) * gridConfig.cellSize
2093
+ ];
2094
+ }
2095
+ return cameraTarget;
2096
+ }, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
1924
2097
  const DefaultTileRenderer = useCallback(
1925
2098
  ({ tile, position }) => {
1926
2099
  const isSelected = tile.id ? selectedTileIds.includes(tile.id) : false;
@@ -1942,7 +2115,8 @@ var GameCanvas3D = forwardRef(
1942
2115
  else if (isAttackTarget) emissive = 4456448;
1943
2116
  else if (isValidMove) emissive = 17408;
1944
2117
  else if (isHovered) emissive = 2236962;
1945
- if (tile.modelUrl) {
2118
+ const tileModel = tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""];
2119
+ if (tileModel?.url) {
1946
2120
  return /* @__PURE__ */ jsx(
1947
2121
  "group",
1948
2122
  {
@@ -1954,7 +2128,7 @@ var GameCanvas3D = forwardRef(
1954
2128
  children: /* @__PURE__ */ jsx(
1955
2129
  ModelLoader,
1956
2130
  {
1957
- url: tile.modelUrl.url,
2131
+ url: tileModel.url,
1958
2132
  scale: 0.95,
1959
2133
  fallbackGeometry: "box",
1960
2134
  castShadow: true,
@@ -1979,7 +2153,7 @@ var GameCanvas3D = forwardRef(
1979
2153
  }
1980
2154
  );
1981
2155
  },
1982
- [selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover]
2156
+ [selectedTileIds, hoveredTile, validMoves, attackTargets, handleTileClick, handleTileHover, assetManifest]
1983
2157
  );
1984
2158
  const UNIT_BASE_MODEL_SCALE = 0.5;
1985
2159
  const UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
@@ -1990,6 +2164,7 @@ var GameCanvas3D = forwardRef(
1990
2164
  const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
1991
2165
  const hasAtlas = unitAtlasUrl(unit) !== null;
1992
2166
  const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
2167
+ const unitModel = unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""];
1993
2168
  const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * gridConfig.cellSize;
1994
2169
  const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * gridConfig.cellSize;
1995
2170
  const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * gridConfig.cellSize;
@@ -2014,13 +2189,14 @@ var GameCanvas3D = forwardRef(
2014
2189
  height: billboardHeight
2015
2190
  }
2016
2191
  ) })
2017
- ) : unit.modelUrl ? (
2018
- /* GLB unit model (box fallback while loading / on error) */
2192
+ ) : unitModel?.url ? (
2193
+ /* GLB unit model LOLO's `animation` field drives the named clip */
2019
2194
  /* @__PURE__ */ jsx(
2020
2195
  ModelLoader,
2021
2196
  {
2022
- url: unit.modelUrl.url,
2197
+ url: unitModel.url,
2023
2198
  scale: modelScale,
2199
+ animation: unit.animation ?? "idle",
2024
2200
  fallbackGeometry: "box",
2025
2201
  castShadow: true
2026
2202
  }
@@ -2068,18 +2244,19 @@ var GameCanvas3D = forwardRef(
2068
2244
  }
2069
2245
  );
2070
2246
  },
2071
- [selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig]
2247
+ [selectedUnitId, handleUnitClick, resolveUnitFrame, unitScale, gridConfig, assetManifest]
2072
2248
  );
2073
2249
  const DefaultFeatureRenderer = useCallback(
2074
2250
  ({
2075
2251
  feature,
2076
2252
  position
2077
2253
  }) => {
2078
- if (feature.assetUrl) {
2254
+ const featureModel = feature.assetUrl ?? assetManifest?.features?.[feature.type];
2255
+ if (featureModel?.url) {
2079
2256
  return /* @__PURE__ */ jsx(
2080
2257
  ModelLoader,
2081
2258
  {
2082
- url: feature.assetUrl.url,
2259
+ url: featureModel.url,
2083
2260
  position,
2084
2261
  scale: 0.5,
2085
2262
  rotation: [0, feature.rotation ?? 0, 0],
@@ -2125,7 +2302,7 @@ var GameCanvas3D = forwardRef(
2125
2302
  }
2126
2303
  return null;
2127
2304
  },
2128
- [handleFeatureClick]
2305
+ [handleFeatureClick, assetManifest]
2129
2306
  );
2130
2307
  if (externalLoading || assetsLoading && preloadAssets.length > 0) {
2131
2308
  return /* @__PURE__ */ jsx(
@@ -2179,6 +2356,13 @@ var GameCanvas3D = forwardRef(
2179
2356
  },
2180
2357
  children: [
2181
2358
  /* @__PURE__ */ jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
2359
+ cameraMode === "follow" && /* @__PURE__ */ jsx(
2360
+ FollowCamera,
2361
+ {
2362
+ target: followTarget,
2363
+ offset: player ? [0, 2, 9] : [5, 7, 5]
2364
+ }
2365
+ ),
2182
2366
  /* @__PURE__ */ jsx("ambientLight", { intensity: 0.6 }),
2183
2367
  /* @__PURE__ */ jsx(
2184
2368
  "directionalLight",
@@ -2186,7 +2370,9 @@ var GameCanvas3D = forwardRef(
2186
2370
  position: [10, 20, 10],
2187
2371
  intensity: 0.8,
2188
2372
  castShadow: shadows,
2189
- "shadow-mapSize": [2048, 2048]
2373
+ "shadow-mapSize": [2048, 2048],
2374
+ "shadow-bias": -4e-4,
2375
+ "shadow-normalBias": 0.04
2190
2376
  }
2191
2377
  ),
2192
2378
  /* @__PURE__ */ jsx("hemisphereLight", { intensity: 0.3, color: "#87ceeb", groundColor: "#362d1d" }),
@@ -2212,7 +2398,21 @@ var GameCanvas3D = forwardRef(
2212
2398
  fadeStrength: 1
2213
2399
  }
2214
2400
  ),
2215
- /* @__PURE__ */ jsxs("group", { children: [
2401
+ player ? (
2402
+ /* Side-scroller mode — LOLO-owned pixel space mapped to world units */
2403
+ /* @__PURE__ */ jsx(
2404
+ SideScene,
2405
+ {
2406
+ player,
2407
+ platforms,
2408
+ worldHeight,
2409
+ ppu: pixelsPerUnit,
2410
+ playerSprite,
2411
+ tileSprites,
2412
+ interpolate: interpolateUnits
2413
+ }
2414
+ )
2415
+ ) : /* @__PURE__ */ jsxs("group", { children: [
2216
2416
  tiles.map((tile, index) => {
2217
2417
  const position = gridToWorld2(
2218
2418
  tile.x,
@@ -2233,12 +2433,12 @@ var GameCanvas3D = forwardRef(
2233
2433
  }),
2234
2434
  units.map((unit) => {
2235
2435
  const position = gridToWorld2(
2236
- unit.x ?? 0,
2237
- unit.z ?? unit.y ?? 0,
2436
+ unit.x ?? unit.position?.x ?? 0,
2437
+ unit.z ?? unit.y ?? unit.position?.y ?? 0,
2238
2438
  (unit.elevation ?? 0) + 0.5
2239
2439
  );
2240
2440
  const Renderer = CustomUnitRenderer || DefaultUnitRenderer;
2241
- return /* @__PURE__ */ jsx(Renderer, { unit, position }, unit.id);
2441
+ return /* @__PURE__ */ jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: /* @__PURE__ */ jsx(Renderer, { unit, position: [0, 0, 0] }) }, unit.id);
2242
2442
  })
2243
2443
  ] }),
2244
2444
  children,
@@ -2246,6 +2446,7 @@ var GameCanvas3D = forwardRef(
2246
2446
  OrbitControls,
2247
2447
  {
2248
2448
  ref: controlsRef,
2449
+ enabled: cameraMode !== "follow",
2249
2450
  target: cameraTarget,
2250
2451
  enableDamping: true,
2251
2452
  dampingFactor: 0.05,
@@ -2278,9 +2479,6 @@ var GameCanvas3D = forwardRef(
2278
2479
  }
2279
2480
  );
2280
2481
  GameCanvas3D.displayName = "GameCanvas3D";
2281
- function Canvas3D(props) {
2282
- return /* @__PURE__ */ jsx(GameCanvas3D, { ...props });
2283
- }
2284
2482
  var DEFAULT_FAMILY = "lucide";
2285
2483
  var VALID_FAMILIES = [
2286
2484
  "lucide",
@@ -2353,7 +2551,7 @@ function loadLib(key, importer) {
2353
2551
  return p;
2354
2552
  }
2355
2553
  function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
2356
- const Lazy = React6.lazy(async () => {
2554
+ const Lazy = React8__default.lazy(async () => {
2357
2555
  const lib = await loadLib(libKey, importer);
2358
2556
  const Comp = pick(lib);
2359
2557
  if (!Comp) {
@@ -2363,7 +2561,7 @@ function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
2363
2561
  return { default: Comp };
2364
2562
  });
2365
2563
  const Wrapped = (props) => /* @__PURE__ */ jsx(
2366
- React6.Suspense,
2564
+ React8__default.Suspense,
2367
2565
  {
2368
2566
  fallback: /* @__PURE__ */ jsx(
2369
2567
  "span",
@@ -3032,7 +3230,7 @@ var Icon = ({
3032
3230
  const directIcon = typeof icon === "string" ? void 0 : icon;
3033
3231
  const effectiveName = typeof icon === "string" ? icon : name;
3034
3232
  const family = useIconFamily();
3035
- const RenderedComponent = React6.useMemo(() => {
3233
+ const RenderedComponent = React8__default.useMemo(() => {
3036
3234
  if (directIcon) return null;
3037
3235
  return effectiveName ? resolveIconForFamily(effectiveName, family) : null;
3038
3236
  }, [directIcon, effectiveName, family]);
@@ -3080,6 +3278,143 @@ var Icon = ({
3080
3278
  );
3081
3279
  };
3082
3280
  Icon.displayName = "Icon";
3281
+
3282
+ // lib/atlasSlice.ts
3283
+ var atlasCache = /* @__PURE__ */ new Map();
3284
+ function isTilesheet(a) {
3285
+ return typeof a.tileWidth === "number";
3286
+ }
3287
+ function getAtlas(url, onReady) {
3288
+ if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3289
+ atlasCache.set(url, void 0);
3290
+ fetch(url).then((r) => r.json()).then((json) => {
3291
+ atlasCache.set(url, json);
3292
+ onReady();
3293
+ }).catch(() => {
3294
+ atlasCache.set(url, null);
3295
+ });
3296
+ return void 0;
3297
+ }
3298
+ function subRectFor(atlas, sprite) {
3299
+ if (isTilesheet(atlas)) {
3300
+ let col;
3301
+ let row;
3302
+ if (sprite.includes(",")) {
3303
+ const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
3304
+ col = c;
3305
+ row = r;
3306
+ } else {
3307
+ const i = Number(sprite);
3308
+ if (!Number.isFinite(i)) return null;
3309
+ col = i % atlas.columns;
3310
+ row = Math.floor(i / atlas.columns);
3311
+ }
3312
+ if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
3313
+ const margin = atlas.margin ?? 0;
3314
+ const spacing = atlas.spacing ?? 0;
3315
+ return {
3316
+ sx: margin + col * (atlas.tileWidth + spacing),
3317
+ sy: margin + row * (atlas.tileHeight + spacing),
3318
+ sw: atlas.tileWidth,
3319
+ sh: atlas.tileHeight
3320
+ };
3321
+ }
3322
+ const st = atlas.subTextures[sprite];
3323
+ if (!st) return null;
3324
+ return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
3325
+ }
3326
+ function isAtlasAsset(asset) {
3327
+ return !!asset && typeof asset.atlas === "string" && typeof asset.sprite === "string";
3328
+ }
3329
+ var imageCache = /* @__PURE__ */ new Map();
3330
+ var imageWaiters = /* @__PURE__ */ new Map();
3331
+ function getSheetImage(url, onReady) {
3332
+ const cached = imageCache.get(url);
3333
+ if (cached) return cached;
3334
+ (imageWaiters.get(url) ?? imageWaiters.set(url, /* @__PURE__ */ new Set()).get(url)).add(onReady);
3335
+ if (!imageCache.has(url)) {
3336
+ imageCache.set(url, null);
3337
+ const img = new Image();
3338
+ img.crossOrigin = "anonymous";
3339
+ img.onload = () => {
3340
+ imageCache.set(url, img);
3341
+ imageWaiters.get(url)?.forEach((fn) => fn());
3342
+ imageWaiters.delete(url);
3343
+ };
3344
+ img.src = url;
3345
+ }
3346
+ return null;
3347
+ }
3348
+ function AtlasImage({
3349
+ asset,
3350
+ size,
3351
+ width,
3352
+ height,
3353
+ fill = false,
3354
+ fit = "contain",
3355
+ alt,
3356
+ className,
3357
+ style,
3358
+ "aria-hidden": ariaHidden
3359
+ }) {
3360
+ const [, bump] = React8.useReducer((x) => x + 1, 0);
3361
+ const canvasRef = React8.useRef(null);
3362
+ const sliced = isAtlasAsset(asset);
3363
+ const atlas = sliced ? getAtlas(asset.atlas, bump) : void 0;
3364
+ const img = sliced && asset?.url ? getSheetImage(asset.url, bump) : null;
3365
+ const rect = sliced && atlas ? subRectFor(atlas, asset.sprite) : null;
3366
+ React8.useEffect(() => {
3367
+ const canvas = canvasRef.current;
3368
+ if (!canvas || !img || !rect) return;
3369
+ canvas.width = rect.sw;
3370
+ canvas.height = rect.sh;
3371
+ const ctx = canvas.getContext("2d");
3372
+ if (!ctx) return;
3373
+ ctx.imageSmoothingEnabled = false;
3374
+ ctx.clearRect(0, 0, rect.sw, rect.sh);
3375
+ ctx.drawImage(img, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
3376
+ }, [img, rect?.sx, rect?.sy, rect?.sw, rect?.sh]);
3377
+ const w = fill ? "100%" : width ?? size;
3378
+ const h = fill ? "100%" : height ?? size;
3379
+ const boxStyle = {
3380
+ ...fill ? { position: "absolute", inset: 0 } : {},
3381
+ width: w,
3382
+ height: h,
3383
+ objectFit: fit,
3384
+ imageRendering: "pixelated",
3385
+ ...style
3386
+ };
3387
+ if (!asset?.url) return null;
3388
+ if (sliced) {
3389
+ if (!rect || !img) {
3390
+ return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("inline-block flex-shrink-0", className), style: { ...boxStyle, objectFit: void 0 } });
3391
+ }
3392
+ return /* @__PURE__ */ jsx(
3393
+ "canvas",
3394
+ {
3395
+ ref: canvasRef,
3396
+ role: ariaHidden ? void 0 : "img",
3397
+ "aria-hidden": ariaHidden,
3398
+ "aria-label": ariaHidden ? void 0 : alt ?? asset.name ?? asset.category ?? "",
3399
+ className: cn("flex-shrink-0", className),
3400
+ style: boxStyle
3401
+ }
3402
+ );
3403
+ }
3404
+ return /* @__PURE__ */ jsx(
3405
+ "img",
3406
+ {
3407
+ src: asset.url,
3408
+ alt: alt ?? asset.name ?? asset.category ?? "",
3409
+ "aria-hidden": ariaHidden,
3410
+ ...typeof w === "number" ? { width: w } : {},
3411
+ ...typeof h === "number" ? { height: h } : {},
3412
+ className: cn("flex-shrink-0", className),
3413
+ style: boxStyle
3414
+ }
3415
+ );
3416
+ }
3417
+ AtlasImage.displayName = "AtlasImage";
3083
3418
  var variantStyles = {
3084
3419
  primary: [
3085
3420
  "bg-primary text-primary-foreground",
@@ -3152,7 +3487,7 @@ function resolveIconProp(value, sizeClass) {
3152
3487
  const IconComp = value;
3153
3488
  return /* @__PURE__ */ jsx(IconComp, { className: sizeClass });
3154
3489
  }
3155
- if (React6.isValidElement(value)) {
3490
+ if (React8__default.isValidElement(value)) {
3156
3491
  return value;
3157
3492
  }
3158
3493
  if (typeof value === "object" && value !== null && isIconLike(value)) {
@@ -3161,7 +3496,7 @@ function resolveIconProp(value, sizeClass) {
3161
3496
  }
3162
3497
  return value;
3163
3498
  }
3164
- var Button = React6.forwardRef(
3499
+ var Button = React8__default.forwardRef(
3165
3500
  ({
3166
3501
  className,
3167
3502
  variant = "primary",
@@ -3185,7 +3520,7 @@ var Button = React6.forwardRef(
3185
3520
  const leftIconValue = leftIcon || iconProp;
3186
3521
  const rightIconValue = rightIcon || iconRightProp;
3187
3522
  const px = size === "sm" ? 16 : size === "lg" ? 20 : 16;
3188
- const resolvedLeftIcon = iconAsset?.url ? /* @__PURE__ */ 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]);
3523
+ const resolvedLeftIcon = iconAsset?.url ? /* @__PURE__ */ jsx(AtlasImage, { asset: iconAsset, size: px, alt: iconAsset.name ?? iconAsset.category ?? "", className: "flex-shrink-0" }) : resolveIconProp(leftIconValue, iconSizeStyles[size]);
3189
3524
  const resolvedRightIcon = resolveIconProp(rightIconValue, iconSizeStyles[size]);
3190
3525
  const handleClick = (e) => {
3191
3526
  if (action) {
@@ -3309,7 +3644,7 @@ var Typography = ({
3309
3644
  }) => {
3310
3645
  const variant = variantProp ?? (level ? `h${level}` : "body1");
3311
3646
  const Component2 = as || defaultElements[variant];
3312
- return React6.createElement(
3647
+ return React8__default.createElement(
3313
3648
  Component2,
3314
3649
  {
3315
3650
  id,
@@ -3382,7 +3717,7 @@ var Stack = ({
3382
3717
  };
3383
3718
  const isHorizontal = direction === "horizontal";
3384
3719
  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";
3385
- return React6.createElement(
3720
+ return React8__default.createElement(
3386
3721
  Component2,
3387
3722
  {
3388
3723
  className: cn(
@@ -3757,7 +4092,7 @@ var positionStyles = {
3757
4092
  fixed: "fixed",
3758
4093
  sticky: "sticky"
3759
4094
  };
3760
- var Box = React6.forwardRef(
4095
+ var Box = React8__default.forwardRef(
3761
4096
  ({
3762
4097
  padding,
3763
4098
  paddingX,
@@ -3822,7 +4157,7 @@ var Box = React6.forwardRef(
3822
4157
  onPointerDown?.(e);
3823
4158
  }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
3824
4159
  const isClickable = action || onClick;
3825
- return React6.createElement(
4160
+ return React8__default.createElement(
3826
4161
  Component2,
3827
4162
  {
3828
4163
  ref,
@@ -6610,7 +6945,7 @@ var Avl3DViewer = ({
6610
6945
  const handleTraitClick = useCallback((name) => {
6611
6946
  dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
6612
6947
  }, []);
6613
- const [highlightedTrait, setHighlightedTrait] = React6.useState(null);
6948
+ const [highlightedTrait, setHighlightedTrait] = React8__default.useState(null);
6614
6949
  const handleTransitionClick = useCallback((index) => {
6615
6950
  dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
6616
6951
  }, []);
@@ -6697,7 +7032,7 @@ var Avl3DViewer = ({
6697
7032
  gap: "xs",
6698
7033
  align: "center",
6699
7034
  className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
6700
- children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React6.Fragment, { children: [
7035
+ children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React8__default.Fragment, { children: [
6701
7036
  i > 0 && /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
6702
7037
  i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsx(
6703
7038
  Box,
@@ -6973,4 +7308,4 @@ var SpatialHashGrid = class {
6973
7308
  }
6974
7309
  };
6975
7310
 
6976
- export { AVL_3D_COLORS, AssetLoader, Avl3DApplicationScene, Avl3DContext, Avl3DEffects, Avl3DOrbitalScene, Avl3DTraitScene, Avl3DTransitionScene, Avl3DViewer, CAMERA_POSITIONS, Camera3D, Canvas3D, Canvas3DErrorBoundary, Canvas3DLoadingState, GameBoard3D, GameCanvas3D, GameCanvas3DBattleTemplate, GameCanvas3DCastleTemplate, GameCanvas3DWorldMapTemplate, Lighting3D, ModelLoader, Scene3D, SpatialHashGrid, arcCurve3D, assetLoader, calculateLODLevel, createGridHighlight, cullInstancedMesh, fibonacciSpherePositions, filterByFrustum, getCellsInRadius, getNeighbors, getVisibleIndices, goldenSpiralPositions, gridDistance, gridManhattanDistance, gridToWorld, isInBounds, isInFrustum, normalizeMouseCoordinates, orbitRingPositions, raycastToObjects, raycastToPlane, selfLoopCurve3D, treeLayout3D, updateInstanceLOD, useAssetLoader, useAvl3DConfig, useGameCanvas3DEvents, useRaycaster, useSceneGraph, useThree3 as useThree, worldToGrid };
7311
+ export { AVL_3D_COLORS, AssetLoader, Avl3DApplicationScene, Avl3DContext, Avl3DEffects, Avl3DOrbitalScene, Avl3DTraitScene, Avl3DTransitionScene, Avl3DViewer, CAMERA_POSITIONS, Camera3D, Canvas3DErrorBoundary, Canvas3DLoadingState, GameBoard3D, GameCanvas3D, GameCanvas3DBattleTemplate, GameCanvas3DCastleTemplate, GameCanvas3DWorldMapTemplate, Lighting3D, ModelLoader, Scene3D, SpatialHashGrid, arcCurve3D, assetLoader, calculateLODLevel, createGridHighlight, cullInstancedMesh, fibonacciSpherePositions, filterByFrustum, getCellsInRadius, getNeighbors, getVisibleIndices, goldenSpiralPositions, gridDistance, gridManhattanDistance, gridToWorld, isInBounds, isInFrustum, normalizeMouseCoordinates, orbitRingPositions, raycastToObjects, raycastToPlane, selfLoopCurve3D, treeLayout3D, updateInstanceLOD, useAssetLoader, useAvl3DConfig, useGameCanvas3DEvents, useRaycaster, useSceneGraph, useThree3 as useThree, worldToGrid };