@almadar/ui 5.104.0 → 5.107.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.
Files changed (135) hide show
  1. package/dist/avl/index.cjs +1911 -4127
  2. package/dist/avl/index.js +1006 -3222
  3. package/dist/components/core/atoms/index.d.ts +1 -1
  4. package/dist/components/core/molecules/index.d.ts +1 -1
  5. package/dist/components/core/organisms/index.d.ts +1 -1
  6. package/dist/components/core/templates/index.d.ts +2 -2
  7. package/dist/components/game/{2d/atoms → atoms}/ActionButton.d.ts +1 -1
  8. package/dist/components/game/{2d/atoms → atoms}/ChoiceButton.d.ts +1 -1
  9. package/dist/components/game/{2d/atoms → atoms}/ControlButton.d.ts +1 -1
  10. package/dist/components/game/atoms/DrawShape.d.ts +50 -0
  11. package/dist/components/game/atoms/DrawSprite.d.ts +47 -0
  12. package/dist/components/game/atoms/DrawText.d.ts +31 -0
  13. package/dist/components/game/{2d/atoms → atoms}/GameAudioToggle.d.ts +1 -1
  14. package/dist/components/game/{2d/atoms → atoms}/GameIcon.d.ts +1 -1
  15. package/dist/components/game/{2d/atoms → atoms}/ItemSlot.d.ts +1 -1
  16. package/dist/components/game/{2d/atoms → atoms}/MiniMap.d.ts +1 -1
  17. package/dist/components/game/{2d/atoms → atoms}/ResourceCounter.d.ts +2 -2
  18. package/dist/components/game/{2d/atoms → atoms}/ScoreDisplay.d.ts +1 -1
  19. package/dist/components/game/{2d/atoms → atoms}/Sprite.d.ts +1 -1
  20. package/dist/components/game/{2d/atoms → atoms}/StateIndicator.d.ts +1 -1
  21. package/dist/components/game/{2d/atoms → atoms}/StatusEffect.d.ts +1 -1
  22. package/dist/components/game/{2d/atoms → atoms}/WaypointMarker.d.ts +1 -1
  23. package/dist/components/game/{shared/hooks → hooks}/useCamera.d.ts +1 -1
  24. package/dist/components/game/{shared/hooks → hooks}/useUnitSpriteAtlas.d.ts +2 -2
  25. package/dist/components/game/{shared → lib}/index.d.ts +1 -2
  26. package/dist/components/game/{shared → lib}/isometric.d.ts +38 -0
  27. package/dist/components/game/{shared → lib}/isometricTypes.d.ts +4 -0
  28. package/dist/components/game/{shared/lib → lib}/puzzleObject.d.ts +1 -1
  29. package/dist/components/game/{2d/molecules → molecules}/ActionTile.d.ts +1 -1
  30. package/dist/components/game/molecules/Canvas.d.ts +96 -0
  31. package/dist/components/game/molecules/Canvas2D.d.ts +120 -0
  32. package/dist/components/game/{2d/molecules → molecules}/ControlGrid.d.ts +1 -1
  33. package/dist/components/game/molecules/DrawShapeLayer.d.ts +19 -0
  34. package/dist/components/game/molecules/DrawSpriteLayer.d.ts +21 -0
  35. package/dist/components/game/molecules/DrawTextLayer.d.ts +22 -0
  36. package/dist/components/game/{2d/molecules → molecules}/GameHud.d.ts +1 -1
  37. package/dist/components/game/{2d/molecules → molecules}/InventoryGrid.d.ts +1 -1
  38. package/dist/components/game/{2d/molecules → molecules}/ResourceBar.d.ts +1 -1
  39. package/dist/components/game/{2d/molecules → molecules}/StatBadge.d.ts +1 -1
  40. package/dist/components/game/{2d/molecules → molecules}/StateNode.d.ts +1 -1
  41. package/dist/components/game/{2d/molecules → molecules}/TraitSlot.d.ts +1 -1
  42. package/dist/components/game/{2d/molecules → molecules}/TraitStateViewer.d.ts +1 -1
  43. package/dist/components/game/{2d/molecules → molecules}/TransitionArrow.d.ts +1 -1
  44. package/dist/components/game/{2d/molecules → molecules}/index.d.ts +9 -16
  45. package/dist/components/game/patterns.d.ts +20 -0
  46. package/dist/components/game/{shared/providers → providers}/GameAudioProvider.d.ts +1 -1
  47. package/dist/components/game/{2d/templates → templates}/GameTemplate.d.ts +1 -1
  48. package/dist/components/game/three/atoms/SpriteBillboard3D.d.ts +18 -0
  49. package/dist/components/game/three/game3dTheme.d.ts +55 -0
  50. package/dist/components/game/{shared → three}/hooks/useGameCanvas3DEvents.d.ts +4 -3
  51. package/dist/components/game/{3d → three}/index.cjs +787 -2986
  52. package/dist/components/game/{3d → three}/index.css +3 -3
  53. package/dist/components/game/{3d → three}/index.d.ts +0 -1
  54. package/dist/components/game/{3d → three}/index.js +644 -2842
  55. package/dist/components/game/{shared → three}/lib/game3dAssetManifest.d.ts +1 -1
  56. package/dist/components/game/three/molecules/GameCamera3D.d.ts +35 -0
  57. package/dist/components/game/three/molecules/GameCanvas3D.d.ts +175 -0
  58. package/dist/components/game/{3d → three}/molecules/Lighting3D.d.ts +10 -2
  59. package/dist/components/game/{3d → three}/molecules/index.d.ts +9 -12
  60. package/dist/components/game/{3d → three}/patterns.d.ts +0 -4
  61. package/dist/components/index.cjs +2326 -4362
  62. package/dist/components/index.js +1420 -3449
  63. package/dist/lib/drawable/Drawable3D.d.ts +20 -0
  64. package/dist/lib/drawable/contract.d.ts +68 -0
  65. package/dist/lib/drawable/hitTest.d.ts +23 -0
  66. package/dist/lib/drawable/mesh3d.d.ts +28 -0
  67. package/dist/lib/drawable/paintDispatch.d.ts +22 -0
  68. package/dist/lib/drawable/projector.d.ts +14 -0
  69. package/dist/lib/drawable/projector3d.d.ts +20 -0
  70. package/dist/lib/imageCache.d.ts +9 -0
  71. package/dist/lib/painter2d.d.ts +81 -0
  72. package/dist/lib/webPainter2d.d.ts +7 -0
  73. package/dist/marketing/index.cjs +10 -0
  74. package/dist/marketing/index.js +10 -0
  75. package/dist/providers/index.cjs +1756 -3976
  76. package/dist/providers/index.js +964 -3184
  77. package/dist/runtime/index.cjs +1766 -3982
  78. package/dist/runtime/index.js +980 -3196
  79. package/package.json +13 -13
  80. package/dist/components/game/2d/molecules/Canvas2D.d.ts +0 -175
  81. package/dist/components/game/2d/organisms/BuilderBoard.d.ts +0 -60
  82. package/dist/components/game/2d/organisms/ClassifierBoard.d.ts +0 -66
  83. package/dist/components/game/2d/organisms/DebuggerBoard.d.ts +0 -61
  84. package/dist/components/game/2d/organisms/EventHandlerBoard.d.ts +0 -42
  85. package/dist/components/game/2d/organisms/NegotiatorBoard.d.ts +0 -56
  86. package/dist/components/game/2d/organisms/SequencerBoard.d.ts +0 -69
  87. package/dist/components/game/2d/organisms/SimulatorBoard.d.ts +0 -58
  88. package/dist/components/game/2d/organisms/StateArchitectBoard.d.ts +0 -61
  89. package/dist/components/game/3d/molecules/GameCanvas3D.d.ts +0 -259
  90. package/dist/components/game/3d/organisms/GameBoard3D.d.ts +0 -66
  91. package/dist/components/game/3d/templates/GameCanvas3DBattleTemplate.d.ts +0 -82
  92. package/dist/components/game/3d/templates/GameCanvas3DCastleTemplate.d.ts +0 -87
  93. package/dist/components/game/3d/templates/GameCanvas3DWorldMapTemplate.d.ts +0 -95
  94. package/dist/components/game/shared/effects.d.ts +0 -253
  95. /package/dist/components/game/{2d/atoms → atoms}/ComboCounter.d.ts +0 -0
  96. /package/dist/components/game/{2d/atoms → atoms}/DamageNumber.d.ts +0 -0
  97. /package/dist/components/game/{2d/atoms → atoms}/DialogueBubble.d.ts +0 -0
  98. /package/dist/components/game/{2d/atoms → atoms}/GameCard.d.ts +0 -0
  99. /package/dist/components/game/{2d/atoms → atoms}/HealthBar.d.ts +0 -0
  100. /package/dist/components/game/{2d/atoms → atoms}/TimerDisplay.d.ts +0 -0
  101. /package/dist/components/game/{2d/atoms → atoms}/TurnIndicator.d.ts +0 -0
  102. /package/dist/components/game/{shared/hooks → hooks}/useGameAudio.d.ts +0 -0
  103. /package/dist/components/game/{shared → hooks}/useImageCache.d.ts +0 -0
  104. /package/dist/components/game/{shared → lib}/boardEntity.d.ts +0 -0
  105. /package/dist/components/game/{shared/lib → lib}/editorUtils.d.ts +0 -0
  106. /package/dist/components/game/{shared → lib}/game.d.ts +0 -0
  107. /package/dist/components/game/{shared → lib}/makeAsset.d.ts +0 -0
  108. /package/dist/components/game/{shared/lib → lib}/mechanics.d.ts +0 -0
  109. /package/dist/components/game/{shared/lib → lib}/physicsPresets.d.ts +0 -0
  110. /package/dist/components/game/{shared/lib → lib}/physicsTypes.d.ts +0 -0
  111. /package/dist/components/game/{shared → lib}/spriteAnimation.d.ts +0 -0
  112. /package/dist/components/game/{shared → lib}/spriteAnimationTypes.d.ts +0 -0
  113. /package/dist/components/game/{shared → lib}/spriteSheetConstants.d.ts +0 -0
  114. /package/dist/components/game/{2d/molecules → molecules}/ActionPalette.d.ts +0 -0
  115. /package/dist/components/game/{2d/molecules → molecules}/EventLog.d.ts +0 -0
  116. /package/dist/components/game/{2d/molecules → molecules}/GameMenu.d.ts +0 -0
  117. /package/dist/components/game/{2d/molecules → molecules}/ObjectRulePanel.d.ts +0 -0
  118. /package/dist/components/game/{2d/molecules → molecules}/RuleEditor.d.ts +0 -0
  119. /package/dist/components/game/{2d/molecules → molecules}/SequenceBar.d.ts +0 -0
  120. /package/dist/components/game/{2d/molecules → molecules}/StateGraph.d.ts +0 -0
  121. /package/dist/components/game/{2d/molecules → molecules}/StateJsonView.d.ts +0 -0
  122. /package/dist/components/game/{2d/molecules → molecules}/VariablePanel.d.ts +0 -0
  123. /package/dist/components/game/{2d/templates → templates}/GameShell.d.ts +0 -0
  124. /package/dist/components/game/{shared → three}/hooks/useAssetLoader.d.ts +0 -0
  125. /package/dist/components/game/{shared → three}/hooks/useRaycaster.d.ts +0 -0
  126. /package/dist/components/game/{shared → three}/hooks/useSceneGraph.d.ts +0 -0
  127. /package/dist/components/game/{shared → three}/hooks/useThree.d.ts +0 -0
  128. /package/dist/components/game/{shared → three}/lib/AssetLoader.d.ts +0 -0
  129. /package/dist/components/game/{shared → three}/lib/culling.d.ts +0 -0
  130. /package/dist/components/game/{shared → three}/lib/grid3D.d.ts +0 -0
  131. /package/dist/components/game/{3d → three}/molecules/Camera3D.d.ts +0 -0
  132. /package/dist/components/game/{3d → three}/molecules/Canvas3DErrorBoundary.d.ts +0 -0
  133. /package/dist/components/game/{3d → three}/molecules/Canvas3DLoadingState.d.ts +0 -0
  134. /package/dist/components/game/{3d → three}/molecules/ModelLoader.d.ts +0 -0
  135. /package/dist/components/game/{3d → three}/molecules/Scene3D.d.ts +0 -0
@@ -1,5 +1,4 @@
1
- import * as React8 from 'react';
2
- import React8__default, { forwardRef, useRef, useEffect, useImperativeHandle, useState, useMemo, useCallback, createContext, useContext, Component, useSyncExternalStore, useReducer } from 'react';
1
+ import React7, { forwardRef, useRef, useEffect, useImperativeHandle, useState, useMemo, useCallback, createContext, useContext, Component, useReducer } from 'react';
3
2
  import { useThree, useFrame, Canvas, useLoader } from '@react-three/fiber';
4
3
  import * as THREE6 from 'three';
5
4
  import { Vector3, QuadraticBezierCurve3, MathUtils, Quaternion } from 'three';
@@ -14,8 +13,6 @@ import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
14
13
  import { EventBusContext, useTraitScope } from '@almadar/ui/providers';
15
14
  import { clsx } from 'clsx';
16
15
  import { twMerge } from 'tailwind-merge';
17
- import * as LucideIcons from 'lucide-react';
18
- import { Loader2 } from 'lucide-react';
19
16
  import { EffectComposer, Bloom, DepthOfField, Vignette } from '@react-three/postprocessing';
20
17
  import { useTranslate } from '@almadar/ui/hooks';
21
18
 
@@ -149,6 +146,10 @@ function Lighting3D({
149
146
  shadows = true,
150
147
  shadowMapSize = 2048,
151
148
  shadowCameraSize = 20,
149
+ shadowBias = -1e-3,
150
+ shadowNormalBias,
151
+ shadowCameraNear = 0.1,
152
+ shadowCameraFar = 100,
152
153
  showHelpers = false
153
154
  }) {
154
155
  return /* @__PURE__ */ jsxs(Fragment, { children: [
@@ -165,9 +166,10 @@ function Lighting3D({
165
166
  "shadow-camera-right": shadowCameraSize,
166
167
  "shadow-camera-top": shadowCameraSize,
167
168
  "shadow-camera-bottom": -shadowCameraSize,
168
- "shadow-camera-near": 0.1,
169
- "shadow-camera-far": 100,
170
- "shadow-bias": -1e-3
169
+ "shadow-camera-near": shadowCameraNear,
170
+ "shadow-camera-far": shadowCameraFar,
171
+ "shadow-bias": shadowBias,
172
+ "shadow-normalBias": shadowNormalBias
171
173
  }
172
174
  ),
173
175
  /* @__PURE__ */ jsx(
@@ -702,7 +704,7 @@ var AssetLoader = class {
702
704
  };
703
705
  var assetLoader = new AssetLoader();
704
706
 
705
- // components/game/shared/hooks/useThree.ts
707
+ // components/game/three/hooks/useThree.ts
706
708
  var DEFAULT_OPTIONS = {
707
709
  cameraMode: "isometric",
708
710
  cameraPosition: [10, 10, 10],
@@ -1422,7 +1424,7 @@ function useEmitEvent() {
1422
1424
  );
1423
1425
  }
1424
1426
 
1425
- // components/game/shared/hooks/useGameCanvas3DEvents.ts
1427
+ // components/game/three/hooks/useGameCanvas3DEvents.ts
1426
1428
  function useGameCanvas3DEvents(options) {
1427
1429
  const {
1428
1430
  tileClickEvent,
@@ -1561,127 +1563,33 @@ function useGameCanvas3DEvents(options) {
1561
1563
  };
1562
1564
  }
1563
1565
 
1564
- // components/game/shared/spriteAnimation.ts
1565
- function resolveSheetDirection(facing) {
1566
- switch (facing) {
1567
- case "se":
1568
- return { sheetDir: "se", flipX: false };
1569
- case "sw":
1570
- return { sheetDir: "sw", flipX: false };
1571
- case "ne":
1572
- return { sheetDir: "sw", flipX: true };
1573
- case "nw":
1574
- return { sheetDir: "se", flipX: true };
1566
+ // lib/drawable/hitTest.ts
1567
+ function collectDrawnItems(nodes) {
1568
+ const out = [];
1569
+ for (const n of nodes) {
1570
+ switch (n.type) {
1571
+ case "draw-sprite":
1572
+ case "draw-shape":
1573
+ case "draw-text":
1574
+ out.push({ pos: n.position, id: n.id });
1575
+ break;
1576
+ case "draw-sprite-layer":
1577
+ case "draw-shape-layer":
1578
+ case "draw-text-layer":
1579
+ for (const it of n.items) out.push({ pos: it.position, id: it.id });
1580
+ break;
1581
+ }
1575
1582
  }
1583
+ return out;
1576
1584
  }
1577
- function frameRect(frame, row, columns, frameWidth, frameHeight) {
1578
- return {
1579
- sx: frame % columns * frameWidth,
1580
- sy: row * frameHeight,
1581
- sw: frameWidth,
1582
- sh: frameHeight
1583
- };
1584
- }
1585
-
1586
- // components/game/shared/hooks/useUnitSpriteAtlas.ts
1587
- function unitAtlasUrl(unit) {
1588
- return unit.spriteSheet?.url ?? null;
1589
- }
1590
- function resolveSheetUrl(atlasUrl, relativeSheetPath) {
1591
- try {
1592
- return new URL(relativeSheetPath, atlasUrl).toString();
1593
- } catch {
1594
- const base = atlasUrl.slice(0, atlasUrl.lastIndexOf("/") + 1);
1595
- return `${base}${relativeSheetPath.replace(/^\.\//, "")}`;
1585
+ function buildHitIndex(items) {
1586
+ const m = /* @__PURE__ */ new Map();
1587
+ for (const it of items) {
1588
+ if (it.id !== void 0) m.set(`${it.pos.x},${it.pos.y}`, it.id);
1596
1589
  }
1590
+ return m;
1597
1591
  }
1598
- function useUnitSpriteAtlas(units) {
1599
- const atlasCacheRef = useRef(/* @__PURE__ */ new Map());
1600
- const loadingRef = useRef(/* @__PURE__ */ new Set());
1601
- const [pendingCount, setPendingCount] = useState(0);
1602
- const [, forceTick] = useState(0);
1603
- const atlasUrls = useMemo(() => {
1604
- const set = /* @__PURE__ */ new Set();
1605
- for (const unit of units) {
1606
- const url = unitAtlasUrl(unit);
1607
- if (url) set.add(url);
1608
- }
1609
- return [...set];
1610
- }, [units]);
1611
- useEffect(() => {
1612
- const cache = atlasCacheRef.current;
1613
- const loading = loadingRef.current;
1614
- const toLoad = atlasUrls.filter((url) => !cache.has(url) && !loading.has(url));
1615
- if (toLoad.length === 0) return;
1616
- let cancelled = false;
1617
- setPendingCount((prev) => prev + toLoad.length);
1618
- for (const url of toLoad) {
1619
- loading.add(url);
1620
- fetch(url).then((res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))).then((atlas) => {
1621
- if (cancelled) return;
1622
- cache.set(url, atlas);
1623
- }).catch(() => {
1624
- }).finally(() => {
1625
- if (cancelled) return;
1626
- loading.delete(url);
1627
- setPendingCount((prev) => Math.max(0, prev - 1));
1628
- forceTick((n) => n + 1);
1629
- });
1630
- }
1631
- return () => {
1632
- cancelled = true;
1633
- };
1634
- }, [atlasUrls]);
1635
- const sheetUrls = useMemo(() => {
1636
- const urls = /* @__PURE__ */ new Set();
1637
- for (const unit of units) {
1638
- const atlasUrl = unitAtlasUrl(unit);
1639
- if (!atlasUrl) continue;
1640
- const atlas = atlasCacheRef.current.get(atlasUrl);
1641
- if (!atlas) continue;
1642
- for (const rel of Object.values(atlas.sheets)) {
1643
- if (rel) urls.add(resolveSheetUrl(atlasUrl, rel));
1644
- }
1645
- }
1646
- return [...urls];
1647
- }, [units, pendingCount]);
1648
- const resolveUnitFrame = useCallback((unitId) => {
1649
- const unit = units.find((u) => u.id === unitId);
1650
- if (!unit) return null;
1651
- const atlasUrl = unitAtlasUrl(unit);
1652
- if (!atlasUrl) return null;
1653
- const atlas = atlasCacheRef.current.get(atlasUrl);
1654
- if (!atlas) return null;
1655
- const animation = unit.animation ?? "idle";
1656
- const frame = unit.frame ?? 0;
1657
- const def = atlas.animations[animation] ?? atlas.animations.idle;
1658
- if (!def) return null;
1659
- const { sheetDir, flipX } = resolveSheetDirection("se");
1660
- const rel = atlas.sheets[sheetDir] ?? atlas.sheets.se ?? atlas.sheets.sw;
1661
- if (!rel) return null;
1662
- const sheetUrl = resolveSheetUrl(atlasUrl, rel);
1663
- const rect = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
1664
- return {
1665
- sheetUrl,
1666
- sx: rect.sx,
1667
- sy: rect.sy,
1668
- sw: rect.sw,
1669
- sh: rect.sh,
1670
- flipX,
1671
- applyBreathing: animation === "idle"
1672
- };
1673
- }, [units]);
1674
- return { sheetUrls, resolveUnitFrame, pendingCount };
1675
- }
1676
- function cn(...inputs) {
1677
- return twMerge(clsx(inputs));
1678
- }
1679
- var DEFAULT_GRID_CONFIG = {
1680
- cellSize: 1,
1681
- offsetX: 0,
1682
- offsetZ: 0
1683
- };
1684
- function CameraController({
1592
+ function CameraController3D({
1685
1593
  onCameraChange
1686
1594
  }) {
1687
1595
  const { camera } = useThree();
@@ -1696,48 +1604,7 @@ function CameraController({
1696
1604
  }, [camera.position, onCameraChange]);
1697
1605
  return null;
1698
1606
  }
1699
- function UnitSpriteBillboard({
1700
- sheetUrl,
1701
- resolveFrame,
1702
- height = 1.2
1703
- }) {
1704
- const texture = useLoader(THREE6.TextureLoader, sheetUrl);
1705
- const meshRef = useRef(null);
1706
- const matRef = useRef(null);
1707
- const [aspect, setAspect] = useState(1);
1708
- useFrame(() => {
1709
- const frame = resolveFrame();
1710
- if (!frame || !texture.image) return;
1711
- const imgW = texture.image.width;
1712
- const imgH = texture.image.height;
1713
- if (!imgW || !imgH) return;
1714
- texture.repeat.set((frame.flipX ? -1 : 1) * (frame.sw / imgW), frame.sh / imgH);
1715
- texture.offset.set(
1716
- frame.flipX ? (frame.sx + frame.sw) / imgW : frame.sx / imgW,
1717
- 1 - (frame.sy + frame.sh) / imgH
1718
- );
1719
- texture.magFilter = THREE6.NearestFilter;
1720
- texture.minFilter = THREE6.NearestFilter;
1721
- texture.needsUpdate = true;
1722
- const nextAspect = frame.sw / frame.sh;
1723
- if (Math.abs(nextAspect - aspect) > 1e-3) setAspect(nextAspect);
1724
- if (matRef.current) matRef.current.needsUpdate = true;
1725
- });
1726
- return /* @__PURE__ */ jsxs("mesh", { ref: meshRef, position: [0, height / 2, 0], children: [
1727
- /* @__PURE__ */ jsx("planeGeometry", { args: [height * aspect, height] }),
1728
- /* @__PURE__ */ jsx(
1729
- "meshBasicMaterial",
1730
- {
1731
- ref: matRef,
1732
- map: texture,
1733
- transparent: true,
1734
- alphaTest: 0.1,
1735
- side: THREE6.DoubleSide
1736
- }
1737
- )
1738
- ] });
1739
- }
1740
- function FollowCamera({
1607
+ function FollowCamera3D({
1741
1608
  target,
1742
1609
  offset
1743
1610
  }) {
@@ -1753,371 +1620,156 @@ function FollowCamera({
1753
1620
  });
1754
1621
  return null;
1755
1622
  }
1756
- function LerpedGroup({
1757
- target,
1758
- enabled,
1759
- children
1760
- }) {
1761
- const ref = useRef(null);
1762
- const goal = useRef(new THREE6.Vector3());
1763
- useFrame((_, delta) => {
1764
- const g = ref.current;
1765
- if (!g) return;
1766
- goal.current.set(target[0], target[1], target[2]);
1767
- if (enabled) g.position.lerp(goal.current, Math.min(1, delta * 10));
1768
- else g.position.copy(goal.current);
1769
- });
1770
- return /* @__PURE__ */ jsx("group", { ref, position: target, children });
1771
- }
1772
- var SIDE_PLATFORM_COLORS = {
1773
- ground: "#5b8c3e",
1774
- platform: "#8b5a2b",
1775
- hazard: "#cc4444",
1776
- goal: "#4488cc"
1777
- };
1778
- function SideScene({
1779
- player,
1780
- platforms,
1781
- worldHeight,
1782
- ppu,
1783
- playerSprite,
1784
- tileSprites,
1785
- interpolate,
1786
- features = [],
1787
- events = [],
1788
- assetManifest
1789
- }) {
1790
- const pw = player.width ?? 32;
1791
- const ph = player.height ?? 48;
1792
- const playerPos = [
1793
- (player.x + pw / 2) / ppu,
1794
- (worldHeight - player.y - ph) / ppu,
1795
- 0
1796
- ];
1797
- const playerH = Math.max(ph / ppu, 0.5);
1798
- return /* @__PURE__ */ jsxs("group", { children: [
1799
- platforms.map((p, i) => {
1800
- const platformType = p.type ?? "platform";
1801
- const model = tileSprites?.[platformType]?.url;
1802
- const topY = (worldHeight - p.y) / ppu;
1803
- if (model) {
1804
- const cells = Math.max(1, Math.round(p.width / ppu));
1805
- const x0 = p.x / ppu;
1806
- 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}`);
1807
- }
1808
- return /* @__PURE__ */ jsxs(
1809
- "mesh",
1810
- {
1811
- position: [(p.x + p.width / 2) / ppu, topY - p.height / 2 / ppu, 0],
1812
- children: [
1813
- /* @__PURE__ */ jsx("boxGeometry", { args: [p.width / ppu, p.height / ppu, 1] }),
1814
- /* @__PURE__ */ jsx("meshStandardMaterial", { color: SIDE_PLATFORM_COLORS[platformType] ?? "#888888" })
1815
- ]
1816
- },
1817
- `plat-${i}`
1818
- );
1819
- }),
1820
- features.map((feature, i) => {
1821
- const model = feature.assetUrl ?? assetManifest?.features?.[feature.type];
1822
- const fx = feature.x / ppu;
1823
- const fy = (worldHeight - feature.y) / ppu;
1824
- if (!model?.url) return null;
1825
- return /* @__PURE__ */ jsx("group", { position: [fx, fy, 0], children: /* @__PURE__ */ jsx(ModelLoader, { url: model.url, scale: 0.6, tint: feature.color, fallbackGeometry: "sphere", castShadow: true }) }, feature.id ?? `sfeat-${i}`);
1826
- }),
1827
- events.map((ev, i) => /* @__PURE__ */ jsx(
1828
- EventMarker,
1623
+ function SpriteBillboard({ node, world }) {
1624
+ const texture = useLoader(THREE6.TextureLoader, node.asset.url);
1625
+ const img = texture.image;
1626
+ const imgW = img?.width || 1;
1627
+ const imgH = img?.height || 1;
1628
+ if (node.frame) {
1629
+ texture.repeat.set(node.frame.w / imgW, node.frame.h / imgH);
1630
+ texture.offset.set(node.frame.x / imgW, 1 - (node.frame.y + node.frame.h) / imgH);
1631
+ texture.magFilter = texture.minFilter = THREE6.NearestFilter;
1632
+ texture.needsUpdate = true;
1633
+ }
1634
+ const aspect = node.frame ? node.frame.w / node.frame.h : imgW / imgH;
1635
+ const height = node.height ?? 1;
1636
+ const width = node.width ?? height * aspect;
1637
+ return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + height / 2, world[2]], children: /* @__PURE__ */ jsxs("mesh", { children: [
1638
+ /* @__PURE__ */ jsx("planeGeometry", { args: [width, height] }),
1639
+ /* @__PURE__ */ jsx(
1640
+ "meshBasicMaterial",
1829
1641
  {
1830
- event: ev,
1831
- position: [ev.x / ppu, (worldHeight - (ev.y ?? 0)) / ppu + 0.8, 0.2]
1832
- },
1833
- ev.id ?? `sev-${i}`
1834
- )),
1835
- /* @__PURE__ */ jsx(LerpedGroup, { target: playerPos, enabled: interpolate, children: playerSprite?.url ? /* @__PURE__ */ jsx("group", { position: [0, playerH / 2, 0], children: /* @__PURE__ */ jsx(
1642
+ map: texture,
1643
+ transparent: true,
1644
+ alphaTest: 0.1,
1645
+ side: THREE6.DoubleSide,
1646
+ opacity: node.opacity ?? 1
1647
+ }
1648
+ )
1649
+ ] }) });
1650
+ }
1651
+ function Sprite3D({ node, projector }) {
1652
+ if (node.asset.dimension === "3d" && node.asset.url) {
1653
+ return /* @__PURE__ */ jsx("group", { position: projector.toWorld(node.position), children: /* @__PURE__ */ jsx(
1836
1654
  ModelLoader,
1837
1655
  {
1838
- url: playerSprite.url,
1839
- scale: playerH,
1840
- rotation: [0, player.facingRight ? 90 : -90, 0],
1841
- animation: player.animation ?? "idle",
1656
+ url: node.asset.url,
1657
+ scale: node.width ?? projector.cellSize,
1658
+ rotation: [0, node.rotation ?? 0, 0],
1842
1659
  fallbackGeometry: "box",
1843
- castShadow: true
1660
+ castShadow: true,
1661
+ receiveShadow: true
1844
1662
  }
1845
- ) }) : /* @__PURE__ */ jsxs("mesh", { position: [0, playerH / 2, 0], children: [
1846
- /* @__PURE__ */ jsx("capsuleGeometry", { args: [playerH * 0.25, playerH * 0.5, 4, 8] }),
1847
- /* @__PURE__ */ jsx("meshStandardMaterial", { color: "#4488ff" })
1848
- ] }) })
1663
+ ) });
1664
+ }
1665
+ if (!node.asset.url) return null;
1666
+ return /* @__PURE__ */ jsx(SpriteBillboard, { node, world: projector.toWorld(node.position) });
1667
+ }
1668
+ function Shape3D({ node, projector }) {
1669
+ const world = projector.toWorld(node.position);
1670
+ const color = node.fill ?? node.stroke ?? "#ffffff";
1671
+ let geometry;
1672
+ switch (node.shape) {
1673
+ case "cell":
1674
+ case "rect": {
1675
+ const w = node.shape === "cell" ? projector.cellSize * 0.95 : node.width ?? projector.cellSize;
1676
+ const h = node.shape === "cell" ? projector.cellSize * 0.95 : node.height ?? projector.cellSize;
1677
+ geometry = /* @__PURE__ */ jsx("planeGeometry", { args: [w, h] });
1678
+ break;
1679
+ }
1680
+ case "ellipse": {
1681
+ const radiusX = node.radiusX ?? 0.4;
1682
+ geometry = node.stroke && !node.fill ? /* @__PURE__ */ jsx("ringGeometry", { args: [Math.max(0, radiusX - 0.08), radiusX, 32] }) : /* @__PURE__ */ jsx("circleGeometry", { args: [radiusX, 32] });
1683
+ break;
1684
+ }
1685
+ case "poly": {
1686
+ if (!node.points || node.points.length === 0) return null;
1687
+ const s = new THREE6.Shape();
1688
+ node.points.forEach((p, i) => {
1689
+ if (i === 0) s.moveTo(p.x, p.y);
1690
+ else s.lineTo(p.x, p.y);
1691
+ });
1692
+ s.closePath();
1693
+ geometry = /* @__PURE__ */ jsx("shapeGeometry", { args: [s] });
1694
+ break;
1695
+ }
1696
+ default:
1697
+ return null;
1698
+ }
1699
+ return /* @__PURE__ */ jsxs("mesh", { rotation: [-Math.PI / 2, 0, 0], position: [world[0], world[1] + 0.02, world[2]], children: [
1700
+ geometry,
1701
+ /* @__PURE__ */ jsx("meshBasicMaterial", { color, transparent: true, opacity: node.opacity ?? 1, side: THREE6.DoubleSide })
1849
1702
  ] });
1850
1703
  }
1851
- var UNIT_BASE_MODEL_SCALE = 0.5;
1852
- var UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
1853
- var UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
1854
- var TILE_TOP_OFFSET = 0.1;
1855
- var EVENT_COLORS = {
1856
- hit: "#ff5544",
1857
- damage: "#ff5544",
1858
- attack: "#ff8844",
1859
- heal: "#44dd66",
1860
- pickup: "#ffcc33",
1861
- score: "#ffcc33",
1862
- death: "#aa3333",
1863
- win: "#66ff88",
1864
- lose: "#ff6666"
1865
- };
1866
- function EventMarker({
1867
- event,
1868
- position
1869
- }) {
1870
- return /* @__PURE__ */ jsx(Billboard, { position, children: /* @__PURE__ */ jsx(
1704
+ function Text3D({ node, projector }) {
1705
+ const world = projector.toWorld(node.position);
1706
+ return /* @__PURE__ */ jsx(Billboard, { position: [world[0], world[1] + 1.2, world[2]], children: /* @__PURE__ */ jsx(
1871
1707
  Text,
1872
1708
  {
1873
1709
  fontSize: 0.32,
1874
- color: EVENT_COLORS[event.type] ?? "#ffffff",
1875
- outlineWidth: 0.02,
1876
- outlineColor: "#000000",
1710
+ color: node.color,
1877
1711
  anchorX: "center",
1878
1712
  anchorY: "middle",
1879
- children: event.message ?? event.type
1713
+ outlineWidth: 0.02,
1714
+ outlineColor: "#000000",
1715
+ children: node.text
1880
1716
  }
1881
1717
  ) });
1882
1718
  }
1883
- function DefaultTile({
1884
- tile,
1885
- position,
1886
- model,
1887
- isSelected,
1888
- isHovered,
1889
- isValidMove,
1890
- isAttackTarget,
1891
- onTileClick,
1892
- onTileHover
1893
- }) {
1894
- let color = 8421504;
1895
- if (tile.type === "water") color = 4491468;
1896
- else if (tile.type === "grass") color = 4500036;
1897
- else if (tile.type === "sand") color = 14535816;
1898
- else if (tile.type === "rock") color = 8947848;
1899
- else if (tile.type === "snow") color = 15658734;
1900
- let emissive = 0;
1901
- if (isSelected) emissive = 4473924;
1902
- else if (isAttackTarget) emissive = 4456448;
1903
- else if (isValidMove) emissive = 17408;
1904
- else if (isHovered) emissive = 2236962;
1905
- if (model?.url) {
1906
- return /* @__PURE__ */ jsx(
1907
- "group",
1908
- {
1909
- position,
1910
- onClick: (e) => onTileClick(tile, e),
1911
- onPointerEnter: (e) => onTileHover(tile, e),
1912
- onPointerLeave: (e) => onTileHover(null, e),
1913
- userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
1914
- children: /* @__PURE__ */ jsx(
1915
- ModelLoader,
1916
- {
1917
- url: model.url,
1918
- scale: 0.95,
1919
- rotation: [0, tile.rotation ?? 0, 0],
1920
- fallbackGeometry: "box",
1921
- castShadow: true,
1922
- receiveShadow: true
1923
- }
1924
- )
1925
- }
1926
- );
1719
+ function Drawable3D({ node, projector }) {
1720
+ switch (node.type) {
1721
+ case "draw-sprite":
1722
+ return /* @__PURE__ */ jsx(Sprite3D, { node, projector });
1723
+ case "draw-shape":
1724
+ return /* @__PURE__ */ jsx(Shape3D, { node, projector });
1725
+ case "draw-text":
1726
+ return /* @__PURE__ */ jsx(Text3D, { node, projector });
1727
+ case "draw-sprite-layer":
1728
+ return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Sprite3D, { node: item, projector }, i)) });
1729
+ case "draw-shape-layer":
1730
+ return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Shape3D, { node: item, projector }, i)) });
1731
+ case "draw-text-layer":
1732
+ return /* @__PURE__ */ jsx(Fragment, { children: node.items.map((item, i) => /* @__PURE__ */ jsx(Text3D, { node: item, projector }, i)) });
1927
1733
  }
1928
- return /* @__PURE__ */ jsxs(
1929
- "mesh",
1930
- {
1931
- position,
1932
- onClick: (e) => onTileClick(tile, e),
1933
- onPointerEnter: (e) => onTileHover(tile, e),
1934
- onPointerLeave: (e) => onTileHover(null, e),
1935
- userData: { type: "tile", tileId: tile.id, gridX: tile.x, gridZ: tile.z ?? tile.y },
1936
- children: [
1937
- /* @__PURE__ */ jsx("boxGeometry", { args: [0.95, 0.2, 0.95] }),
1938
- /* @__PURE__ */ jsx("meshStandardMaterial", { color, emissive })
1939
- ]
1940
- }
1941
- );
1942
1734
  }
1943
- function DefaultUnit({
1944
- unit,
1945
- position,
1946
- model,
1947
- isSelected,
1948
- unitScale,
1949
- cellSize,
1950
- resolveUnitFrame,
1951
- onUnitClick
1952
- }) {
1953
- const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
1954
- const hasAtlas = unitAtlasUrl(unit) !== null;
1955
- const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
1956
- const modelScale = UNIT_BASE_MODEL_SCALE * unitScale * cellSize;
1957
- const billboardHeight = UNIT_BASE_BILLBOARD_HEIGHT * unitScale * cellSize;
1958
- const primitiveRadius = UNIT_BASE_PRIMITIVE_RADIUS * unitScale * cellSize;
1959
- return /* @__PURE__ */ jsxs(
1960
- "group",
1961
- {
1962
- position,
1963
- onClick: (e) => onUnitClick(unit, e),
1964
- userData: { type: "unit", unitId: unit.id },
1965
- children: [
1966
- isSelected && /* @__PURE__ */ jsxs("mesh", { position: [0, 0.05, 0], rotation: [-Math.PI / 2, 0, 0], children: [
1967
- /* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
1968
- /* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
1969
- ] }),
1970
- hasAtlas && initialFrame ? (
1971
- /* Animated sprite-sheet billboard — single cropped frame, by state */
1972
- /* @__PURE__ */ jsx(Billboard, { children: /* @__PURE__ */ jsx(
1973
- UnitSpriteBillboard,
1974
- {
1975
- sheetUrl: initialFrame.sheetUrl,
1976
- resolveFrame: () => resolveUnitFrame(unit.id),
1977
- height: billboardHeight
1978
- }
1979
- ) })
1980
- ) : model?.url ? (
1981
- /* GLB unit model — LOLO's `animation` field drives the named clip;
1982
- `heading` (radians) faces the model along its travel direction (driving). */
1983
- /* @__PURE__ */ jsx(
1984
- ModelLoader,
1985
- {
1986
- url: model.url,
1987
- scale: modelScale,
1988
- rotation: [0, unit.heading ?? 0, 0],
1989
- animation: unit.animation ?? "idle",
1990
- fallbackGeometry: "box",
1991
- castShadow: true
1992
- }
1993
- )
1994
- ) : /* @__PURE__ */ jsxs(Fragment, { children: [
1995
- /* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius, 0], children: [
1996
- /* @__PURE__ */ jsx("cylinderGeometry", { args: [primitiveRadius, primitiveRadius, primitiveRadius * 0.33, 8] }),
1997
- /* @__PURE__ */ jsx("meshStandardMaterial", { color })
1998
- ] }),
1999
- /* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius * 2, 0], children: [
2000
- /* @__PURE__ */ jsx("capsuleGeometry", { args: [primitiveRadius * 0.67, primitiveRadius * 1.33, 4, 8] }),
2001
- /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2002
- ] }),
2003
- /* @__PURE__ */ jsxs("mesh", { position: [0, primitiveRadius * 3, 0], children: [
2004
- /* @__PURE__ */ jsx("sphereGeometry", { args: [primitiveRadius * 0.4, 8, 8] }),
2005
- /* @__PURE__ */ jsx("meshStandardMaterial", { color })
2006
- ] })
2007
- ] }),
2008
- unit.health !== void 0 && unit.maxHealth !== void 0 && /* @__PURE__ */ jsxs("group", { position: [0, billboardHeight, 0], children: [
2009
- /* @__PURE__ */ jsxs("mesh", { position: [-0.25, 0, 0], children: [
2010
- /* @__PURE__ */ jsx("planeGeometry", { args: [0.5, 0.05] }),
2011
- /* @__PURE__ */ jsx("meshBasicMaterial", { color: 3355443 })
2012
- ] }),
2013
- /* @__PURE__ */ jsxs(
2014
- "mesh",
2015
- {
2016
- position: [
2017
- -0.25 + 0.5 * (unit.health / unit.maxHealth) / 2,
2018
- 0,
2019
- 0.01
2020
- ],
2021
- children: [
2022
- /* @__PURE__ */ jsx("planeGeometry", { args: [0.5 * (unit.health / unit.maxHealth), 0.05] }),
2023
- /* @__PURE__ */ jsx(
2024
- "meshBasicMaterial",
2025
- {
2026
- color: unit.health / unit.maxHealth > 0.5 ? 4500036 : unit.health / unit.maxHealth > 0.25 ? 11184708 : 16729156
2027
- }
2028
- )
2029
- ]
2030
- }
2031
- )
2032
- ] })
2033
- ]
2034
- }
2035
- );
1735
+
1736
+ // lib/drawable/projector3d.ts
1737
+ function create3DProjector(opts = {}) {
1738
+ const cellSize = opts.cellSize ?? 1;
1739
+ const offsetX = opts.offsetX ?? 0;
1740
+ const offsetZ = opts.offsetZ ?? 0;
1741
+ return {
1742
+ cellSize,
1743
+ toWorld: (pos) => [pos.x * cellSize + offsetX, pos.z ?? 0, pos.y * cellSize + offsetZ]
1744
+ };
2036
1745
  }
2037
- function DefaultFeature({
2038
- feature,
2039
- position,
2040
- model,
2041
- onFeatureClick
2042
- }) {
2043
- if (model?.url) {
2044
- return /* @__PURE__ */ jsx(
2045
- ModelLoader,
2046
- {
2047
- url: model.url,
2048
- position,
2049
- scale: 0.5,
2050
- rotation: [0, feature.rotation ?? 0, 0],
2051
- tint: feature.color,
2052
- onClick: () => onFeatureClick(feature, null),
2053
- fallbackGeometry: "box"
2054
- }
2055
- );
2056
- }
2057
- if (feature.type === "tree") {
2058
- return /* @__PURE__ */ jsxs(
2059
- "group",
2060
- {
2061
- position,
2062
- onClick: (e) => onFeatureClick(feature, e),
2063
- userData: { type: "feature", featureId: feature.id },
2064
- children: [
2065
- /* @__PURE__ */ jsxs("mesh", { position: [0, 0.4, 0], children: [
2066
- /* @__PURE__ */ jsx("cylinderGeometry", { args: [0.1, 0.15, 0.8, 6] }),
2067
- /* @__PURE__ */ jsx("meshStandardMaterial", { color: 9127187 })
2068
- ] }),
2069
- /* @__PURE__ */ jsxs("mesh", { position: [0, 0.9, 0], children: [
2070
- /* @__PURE__ */ jsx("coneGeometry", { args: [0.5, 0.8, 8] }),
2071
- /* @__PURE__ */ jsx("meshStandardMaterial", { color: 2263842 })
2072
- ] })
2073
- ]
2074
- }
2075
- );
2076
- }
2077
- if (feature.type === "rock") {
2078
- return /* @__PURE__ */ jsxs(
2079
- "mesh",
2080
- {
2081
- position: [position[0], position[1] + 0.3, position[2]],
2082
- onClick: (e) => onFeatureClick(feature, e),
2083
- userData: { type: "feature", featureId: feature.id },
2084
- children: [
2085
- /* @__PURE__ */ jsx("dodecahedronGeometry", { args: [0.3, 0] }),
2086
- /* @__PURE__ */ jsx("meshStandardMaterial", { color: 8421504 })
2087
- ]
2088
- }
2089
- );
2090
- }
2091
- return null;
1746
+
1747
+ // components/game/three/game3dTheme.ts
1748
+ var GRID_COLORS_3D = {
1749
+ cell: "#444444",
1750
+ section: "#666666"
1751
+ };
1752
+ var DEFAULT_BACKGROUND_3D = "#1a1a2e";
1753
+ function cn(...inputs) {
1754
+ return twMerge(clsx(inputs));
2092
1755
  }
1756
+ var DEFAULT_GRID_CONFIG = {
1757
+ cellSize: 1,
1758
+ offsetX: 0,
1759
+ offsetZ: 0
1760
+ };
2093
1761
  var GameCanvas3D = forwardRef(
2094
1762
  ({
2095
- tiles = [],
2096
- units = [],
2097
- features = [],
2098
- events = [],
2099
- orientation = "standard",
2100
1763
  cameraMode = "isometric",
1764
+ followTarget,
2101
1765
  showGrid = true,
2102
- showCoordinates = false,
2103
- showTileInfo = false,
1766
+ orientation = "standard",
2104
1767
  overlay = "default",
2105
1768
  shadows = true,
2106
- backgroundColor = "#1a1a2e",
2107
- onTileClick,
2108
- onUnitClick,
2109
- onFeatureClick,
2110
- onCanvasClick,
2111
- onTileHover,
2112
- onUnitAnimation,
2113
- assetLoader: customAssetLoader,
2114
- tileRenderer: CustomTileRenderer,
2115
- unitRenderer: CustomUnitRenderer,
2116
- featureRenderer: CustomFeatureRenderer,
1769
+ backgroundColor = DEFAULT_BACKGROUND_3D,
2117
1770
  className,
2118
1771
  isLoading: externalLoading,
2119
1772
  error: externalError,
2120
- preloadAssets = [],
2121
1773
  tileClickEvent,
2122
1774
  unitClickEvent,
2123
1775
  featureClickEvent,
@@ -2127,27 +1779,13 @@ var GameCanvas3D = forwardRef(
2127
1779
  unitAnimationEvent,
2128
1780
  cameraChangeEvent,
2129
1781
  loadingMessage = "Loading 3D Scene...",
2130
- useInstancing = true,
2131
- validMoves = [],
2132
- attackTargets = [],
2133
- selectedTileIds = [],
2134
- selectedUnitId = null,
2135
- unitScale = 1,
2136
1782
  keyMap,
2137
1783
  keyUpMap,
2138
- player,
2139
- platforms = [],
2140
- worldHeight = 400,
2141
- pixelsPerUnit = 32,
2142
- playerSprite,
2143
- tileSprites,
2144
- assetManifest,
2145
- interpolateUnits = false,
2146
- children
1784
+ children,
1785
+ drawables
2147
1786
  }, ref) => {
2148
1787
  const containerRef = useRef(null);
2149
1788
  const controlsRef = useRef(null);
2150
- const [hoveredTile, setHoveredTile] = useState(null);
2151
1789
  const [internalError, setInternalError] = useState(null);
2152
1790
  const eventBus = useEventBus();
2153
1791
  const keysRef = useRef(/* @__PURE__ */ new Set());
@@ -2174,35 +1812,6 @@ var GameCanvas3D = forwardRef(
2174
1812
  window.removeEventListener("keyup", up);
2175
1813
  };
2176
1814
  }, [keyMap, keyUpMap, eventBus]);
2177
- useEffect(() => {
2178
- const el = containerRef.current;
2179
- if (!el) return;
2180
- let node = el;
2181
- let depth = 0;
2182
- while (node && depth < 8) {
2183
- const cs = window.getComputedStyle(node);
2184
- const rect = node.getBoundingClientRect();
2185
- console.log("[almadar:ui:game:3d-height]", {
2186
- depth,
2187
- tag: node.tagName,
2188
- id: node.id || void 0,
2189
- className: node.className?.slice?.(0, 60) || void 0,
2190
- rectHeight: rect.height,
2191
- computedHeight: cs.height,
2192
- computedMinHeight: cs.minHeight,
2193
- computedFlex: cs.flex,
2194
- computedOverflow: cs.overflow
2195
- });
2196
- node = node.parentElement;
2197
- depth++;
2198
- }
2199
- }, []);
2200
- const { sheetUrls: atlasSheetUrls, resolveUnitFrame } = useUnitSpriteAtlas(units);
2201
- const preloadUrls = useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
2202
- const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
2203
- preloadUrls,
2204
- loader: customAssetLoader
2205
- });
2206
1815
  const eventHandlers = useGameCanvas3DEvents({
2207
1816
  tileClickEvent,
2208
1817
  unitClickEvent,
@@ -2211,34 +1820,35 @@ var GameCanvas3D = forwardRef(
2211
1820
  tileHoverEvent,
2212
1821
  tileLeaveEvent,
2213
1822
  unitAnimationEvent,
2214
- cameraChangeEvent,
2215
- onTileClick,
2216
- onUnitClick,
2217
- onFeatureClick,
2218
- onCanvasClick,
2219
- onTileHover,
2220
- onUnitAnimation
1823
+ cameraChangeEvent
2221
1824
  });
1825
+ const drawnItems = useMemo(() => collectDrawnItems(drawables ?? []), [drawables]);
1826
+ const scenePositions = useMemo(() => drawnItems.map((i) => i.pos), [drawnItems]);
1827
+ const hitIndex = useMemo(() => buildHitIndex(drawnItems), [drawnItems]);
2222
1828
  const gridBounds = useMemo(() => {
2223
- if (tiles.length === 0) {
1829
+ if (scenePositions.length === 0) {
2224
1830
  return { minX: 0, maxX: 10, minZ: 0, maxZ: 10 };
2225
1831
  }
2226
- const xs = tiles.map((t) => t.x);
2227
- const zs = tiles.map((t) => t.z || t.y || 0);
2228
- return {
2229
- minX: Math.min(...xs),
2230
- maxX: Math.max(...xs),
2231
- minZ: Math.min(...zs),
2232
- maxZ: Math.max(...zs)
2233
- };
2234
- }, [tiles]);
2235
- const cameraTarget = useMemo(() => {
2236
- return [
1832
+ let minX = Infinity;
1833
+ let maxX = -Infinity;
1834
+ let minZ = Infinity;
1835
+ let maxZ = -Infinity;
1836
+ for (const p of scenePositions) {
1837
+ if (p.x < minX) minX = p.x;
1838
+ if (p.x > maxX) maxX = p.x;
1839
+ if (p.y < minZ) minZ = p.y;
1840
+ if (p.y > maxZ) maxZ = p.y;
1841
+ }
1842
+ return { minX, maxX, minZ, maxZ };
1843
+ }, [scenePositions]);
1844
+ const cameraTarget = useMemo(
1845
+ () => [
2237
1846
  (gridBounds.minX + gridBounds.maxX) / 2,
2238
1847
  0,
2239
1848
  (gridBounds.minZ + gridBounds.maxZ) / 2
2240
- ];
2241
- }, [gridBounds]);
1849
+ ],
1850
+ [gridBounds]
1851
+ );
2242
1852
  const gridConfig = useMemo(
2243
1853
  () => ({
2244
1854
  ...DEFAULT_GRID_CONFIG,
@@ -2247,12 +1857,12 @@ var GameCanvas3D = forwardRef(
2247
1857
  }),
2248
1858
  [gridBounds]
2249
1859
  );
2250
- const gridToWorld2 = useCallback(
2251
- (x, z, y = 0) => {
2252
- const worldX = (x - gridBounds.minX) * gridConfig.cellSize;
2253
- const worldZ = (z - gridBounds.minZ) * gridConfig.cellSize;
2254
- return [worldX, y * gridConfig.cellSize, worldZ];
2255
- },
1860
+ const drawableProjector = useMemo(
1861
+ () => create3DProjector({
1862
+ cellSize: gridConfig.cellSize,
1863
+ offsetX: -gridBounds.minX * gridConfig.cellSize,
1864
+ offsetZ: -gridBounds.minZ * gridConfig.cellSize
1865
+ }),
2256
1866
  [gridBounds, gridConfig]
2257
1867
  );
2258
1868
  useImperativeHandle(ref, () => ({
@@ -2286,42 +1896,8 @@ var GameCanvas3D = forwardRef(
2286
1896
  return canvas.toDataURL("image/png");
2287
1897
  }
2288
1898
  return null;
2289
- },
2290
- export: () => ({
2291
- tiles,
2292
- units,
2293
- features
2294
- })
1899
+ }
2295
1900
  }));
2296
- const handleTileClick = useCallback(
2297
- (tile, event) => {
2298
- eventHandlers.handleTileClick(tile, event);
2299
- },
2300
- [eventHandlers]
2301
- );
2302
- const handleUnitClick = useCallback(
2303
- (unit, event) => {
2304
- eventHandlers.handleUnitClick(unit, event);
2305
- },
2306
- [eventHandlers]
2307
- );
2308
- const handleFeatureClick = useCallback(
2309
- (feature, event) => {
2310
- if (event) {
2311
- eventHandlers.handleFeatureClick(feature, event);
2312
- }
2313
- },
2314
- [eventHandlers]
2315
- );
2316
- const handleTileHover = useCallback(
2317
- (tile, event) => {
2318
- setHoveredTile(tile);
2319
- if (event) {
2320
- eventHandlers.handleTileHover(tile, event);
2321
- }
2322
- },
2323
- [eventHandlers]
2324
- );
2325
1901
  const cameraConfig = useMemo(() => {
2326
1902
  const size = Math.max(
2327
1903
  gridBounds.maxX - gridBounds.minX,
@@ -2334,65 +1910,45 @@ var GameCanvas3D = forwardRef(
2334
1910
  const d = size * 1;
2335
1911
  switch (cameraMode) {
2336
1912
  case "isometric":
2337
- return {
2338
- position: [cx + d, d * 0.8, cz + d],
2339
- fov: 45
2340
- };
1913
+ return { position: [cx + d, d * 0.8, cz + d], fov: 45 };
2341
1914
  case "top-down":
2342
- return {
2343
- position: [cx, d * 2, cz + d * 0.35],
2344
- fov: 45
2345
- };
1915
+ return { position: [cx, d * 2, cz + d * 0.35], fov: 45 };
2346
1916
  case "follow":
2347
- return {
2348
- position: [cx, d * 0.5, cz + d],
2349
- fov: 45
2350
- };
1917
+ return { position: [cx, d * 0.5, cz + d], fov: 45 };
2351
1918
  case "perspective":
2352
1919
  default:
2353
- return {
2354
- position: [cx + d, d, cz + d],
2355
- fov: 45
2356
- };
1920
+ return { position: [cx + d, d, cz + d], fov: 45 };
2357
1921
  }
2358
1922
  }, [cameraMode, gridBounds]);
2359
- const followTarget = useMemo(() => {
2360
- if (player) {
2361
- return [
2362
- (player.x + (player.width ?? 32) / 2) / pixelsPerUnit,
2363
- (worldHeight - player.y - (player.height ?? 48) / 2) / pixelsPerUnit,
2364
- 0
2365
- ];
2366
- }
2367
- const selected = units.find((u) => u.id === selectedUnitId);
2368
- if (selected) {
2369
- const sx = selected.x ?? selected.position?.x ?? 0;
2370
- const sz = selected.z ?? selected.y ?? selected.position?.y ?? 0;
2371
- return [
2372
- (sx - gridBounds.minX) * gridConfig.cellSize,
2373
- 0,
2374
- (sz - gridBounds.minZ) * gridConfig.cellSize
2375
- ];
2376
- }
1923
+ const followWorld = useMemo(() => {
1924
+ if (followTarget) return drawableProjector.toWorld(followTarget);
2377
1925
  return cameraTarget;
2378
- }, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
2379
- const followOffset = useMemo(() => {
2380
- if (cameraMode === "chase") {
2381
- const selected = units.find((u) => u.id === selectedUnitId);
2382
- const h = selected?.heading ?? 0;
2383
- const dist = 7;
2384
- const height = 4;
2385
- return [-Math.sin(h) * dist, height, -Math.cos(h) * dist];
1926
+ }, [followTarget, drawableProjector, cameraTarget]);
1927
+ const followOffset = useMemo(
1928
+ () => cameraMode === "chase" ? [0, 4, -7] : [5, 7, 5],
1929
+ [cameraMode]
1930
+ );
1931
+ const handleGroundClick = useCallback((e) => {
1932
+ if (!tileClickEvent && !unitClickEvent) return;
1933
+ e.stopPropagation();
1934
+ const cell = {
1935
+ x: Math.round(e.point.x / gridConfig.cellSize + gridBounds.minX),
1936
+ y: Math.round(e.point.z / gridConfig.cellSize + gridBounds.minZ)
1937
+ };
1938
+ const hitId = hitIndex.get(`${cell.x},${cell.y}`);
1939
+ if (hitId !== void 0 && unitClickEvent) {
1940
+ eventBus.emit(`UI:${unitClickEvent}`, { unitId: hitId, x: cell.x, z: cell.y });
1941
+ } else if (tileClickEvent) {
1942
+ eventBus.emit(`UI:${tileClickEvent}`, { x: cell.x, z: cell.y });
2386
1943
  }
2387
- return player ? [0, 2, 9] : [5, 7, 5];
2388
- }, [cameraMode, units, selectedUnitId, player]);
2389
- if (externalLoading || assetsLoading && preloadAssets.length > 0) {
1944
+ }, [tileClickEvent, unitClickEvent, gridConfig, gridBounds, hitIndex, eventBus]);
1945
+ if (externalLoading) {
2390
1946
  return /* @__PURE__ */ jsx(
2391
1947
  Canvas3DLoadingState,
2392
1948
  {
2393
- progress,
2394
- loaded,
2395
- total,
1949
+ progress: 1,
1950
+ loaded: 1,
1951
+ total: 1,
2396
1952
  message: loadingMessage,
2397
1953
  className
2398
1954
  }
@@ -2410,7 +1966,7 @@ var GameCanvas3D = forwardRef(
2410
1966
  {
2411
1967
  onError: (err) => setInternalError(err.message),
2412
1968
  onReset: () => setInternalError(null),
2413
- children: /* @__PURE__ */ jsxs(
1969
+ children: /* @__PURE__ */ jsx(
2414
1970
  "div",
2415
1971
  {
2416
1972
  ref: containerRef,
@@ -2419,195 +1975,103 @@ var GameCanvas3D = forwardRef(
2419
1975
  "data-orientation": orientation,
2420
1976
  "data-camera-mode": cameraMode,
2421
1977
  "data-overlay": overlay,
2422
- children: [
2423
- /* @__PURE__ */ jsxs(
2424
- Canvas,
2425
- {
2426
- shadows,
2427
- camera: {
2428
- position: cameraConfig.position,
2429
- fov: cameraConfig.fov,
2430
- near: 0.1,
2431
- far: 1e3
2432
- },
2433
- style: { background: backgroundColor, position: "absolute", inset: 0 },
2434
- onClick: (e) => {
2435
- if (e.target === e.currentTarget) {
2436
- eventHandlers.handleCanvasClick(e);
1978
+ children: /* @__PURE__ */ jsxs(
1979
+ Canvas,
1980
+ {
1981
+ shadows,
1982
+ camera: {
1983
+ position: cameraConfig.position,
1984
+ fov: cameraConfig.fov,
1985
+ near: 0.1,
1986
+ far: 1e3
1987
+ },
1988
+ style: { background: backgroundColor, position: "absolute", inset: 0 },
1989
+ onClick: (e) => {
1990
+ if (e.target === e.currentTarget) {
1991
+ eventHandlers.handleCanvasClick(e);
1992
+ }
1993
+ },
1994
+ children: [
1995
+ /* @__PURE__ */ jsx(CameraController3D, { onCameraChange: eventHandlers.handleCameraChange }),
1996
+ (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsx(FollowCamera3D, { target: followWorld, offset: followOffset }),
1997
+ /* @__PURE__ */ jsx(
1998
+ Lighting3D,
1999
+ {
2000
+ shadows,
2001
+ shadowBias: -4e-4,
2002
+ shadowNormalBias: 0.04,
2003
+ shadowCameraSize: 5,
2004
+ shadowCameraNear: 0.5,
2005
+ shadowCameraFar: 500
2437
2006
  }
2438
- },
2439
- children: [
2440
- /* @__PURE__ */ jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
2441
- (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsx(
2442
- FollowCamera,
2443
- {
2444
- target: followTarget,
2445
- offset: followOffset
2446
- }
2447
- ),
2448
- /* @__PURE__ */ jsx("ambientLight", { intensity: 0.6 }),
2449
- /* @__PURE__ */ jsx(
2450
- "directionalLight",
2451
- {
2452
- position: [10, 20, 10],
2453
- intensity: 0.8,
2454
- castShadow: shadows,
2455
- "shadow-mapSize": [2048, 2048],
2456
- "shadow-bias": -4e-4,
2457
- "shadow-normalBias": 0.04
2458
- }
2459
- ),
2460
- /* @__PURE__ */ jsx("hemisphereLight", { intensity: 0.3, color: "#87ceeb", groundColor: "#362d1d" }),
2461
- showGrid && /* @__PURE__ */ jsx(
2462
- Grid,
2463
- {
2464
- args: [
2465
- Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
2466
- Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
2467
- ],
2468
- position: [
2469
- (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
2470
- 0,
2471
- (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
2472
- ],
2473
- cellSize: 1,
2474
- cellThickness: 1,
2475
- cellColor: "#444444",
2476
- sectionSize: 5,
2477
- sectionThickness: 1.5,
2478
- sectionColor: "#666666",
2479
- fadeDistance: 50,
2480
- fadeStrength: 1
2481
- }
2482
- ),
2483
- player ? (
2484
- /* Side-scroller mode — LOLO-owned pixel space mapped to world units */
2485
- /* @__PURE__ */ jsx(
2486
- SideScene,
2487
- {
2488
- player,
2489
- platforms,
2490
- worldHeight,
2491
- ppu: pixelsPerUnit,
2492
- playerSprite,
2493
- tileSprites,
2494
- interpolate: interpolateUnits,
2495
- features,
2496
- events,
2497
- assetManifest
2498
- }
2499
- )
2500
- ) : /* @__PURE__ */ jsxs("group", { children: [
2501
- tiles.map((tile, index) => {
2502
- const position = gridToWorld2(
2503
- tile.x,
2504
- tile.z ?? tile.y ?? 0,
2505
- tile.elevation ?? 0
2506
- );
2507
- const key = tile.id ?? `tile-${index}`;
2508
- if (CustomTileRenderer) {
2509
- return /* @__PURE__ */ jsx(CustomTileRenderer, { tile, position }, key);
2510
- }
2511
- return /* @__PURE__ */ jsx(
2512
- DefaultTile,
2513
- {
2514
- tile,
2515
- position,
2516
- model: tile.modelUrl ?? assetManifest?.terrains?.[tile.terrain ?? tile.type ?? ""],
2517
- isSelected: tile.id ? selectedTileIds.includes(tile.id) : false,
2518
- isHovered: hoveredTile?.id === tile.id,
2519
- isValidMove: validMoves.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
2520
- isAttackTarget: attackTargets.some((m) => m.x === tile.x && m.z === (tile.z ?? tile.y ?? 0)),
2521
- onTileClick: handleTileClick,
2522
- onTileHover: handleTileHover
2523
- },
2524
- key
2525
- );
2526
- }),
2527
- features.map((feature, index) => {
2528
- const position = gridToWorld2(
2529
- feature.x,
2530
- feature.z ?? feature.y ?? 0,
2531
- (feature.elevation ?? 0) + TILE_TOP_OFFSET
2532
- );
2533
- const key = feature.id ?? `feature-${index}`;
2534
- if (CustomFeatureRenderer) {
2535
- return /* @__PURE__ */ jsx(CustomFeatureRenderer, { feature, position }, key);
2536
- }
2537
- return /* @__PURE__ */ jsx(
2538
- DefaultFeature,
2539
- {
2540
- feature,
2541
- position,
2542
- model: feature.assetUrl ?? assetManifest?.features?.[feature.type],
2543
- onFeatureClick: handleFeatureClick
2544
- },
2545
- key
2546
- );
2547
- }),
2548
- units.map((unit) => {
2549
- const position = gridToWorld2(
2550
- unit.x ?? unit.position?.x ?? 0,
2551
- unit.z ?? unit.y ?? unit.position?.y ?? 0,
2552
- (unit.elevation ?? 0) + TILE_TOP_OFFSET
2553
- );
2554
- return /* @__PURE__ */ jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: CustomUnitRenderer ? /* @__PURE__ */ jsx(CustomUnitRenderer, { unit, position: [0, 0, 0] }) : /* @__PURE__ */ jsx(
2555
- DefaultUnit,
2007
+ ),
2008
+ showGrid && /* @__PURE__ */ jsx(
2009
+ Grid,
2010
+ {
2011
+ args: [
2012
+ Math.max(gridBounds.maxX - gridBounds.minX + 2, 10),
2013
+ Math.max(gridBounds.maxZ - gridBounds.minZ + 2, 10)
2014
+ ],
2015
+ position: [
2016
+ (gridBounds.maxX - gridBounds.minX) / 2 - 0.5,
2017
+ 0,
2018
+ (gridBounds.maxZ - gridBounds.minZ) / 2 - 0.5
2019
+ ],
2020
+ cellSize: 1,
2021
+ cellThickness: 1,
2022
+ cellColor: GRID_COLORS_3D.cell,
2023
+ sectionSize: 5,
2024
+ sectionThickness: 1.5,
2025
+ sectionColor: GRID_COLORS_3D.section,
2026
+ fadeDistance: 50,
2027
+ fadeStrength: 1
2028
+ }
2029
+ ),
2030
+ drawables && drawables.length > 0 && /* @__PURE__ */ jsx("group", { children: drawables.map((node, i) => /* @__PURE__ */ jsx(Drawable3D, { node, projector: drawableProjector }, i)) }),
2031
+ (tileClickEvent || unitClickEvent) && /* @__PURE__ */ jsxs(
2032
+ "mesh",
2033
+ {
2034
+ rotation: [-Math.PI / 2, 0, 0],
2035
+ position: [
2036
+ (gridBounds.maxX - gridBounds.minX) / 2 * gridConfig.cellSize,
2037
+ 0,
2038
+ (gridBounds.maxZ - gridBounds.minZ) / 2 * gridConfig.cellSize
2039
+ ],
2040
+ onClick: handleGroundClick,
2041
+ children: [
2042
+ /* @__PURE__ */ jsx(
2043
+ "planeGeometry",
2556
2044
  {
2557
- unit,
2558
- position: [0, 0, 0],
2559
- model: unit.modelUrl ?? assetManifest?.units?.[unit.unitType ?? ""],
2560
- isSelected: selectedUnitId === unit.id,
2561
- unitScale,
2562
- cellSize: gridConfig.cellSize,
2563
- resolveUnitFrame,
2564
- onUnitClick: handleUnitClick
2045
+ args: [
2046
+ (gridBounds.maxX - gridBounds.minX + 4) * gridConfig.cellSize,
2047
+ (gridBounds.maxZ - gridBounds.minZ + 4) * gridConfig.cellSize
2048
+ ]
2565
2049
  }
2566
- ) }, unit.id);
2567
- }),
2568
- events.map((ev, i) => {
2569
- const position = gridToWorld2(ev.x, ev.z ?? ev.y ?? 0, 0);
2570
- return /* @__PURE__ */ jsx(
2571
- EventMarker,
2572
- {
2573
- event: ev,
2574
- position: [position[0], position[1] + 1.4, position[2]]
2575
- },
2576
- ev.id ?? `ev-${i}`
2577
- );
2578
- })
2579
- ] }),
2580
- children,
2581
- /* @__PURE__ */ jsx(
2582
- OrbitControls,
2583
- {
2584
- ref: controlsRef,
2585
- enabled: cameraMode !== "follow" && cameraMode !== "chase",
2586
- target: cameraTarget,
2587
- enableDamping: true,
2588
- dampingFactor: 0.05,
2589
- enableZoom: true,
2590
- enablePan: true,
2591
- touches: { ONE: THREE6.TOUCH.ROTATE, TWO: THREE6.TOUCH.DOLLY_PAN },
2592
- minDistance: 2,
2593
- maxDistance: 100,
2594
- maxPolarAngle: Math.PI / 2 - 0.1
2595
- }
2596
- )
2597
- ]
2598
- }
2599
- ),
2600
- showCoordinates && hoveredTile && /* @__PURE__ */ jsxs("div", { className: "game-canvas-3d__coordinates", children: [
2601
- "X: ",
2602
- hoveredTile.x,
2603
- ", Z: ",
2604
- hoveredTile.z ?? hoveredTile.y ?? 0
2605
- ] }),
2606
- showTileInfo && hoveredTile && /* @__PURE__ */ jsxs("div", { className: "game-canvas-3d__tile-info", children: [
2607
- /* @__PURE__ */ jsx("div", { className: "tile-info__type", children: hoveredTile.type }),
2608
- hoveredTile.terrain && /* @__PURE__ */ jsx("div", { className: "tile-info__terrain", children: hoveredTile.terrain })
2609
- ] })
2610
- ]
2050
+ ),
2051
+ /* @__PURE__ */ jsx("meshBasicMaterial", { transparent: true, opacity: 0, depthWrite: false })
2052
+ ]
2053
+ }
2054
+ ),
2055
+ children,
2056
+ /* @__PURE__ */ jsx(
2057
+ OrbitControls,
2058
+ {
2059
+ ref: controlsRef,
2060
+ enabled: cameraMode !== "follow" && cameraMode !== "chase",
2061
+ target: cameraTarget,
2062
+ enableDamping: true,
2063
+ dampingFactor: 0.05,
2064
+ enableZoom: true,
2065
+ enablePan: true,
2066
+ touches: { ONE: THREE6.TOUCH.ROTATE, TWO: THREE6.TOUCH.DOLLY_PAN },
2067
+ minDistance: 2,
2068
+ maxDistance: 100,
2069
+ maxPolarAngle: Math.PI / 2 - 0.1
2070
+ }
2071
+ )
2072
+ ]
2073
+ }
2074
+ )
2611
2075
  }
2612
2076
  )
2613
2077
  }
@@ -2615,1086 +2079,357 @@ var GameCanvas3D = forwardRef(
2615
2079
  }
2616
2080
  );
2617
2081
  GameCanvas3D.displayName = "GameCanvas3D";
2618
- var DEFAULT_FAMILY = "lucide";
2619
- var VALID_FAMILIES = [
2620
- "lucide",
2621
- "phosphor-outline",
2622
- "phosphor-fill",
2623
- "phosphor-duotone",
2624
- "tabler",
2625
- "fa-solid"
2626
- ];
2627
- function getCurrentIconFamily() {
2628
- if (typeof window === "undefined" || typeof document === "undefined") {
2629
- return DEFAULT_FAMILY;
2630
- }
2631
- const raw = getComputedStyle(document.documentElement).getPropertyValue("--icon-family").trim().replace(/^["']|["']$/g, "");
2632
- return VALID_FAMILIES.includes(raw) ? raw : DEFAULT_FAMILY;
2633
- }
2634
- var cachedFamily = null;
2635
- var listeners = /* @__PURE__ */ new Set();
2636
- var observer = null;
2637
- function ensureObserver() {
2638
- if (typeof window === "undefined" || observer) return;
2639
- observer = new MutationObserver(() => {
2640
- const next = getCurrentIconFamily();
2641
- if (next !== cachedFamily) {
2642
- cachedFamily = next;
2643
- listeners.forEach((fn) => fn());
2644
- }
2645
- });
2646
- observer.observe(document.documentElement, {
2647
- attributes: true,
2648
- attributeFilter: ["data-theme", "style"]
2649
- });
2650
- cachedFamily = getCurrentIconFamily();
2651
- }
2652
- function subscribeIconFamily(notify) {
2653
- ensureObserver();
2654
- listeners.add(notify);
2655
- return () => {
2656
- listeners.delete(notify);
2657
- };
2658
- }
2659
- function getIconFamilySnapshot() {
2660
- if (cachedFamily !== null) return cachedFamily;
2661
- cachedFamily = getCurrentIconFamily();
2662
- return cachedFamily;
2663
- }
2664
- function getIconFamilyServerSnapshot() {
2665
- return DEFAULT_FAMILY;
2666
- }
2667
- function useIconFamily() {
2668
- return useSyncExternalStore(
2669
- subscribeIconFamily,
2670
- getIconFamilySnapshot,
2671
- getIconFamilyServerSnapshot
2672
- );
2673
- }
2674
- function kebabToPascal(name) {
2675
- return name.split("-").map((part) => {
2676
- if (/^\d+$/.test(part)) return part;
2677
- return part.charAt(0).toUpperCase() + part.slice(1);
2678
- }).join("");
2679
- }
2680
- var libPromises = /* @__PURE__ */ new Map();
2681
- function loadLib(key, importer) {
2682
- let p = libPromises.get(key);
2683
- if (!p) {
2684
- p = importer();
2685
- libPromises.set(key, p);
2686
- }
2687
- return p;
2688
- }
2689
- function lazyFamilyIcon(libKey, importer, pick, fallbackName, family) {
2690
- const Lazy = React8__default.lazy(async () => {
2691
- const lib = await loadLib(libKey, importer);
2692
- const Comp = pick(lib);
2693
- if (!Comp) {
2694
- warnFallback(fallbackName, family);
2695
- return { default: makeLucideAdapter(fallbackName, true) };
2696
- }
2697
- return { default: Comp };
2698
- });
2699
- const Wrapped = (props) => /* @__PURE__ */ jsx(
2700
- React8__default.Suspense,
2701
- {
2702
- fallback: /* @__PURE__ */ jsx(
2703
- "span",
2704
- {
2705
- "aria-hidden": true,
2706
- className: props.className,
2707
- style: { display: "inline-block", ...props.style }
2708
- }
2709
- ),
2710
- children: /* @__PURE__ */ jsx(Lazy, { ...props })
2711
- }
2712
- );
2713
- Wrapped.displayName = `Lazy.${libKey}.${fallbackName}`;
2714
- return Wrapped;
2715
- }
2716
- var lucideAliases = {
2717
- close: LucideIcons.X,
2718
- trash: LucideIcons.Trash2,
2719
- loader: LucideIcons.Loader2,
2720
- stop: LucideIcons.Square,
2721
- volume: LucideIcons.Volume2,
2722
- "volume-off": LucideIcons.VolumeX,
2723
- refresh: LucideIcons.RefreshCw,
2724
- share: LucideIcons.Share2,
2725
- "sort-asc": LucideIcons.ArrowUpNarrowWide,
2726
- "sort-desc": LucideIcons.ArrowDownNarrowWide
2727
- };
2728
- function isComponentLike(v) {
2729
- return v != null && (typeof v === "function" || typeof v === "object");
2730
- }
2731
- function resolveLucide(name) {
2732
- if (lucideAliases[name]) return lucideAliases[name];
2733
- const pascal = kebabToPascal(name);
2734
- const lucideMap = LucideIcons;
2735
- const direct = lucideMap[pascal];
2736
- if (isComponentLike(direct)) return direct;
2737
- const asIs = lucideMap[name];
2738
- if (isComponentLike(asIs)) return asIs;
2739
- return LucideIcons.HelpCircle;
2740
- }
2741
- var phosphorAliases = {
2742
- // lucide name → phosphor PascalCase name
2743
- // Actions
2744
- plus: "Plus",
2745
- minus: "Minus",
2746
- x: "X",
2747
- check: "Check",
2748
- close: "X",
2749
- edit: "PencilSimple",
2750
- pencil: "PencilSimple",
2751
- trash: "Trash",
2752
- save: "FloppyDisk",
2753
- copy: "Copy",
2754
- share: "Share",
2755
- send: "PaperPlaneRight",
2756
- download: "DownloadSimple",
2757
- upload: "UploadSimple",
2758
- archive: "Archive",
2759
- refresh: "ArrowsClockwise",
2760
- loader: "CircleNotch",
2761
- link: "Link",
2762
- paperclip: "Paperclip",
2763
- // Navigation
2764
- "chevron-down": "CaretDown",
2765
- "chevron-up": "CaretUp",
2766
- "chevron-left": "CaretLeft",
2767
- "chevron-right": "CaretRight",
2768
- "arrow-up": "ArrowUp",
2769
- "arrow-down": "ArrowDown",
2770
- "arrow-left": "ArrowLeft",
2771
- "arrow-right": "ArrowRight",
2772
- menu: "List",
2773
- more: "DotsThree",
2774
- "more-vertical": "DotsThreeVertical",
2775
- "more-horizontal": "DotsThree",
2776
- external: "ArrowSquareOut",
2777
- "external-link": "ArrowSquareOut",
2778
- // Files
2779
- file: "File",
2780
- "file-text": "FileText",
2781
- "file-plus": "FilePlus",
2782
- "file-minus": "FileMinus",
2783
- folder: "Folder",
2784
- "folder-open": "FolderOpen",
2785
- document: "FileText",
2786
- // Charts
2787
- "bar-chart": "ChartBar",
2788
- "bar-chart-2": "ChartBar",
2789
- "bar-chart-3": "ChartBar",
2790
- "line-chart": "ChartLine",
2791
- "pie-chart": "ChartPie",
2792
- activity: "Pulse",
2793
- "trending-up": "TrendUp",
2794
- "trending-down": "TrendDown",
2795
- // Messages
2796
- message: "ChatCircle",
2797
- "message-circle": "ChatCircle",
2798
- "message-square": "ChatText",
2799
- "messages-square": "ChatsCircle",
2800
- comment: "ChatCircle",
2801
- comments: "ChatsCircle",
2802
- inbox: "Tray",
2803
- mail: "Envelope",
2804
- envelope: "Envelope",
2805
- // Status
2806
- "alert-circle": "WarningCircle",
2807
- "alert-triangle": "Warning",
2808
- "check-circle": "CheckCircle",
2809
- "x-circle": "XCircle",
2810
- info: "Info",
2811
- "help-circle": "Question",
2812
- "life-buoy": "Lifebuoy",
2813
- lifebuoy: "Lifebuoy",
2814
- warning: "Warning",
2815
- error: "WarningCircle",
2816
- // Media
2817
- image: "Image",
2818
- video: "VideoCamera",
2819
- film: "FilmStrip",
2820
- camera: "Camera",
2821
- music: "MusicNote",
2822
- play: "Play",
2823
- pause: "Pause",
2824
- stop: "Stop",
2825
- "skip-forward": "SkipForward",
2826
- "skip-back": "SkipBack",
2827
- volume: "SpeakerHigh",
2828
- "volume-2": "SpeakerHigh",
2829
- "volume-x": "SpeakerX",
2830
- mic: "Microphone",
2831
- "mic-off": "MicrophoneSlash",
2832
- // People
2833
- user: "User",
2834
- users: "Users",
2835
- "user-plus": "UserPlus",
2836
- "user-check": "UserCheck",
2837
- // Time
2838
- calendar: "Calendar",
2839
- clock: "Clock",
2840
- timer: "Timer",
2841
- // Location
2842
- map: "MapTrifold",
2843
- "map-pin": "MapPin",
2844
- navigation: "NavigationArrow",
2845
- compass: "Compass",
2846
- globe: "Globe",
2847
- target: "Target",
2848
- // Project / layout
2849
- kanban: "Kanban",
2850
- list: "List",
2851
- table: "Table",
2852
- grid: "GridFour",
2853
- layout: "Layout",
2854
- columns: "Columns",
2855
- rows: "Rows",
2856
- // Misc
2857
- bell: "Bell",
2858
- bookmark: "Bookmark",
2859
- briefcase: "Briefcase",
2860
- flag: "Flag",
2861
- tag: "Tag",
2862
- tags: "Tag",
2863
- star: "Star",
2864
- heart: "Heart",
2865
- home: "House",
2866
- settings: "Gear",
2867
- eye: "Eye",
2868
- "eye-off": "EyeSlash",
2869
- lock: "Lock",
2870
- unlock: "LockOpen",
2871
- key: "Key",
2872
- shield: "Shield",
2873
- search: "MagnifyingGlass",
2874
- filter: "Funnel",
2875
- "sort-asc": "SortAscending",
2876
- "sort-desc": "SortDescending",
2877
- zap: "Lightning",
2878
- sparkles: "Sparkle",
2879
- // Code
2880
- code: "Code",
2881
- terminal: "Terminal",
2882
- database: "Database",
2883
- server: "HardDrives",
2884
- cloud: "Cloud",
2885
- wifi: "WifiHigh",
2886
- package: "Package",
2887
- box: "Package",
2888
- // Theme
2889
- sun: "Sun",
2890
- moon: "Moon",
2891
- // Phone
2892
- phone: "Phone",
2893
- printer: "Printer",
2894
- // Hierarchy
2895
- tree: "Tree",
2896
- network: "Network"
2082
+ var DEFAULT_CONFIG = {
2083
+ cellSize: 1,
2084
+ offsetX: 0,
2085
+ offsetZ: 0,
2086
+ elevation: 0
2897
2087
  };
2898
- function resolvePhosphor(name, weight, family) {
2899
- const target = phosphorAliases[name] ?? kebabToPascal(name);
2900
- return lazyFamilyIcon(
2901
- "phosphor",
2902
- () => import('@phosphor-icons/react'),
2903
- (lib) => {
2904
- const PhosphorComp = lib[target];
2905
- if (!PhosphorComp) return null;
2906
- const Component2 = PhosphorComp;
2907
- const Adapter = (props) => /* @__PURE__ */ jsx(
2908
- Component2,
2909
- {
2910
- weight,
2911
- className: props.className,
2912
- style: props.style,
2913
- size: props.size ?? "1em"
2914
- }
2915
- );
2916
- Adapter.displayName = `Phosphor.${target}.${weight}`;
2917
- return Adapter;
2918
- },
2919
- name,
2920
- family
2088
+ function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
2089
+ const opts = { ...DEFAULT_CONFIG, ...config };
2090
+ return new THREE6.Vector3(
2091
+ gridX * opts.cellSize + opts.offsetX,
2092
+ opts.elevation,
2093
+ gridZ * opts.cellSize + opts.offsetZ
2921
2094
  );
2922
2095
  }
2923
- var tablerAliases = {
2924
- // lucide name tabler suffix (after the `Icon` prefix)
2925
- // Actions
2926
- plus: "Plus",
2927
- minus: "Minus",
2928
- x: "X",
2929
- check: "Check",
2930
- close: "X",
2931
- edit: "Pencil",
2932
- pencil: "Pencil",
2933
- trash: "Trash",
2934
- save: "DeviceFloppy",
2935
- copy: "Copy",
2936
- share: "Share",
2937
- send: "Send",
2938
- download: "Download",
2939
- upload: "Upload",
2940
- archive: "Archive",
2941
- refresh: "Refresh",
2942
- loader: "Loader2",
2943
- link: "Link",
2944
- paperclip: "Paperclip",
2945
- external: "ExternalLink",
2946
- "external-link": "ExternalLink",
2947
- // Navigation
2948
- "chevron-down": "ChevronDown",
2949
- "chevron-up": "ChevronUp",
2950
- "chevron-left": "ChevronLeft",
2951
- "chevron-right": "ChevronRight",
2952
- "arrow-down": "ArrowDown",
2953
- "arrow-up": "ArrowUp",
2954
- "arrow-left": "ArrowLeft",
2955
- "arrow-right": "ArrowRight",
2956
- menu: "Menu2",
2957
- more: "Dots",
2958
- "more-vertical": "DotsVertical",
2959
- // Files
2960
- file: "File",
2961
- "file-text": "FileText",
2962
- "file-plus": "FilePlus",
2963
- "file-check": "FileCheck",
2964
- "file-minus": "FileMinus",
2965
- folder: "Folder",
2966
- "folder-open": "FolderOpen",
2967
- document: "FileText",
2968
- // Charts
2969
- "bar-chart": "ChartBar",
2970
- "bar-chart-2": "ChartBar",
2971
- "bar-chart-3": "ChartBar",
2972
- "line-chart": "ChartLine",
2973
- "pie-chart": "ChartPie",
2974
- activity: "Activity",
2975
- "trending-up": "TrendingUp",
2976
- "trending-down": "TrendingDown",
2977
- // Messages
2978
- message: "Message",
2979
- "message-circle": "MessageCircle",
2980
- "message-square": "Message2",
2981
- "messages-square": "Messages",
2982
- comment: "Message",
2983
- comments: "Messages",
2984
- inbox: "Inbox",
2985
- mail: "Mail",
2986
- envelope: "Mail",
2987
- // Status
2988
- "alert-circle": "AlertCircle",
2989
- "alert-triangle": "AlertTriangle",
2990
- "check-circle": "CircleCheck",
2991
- "x-circle": "CircleX",
2992
- info: "InfoCircle",
2993
- "help-circle": "HelpCircle",
2994
- "life-buoy": "Lifebuoy",
2995
- warning: "AlertTriangle",
2996
- error: "AlertOctagon",
2997
- // Media
2998
- image: "Photo",
2999
- video: "Video",
3000
- camera: "Camera",
3001
- music: "Music",
3002
- play: "PlayerPlay",
3003
- pause: "PlayerPause",
3004
- stop: "PlayerStop",
3005
- "skip-forward": "PlayerSkipForward",
3006
- "skip-back": "PlayerSkipBack",
3007
- volume: "Volume",
3008
- "volume-2": "Volume",
3009
- "volume-x": "VolumeOff",
3010
- mic: "Microphone",
3011
- "mic-off": "MicrophoneOff",
3012
- // People
3013
- user: "User",
3014
- users: "Users",
3015
- "user-plus": "UserPlus",
3016
- "user-check": "UserCheck",
3017
- // Time
3018
- calendar: "Calendar",
3019
- clock: "Clock",
3020
- timer: "Hourglass",
3021
- // Location
3022
- map: "Map",
3023
- "map-pin": "MapPin",
3024
- navigation: "Navigation",
3025
- compass: "Compass",
3026
- globe: "World",
3027
- target: "Target",
3028
- // Project / layout
3029
- kanban: "LayoutKanban",
3030
- list: "List",
3031
- table: "Table",
3032
- grid: "LayoutGrid",
3033
- layout: "Layout",
3034
- columns: "LayoutColumns",
3035
- rows: "LayoutRows",
3036
- // Misc
3037
- bell: "Bell",
3038
- bookmark: "Bookmark",
3039
- briefcase: "Briefcase",
3040
- flag: "Flag",
3041
- tag: "Tag",
3042
- tags: "Tags",
3043
- star: "Star",
3044
- heart: "Heart",
3045
- home: "Home",
3046
- settings: "Settings",
3047
- eye: "Eye",
3048
- "eye-off": "EyeOff",
3049
- lock: "Lock",
3050
- unlock: "LockOpen",
3051
- key: "Key",
3052
- shield: "Shield",
3053
- search: "Search",
3054
- filter: "Filter",
3055
- "sort-asc": "SortAscending",
3056
- "sort-desc": "SortDescending",
3057
- zap: "Bolt",
3058
- sparkles: "Sparkles",
3059
- // Code / data
3060
- code: "Code",
3061
- terminal: "Terminal",
3062
- database: "Database",
3063
- server: "Server",
3064
- cloud: "Cloud",
3065
- wifi: "Wifi",
3066
- package: "Package",
3067
- box: "Box",
3068
- // Theme
3069
- sun: "Sun",
3070
- moon: "Moon",
3071
- // Phone
3072
- phone: "Phone",
3073
- printer: "Printer",
3074
- // Hierarchy
3075
- tree: "Hierarchy",
3076
- network: "Network"
3077
- };
3078
- function resolveTabler(name, family) {
3079
- const suffix = tablerAliases[name] ?? kebabToPascal(name);
3080
- const target = `Icon${suffix}`;
3081
- return lazyFamilyIcon(
3082
- "tabler",
3083
- () => import('@tabler/icons-react'),
3084
- (lib) => {
3085
- const TablerComp = lib[target];
3086
- if (!TablerComp) return null;
3087
- const Component2 = TablerComp;
3088
- const Adapter = (props) => /* @__PURE__ */ jsx(
3089
- Component2,
3090
- {
3091
- stroke: props.strokeWidth ?? 1.5,
3092
- className: props.className,
3093
- style: props.style,
3094
- size: props.size ?? 24
3095
- }
3096
- );
3097
- Adapter.displayName = `Tabler.${target}`;
3098
- return Adapter;
3099
- },
3100
- name,
3101
- family
3102
- );
2096
+ function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
2097
+ const opts = { ...DEFAULT_CONFIG, ...config };
2098
+ return {
2099
+ x: Math.round((worldX - opts.offsetX) / opts.cellSize),
2100
+ z: Math.round((worldZ - opts.offsetZ) / opts.cellSize)
2101
+ };
3103
2102
  }
3104
- var faAliases = {
3105
- // lucide name fa-solid suffix (after the `Fa` prefix).
3106
- // react-icons/fa ships FontAwesome 5 — names like `FaFileText` don't exist
3107
- // (FA renamed to `FaFileAlt`). When you see a console.warn from
3108
- // [iconFamily] about an unmapped lucide name in this family, add the
3109
- // closest FA5 sibling here so the fallback stays in-family.
3110
- search: "Search",
3111
- close: "Times",
3112
- x: "Times",
3113
- loader: "Spinner",
3114
- refresh: "Sync",
3115
- "sort-asc": "SortAmountUp",
3116
- "sort-desc": "SortAmountDown",
3117
- "chevron-down": "ChevronDown",
3118
- "chevron-up": "ChevronUp",
3119
- "chevron-left": "ChevronLeft",
3120
- "chevron-right": "ChevronRight",
3121
- "help-circle": "QuestionCircle",
3122
- "alert-triangle": "ExclamationTriangle",
3123
- "alert-circle": "ExclamationCircle",
3124
- "check-circle": "CheckCircle",
3125
- "x-circle": "TimesCircle",
3126
- edit: "Edit",
3127
- pencil: "PencilAlt",
3128
- trash: "Trash",
3129
- send: "PaperPlane",
3130
- share: "ShareAlt",
3131
- external: "ExternalLinkAlt",
3132
- plus: "Plus",
3133
- minus: "Minus",
3134
- check: "Check",
3135
- star: "Star",
3136
- heart: "Heart",
3137
- home: "Home",
3138
- user: "User",
3139
- users: "Users",
3140
- "user-plus": "UserPlus",
3141
- "user-check": "UserCheck",
3142
- settings: "Cog",
3143
- menu: "Bars",
3144
- "arrow-up": "ArrowUp",
3145
- "arrow-down": "ArrowDown",
3146
- "arrow-left": "ArrowLeft",
3147
- "arrow-right": "ArrowRight",
3148
- copy: "Copy",
3149
- download: "Download",
3150
- upload: "Upload",
3151
- filter: "Filter",
3152
- calendar: "Calendar",
3153
- clock: "Clock",
3154
- bell: "Bell",
3155
- mail: "Envelope",
3156
- envelope: "Envelope",
3157
- lock: "Lock",
3158
- unlock: "LockOpen",
3159
- eye: "Eye",
3160
- "eye-off": "EyeSlash",
3161
- more: "EllipsisH",
3162
- "more-vertical": "EllipsisV",
3163
- info: "InfoCircle",
3164
- warning: "ExclamationTriangle",
3165
- error: "ExclamationCircle",
3166
- // Time
3167
- timer: "Hourglass",
3168
- // Files (FA renamed FileText → FileAlt)
3169
- file: "File",
3170
- "file-text": "FileAlt",
3171
- "file-plus": "FileMedical",
3172
- "file-minus": "FileExcel",
3173
- "file-check": "FileSignature",
3174
- document: "FileAlt",
3175
- // Charts (lucide BarChart2 / BarChart3 → FA ChartBar)
3176
- "bar-chart": "ChartBar",
3177
- "bar-chart-2": "ChartBar",
3178
- "bar-chart-3": "ChartBar",
3179
- "line-chart": "ChartLine",
3180
- "pie-chart": "ChartPie",
3181
- activity: "ChartLine",
3182
- "trending-up": "ChartLine",
3183
- "trending-down": "ChartLine",
3184
- // Messages (lucide MessageCircle/MessageSquare → FA CommentDots/CommentAlt)
3185
- message: "Comment",
3186
- "message-circle": "CommentDots",
3187
- "message-square": "CommentAlt",
3188
- "messages-square": "Comments",
3189
- comment: "Comment",
3190
- comments: "Comments",
3191
- inbox: "Inbox",
3192
- // Support / help
3193
- "life-buoy": "LifeRing",
3194
- lifebuoy: "LifeRing",
3195
- // Project / kanban (FA has no kanban; closest semantic is Tasks/Columns)
3196
- kanban: "Tasks",
3197
- columns: "Columns",
3198
- rows: "Bars",
3199
- layout: "ThLarge",
3200
- grid: "Th",
3201
- list: "List",
3202
- table: "Table",
3203
- // Storage / folders
3204
- folder: "Folder",
3205
- "folder-open": "FolderOpen",
3206
- archive: "Archive",
3207
- bookmark: "Bookmark",
3208
- briefcase: "Briefcase",
3209
- package: "Box",
3210
- box: "Box",
3211
- // Map / location
3212
- map: "Map",
3213
- "map-pin": "MapMarkerAlt",
3214
- navigation: "LocationArrow",
3215
- compass: "Compass",
3216
- globe: "Globe",
3217
- target: "Bullseye",
3218
- // Media
3219
- image: "Image",
3220
- video: "Video",
3221
- film: "Film",
3222
- camera: "Camera",
3223
- music: "Music",
3224
- play: "Play",
3225
- pause: "Pause",
3226
- stop: "Stop",
3227
- "skip-forward": "Forward",
3228
- "skip-back": "Backward",
3229
- volume: "VolumeUp",
3230
- "volume-2": "VolumeUp",
3231
- "volume-x": "VolumeMute",
3232
- mic: "Microphone",
3233
- "mic-off": "MicrophoneSlash",
3234
- phone: "Phone",
3235
- // Code / data
3236
- code: "Code",
3237
- terminal: "Terminal",
3238
- database: "Database",
3239
- server: "Server",
3240
- cloud: "Cloud",
3241
- wifi: "Wifi",
3242
- // Security
3243
- shield: "ShieldAlt",
3244
- key: "Key",
3245
- // Misc actions
3246
- printer: "Print",
3247
- save: "Save",
3248
- link: "Link",
3249
- unlink: "Unlink",
3250
- paperclip: "Paperclip",
3251
- flag: "Flag",
3252
- tag: "Tag",
3253
- tags: "Tags",
3254
- zap: "Bolt",
3255
- sparkles: "Magic",
3256
- // Theme
3257
- sun: "Sun",
3258
- moon: "Moon",
3259
- // Hierarchy (FA has no Tree icon for hierarchies; Sitemap is the org-chart icon)
3260
- tree: "Sitemap",
3261
- network: "NetworkWired"
3262
- };
3263
- function resolveFa(name, family) {
3264
- const suffix = faAliases[name] ?? kebabToPascal(name);
3265
- const target = `Fa${suffix}`;
3266
- return lazyFamilyIcon(
3267
- "fa",
3268
- () => import('react-icons/fa'),
3269
- (lib) => {
3270
- const FaComp = lib[target];
3271
- if (!FaComp) return null;
3272
- const Component2 = FaComp;
3273
- const Adapter = (props) => /* @__PURE__ */ jsx(
3274
- Component2,
3275
- {
3276
- className: props.className,
3277
- style: props.style,
3278
- size: props.size ?? "1em"
3279
- }
3280
- );
3281
- Adapter.displayName = `Fa.${target}`;
3282
- return Adapter;
3283
- },
3284
- name,
3285
- family
3286
- );
2103
+ function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
2104
+ const raycaster = new THREE6.Raycaster();
2105
+ const mouse = new THREE6.Vector2(mouseX, mouseY);
2106
+ raycaster.setFromCamera(mouse, camera);
2107
+ const plane = new THREE6.Plane(new THREE6.Vector3(0, 1, 0), -planeY);
2108
+ const target = new THREE6.Vector3();
2109
+ const intersection = raycaster.ray.intersectPlane(plane, target);
2110
+ return intersection ? target : null;
3287
2111
  }
3288
- var warned = /* @__PURE__ */ new Set();
3289
- function warnFallback(name, family) {
3290
- const key = `${family}::${name}`;
3291
- if (warned.has(key)) return;
3292
- warned.add(key);
3293
- if (typeof console !== "undefined") {
3294
- console.warn(
3295
- `[iconFamily] No '${name}' mapping in family '${family}'; falling back to lucide. Add an alias in lib/iconFamily.ts.`
3296
- );
3297
- }
2112
+ function raycastToObjects(camera, mouseX, mouseY, objects) {
2113
+ const raycaster = new THREE6.Raycaster();
2114
+ const mouse = new THREE6.Vector2(mouseX, mouseY);
2115
+ raycaster.setFromCamera(mouse, camera);
2116
+ const intersects = raycaster.intersectObjects(objects, true);
2117
+ return intersects.length > 0 ? intersects[0] : null;
3298
2118
  }
3299
- function makeLucideAdapter(name, isFallback = false) {
3300
- const LucideComp = resolveLucide(name);
3301
- const Adapter = (props) => {
3302
- const stroke = props.strokeWidth ?? (isFallback ? 2 : void 0);
3303
- const style = isFallback ? { ...props.style ?? {}, strokeWidth: stroke ?? 2 } : props.style;
3304
- return /* @__PURE__ */ jsx(
3305
- LucideComp,
3306
- {
3307
- className: props.className,
3308
- strokeWidth: stroke,
3309
- style,
3310
- size: props.size
3311
- }
3312
- );
3313
- };
3314
- Adapter.displayName = `Lucide.${name}${isFallback ? ".fallback" : ""}`;
3315
- return Adapter;
2119
+ function gridDistance(a, b) {
2120
+ const dx = b.x - a.x;
2121
+ const dz = b.z - a.z;
2122
+ return Math.sqrt(dx * dx + dz * dz);
3316
2123
  }
3317
- function resolveIconForFamily(name, family) {
3318
- switch (family) {
3319
- case "lucide":
3320
- return makeLucideAdapter(name, false);
3321
- // Non-lucide families resolve to a lazy, Suspense-wrapped component that
3322
- // dynamic-imports the library on first render and falls back to lucide
3323
- // internally when the family lacks the icon (see lazyFamilyIcon).
3324
- case "phosphor-outline":
3325
- return resolvePhosphor(name, "regular", family);
3326
- case "phosphor-fill":
3327
- return resolvePhosphor(name, "fill", family);
3328
- case "phosphor-duotone":
3329
- return resolvePhosphor(name, "duotone", family);
3330
- case "tabler":
3331
- return resolveTabler(name, family);
3332
- case "fa-solid":
3333
- return resolveFa(name, family);
3334
- }
2124
+ function gridManhattanDistance(a, b) {
2125
+ return Math.abs(b.x - a.x) + Math.abs(b.z - a.z);
3335
2126
  }
3336
- var colorTokenClasses = {
3337
- primary: "text-primary",
3338
- secondary: "text-secondary",
3339
- success: "text-success",
3340
- warning: "text-warning",
3341
- error: "text-error",
3342
- muted: "text-muted-foreground"
3343
- };
3344
- var sizeClasses = {
3345
- xs: "w-3 h-3",
3346
- sm: "w-4 h-4",
3347
- md: "h-icon-default w-icon-default",
3348
- lg: "w-6 h-6",
3349
- xl: "w-8 h-8"
3350
- };
3351
- var animationClasses = {
3352
- none: "",
3353
- spin: "animate-spin",
3354
- pulse: "animate-pulse"
3355
- };
3356
- var Icon = ({
3357
- icon,
3358
- name,
3359
- size = "md",
3360
- color,
3361
- animation = "none",
3362
- className,
3363
- strokeWidth,
3364
- style
3365
- }) => {
3366
- const directIcon = typeof icon === "string" ? void 0 : icon;
3367
- const effectiveName = typeof icon === "string" ? icon : name;
3368
- const family = useIconFamily();
3369
- const RenderedComponent = React8__default.useMemo(() => {
3370
- if (directIcon) return null;
3371
- return effectiveName ? resolveIconForFamily(effectiveName, family) : null;
3372
- }, [directIcon, effectiveName, family]);
3373
- const effectiveStrokeWidth = strokeWidth ?? void 0;
3374
- const inlineStyle = {
3375
- ...effectiveStrokeWidth === void 0 ? { strokeWidth: "var(--icon-stroke-width, 2)" } : {},
3376
- ...style
3377
- };
3378
- const resolvedColor = color ? color in colorTokenClasses ? colorTokenClasses[color] : color : "text-current";
3379
- const composedClassName = cn(
3380
- sizeClasses[size],
3381
- animationClasses[animation],
3382
- resolvedColor,
3383
- className
3384
- );
3385
- if (directIcon) {
3386
- const Direct = directIcon;
3387
- return /* @__PURE__ */ jsx(
3388
- Direct,
3389
- {
3390
- className: composedClassName,
3391
- strokeWidth: effectiveStrokeWidth,
3392
- style: inlineStyle
3393
- }
3394
- );
3395
- }
3396
- if (RenderedComponent) {
3397
- return /* @__PURE__ */ jsx(
3398
- RenderedComponent,
3399
- {
3400
- className: composedClassName,
3401
- strokeWidth: effectiveStrokeWidth,
3402
- style: inlineStyle
3403
- }
2127
+ function getNeighbors(x, z, includeDiagonal = false) {
2128
+ const neighbors = [
2129
+ { x: x + 1, z },
2130
+ { x: x - 1, z },
2131
+ { x, z: z + 1 },
2132
+ { x, z: z - 1 }
2133
+ ];
2134
+ if (includeDiagonal) {
2135
+ neighbors.push(
2136
+ { x: x + 1, z: z + 1 },
2137
+ { x: x - 1, z: z - 1 },
2138
+ { x: x + 1, z: z - 1 },
2139
+ { x: x - 1, z: z + 1 }
3404
2140
  );
3405
2141
  }
3406
- const Fallback = LucideIcons.HelpCircle;
3407
- return /* @__PURE__ */ jsx(
3408
- Fallback,
3409
- {
3410
- className: composedClassName,
3411
- strokeWidth: effectiveStrokeWidth,
3412
- style: inlineStyle
3413
- }
3414
- );
3415
- };
3416
- Icon.displayName = "Icon";
3417
-
3418
- // lib/atlasSlice.ts
3419
- var atlasCache = /* @__PURE__ */ new Map();
3420
- function isTilesheet(a) {
3421
- return typeof a.tileWidth === "number";
2142
+ return neighbors;
3422
2143
  }
3423
- function getAtlas(url, onReady) {
3424
- if (atlasCache.has(url)) return atlasCache.get(url) ?? void 0;
3425
- atlasCache.set(url, void 0);
3426
- fetch(url).then((r) => r.json()).then((json) => {
3427
- atlasCache.set(url, json);
3428
- onReady();
3429
- }).catch(() => {
3430
- atlasCache.set(url, null);
3431
- });
3432
- return void 0;
2144
+ function isInBounds(x, z, bounds) {
2145
+ return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
3433
2146
  }
3434
- function subRectFor(atlas, sprite) {
3435
- if (isTilesheet(atlas)) {
3436
- let col;
3437
- let row;
3438
- if (sprite.includes(",")) {
3439
- const [c, r] = sprite.split(",").map((n) => Number(n.trim()));
3440
- col = c;
3441
- row = r;
3442
- } else {
3443
- const i = Number(sprite);
3444
- if (!Number.isFinite(i)) return null;
3445
- col = i % atlas.columns;
3446
- row = Math.floor(i / atlas.columns);
2147
+ function getCellsInRadius(centerX, centerZ, radius) {
2148
+ const cells = [];
2149
+ const radiusSquared = radius * radius;
2150
+ const minX = Math.floor(centerX - radius);
2151
+ const maxX = Math.ceil(centerX + radius);
2152
+ const minZ = Math.floor(centerZ - radius);
2153
+ const maxZ = Math.ceil(centerZ + radius);
2154
+ for (let x = minX; x <= maxX; x++) {
2155
+ for (let z = minZ; z <= maxZ; z++) {
2156
+ const dx = x - centerX;
2157
+ const dz = z - centerZ;
2158
+ if (dx * dx + dz * dz <= radiusSquared) {
2159
+ cells.push({ x, z });
2160
+ }
3447
2161
  }
3448
- if (!Number.isFinite(col) || !Number.isFinite(row) || col < 0 || row < 0 || col >= atlas.columns || row >= atlas.rows) return null;
3449
- const margin = atlas.margin ?? 0;
3450
- const spacing = atlas.spacing ?? 0;
3451
- return {
3452
- sx: margin + col * (atlas.tileWidth + spacing),
3453
- sy: margin + row * (atlas.tileHeight + spacing),
3454
- sw: atlas.tileWidth,
3455
- sh: atlas.tileHeight
3456
- };
3457
2162
  }
3458
- const st = atlas.subTextures[sprite];
3459
- if (!st) return null;
3460
- return { sx: st.x, sy: st.y, sw: st.width, sh: st.height };
3461
- }
3462
- function isAtlasAsset(asset) {
3463
- return !!asset && typeof asset.atlas === "string" && typeof asset.sprite === "string";
2163
+ return cells;
3464
2164
  }
3465
- var imageCache = /* @__PURE__ */ new Map();
3466
- var imageWaiters = /* @__PURE__ */ new Map();
3467
- function getSheetImage(url, onReady) {
3468
- const cached = imageCache.get(url);
3469
- if (cached) return cached;
3470
- (imageWaiters.get(url) ?? imageWaiters.set(url, /* @__PURE__ */ new Set()).get(url)).add(onReady);
3471
- if (!imageCache.has(url)) {
3472
- imageCache.set(url, null);
3473
- const img = new Image();
3474
- img.crossOrigin = "anonymous";
3475
- img.onload = () => {
3476
- imageCache.set(url, img);
3477
- imageWaiters.get(url)?.forEach((fn) => fn());
3478
- imageWaiters.delete(url);
3479
- };
3480
- img.src = url;
3481
- }
3482
- return null;
2165
+ function createGridHighlight(color = 16776960, opacity = 0.3) {
2166
+ const geometry = new THREE6.PlaneGeometry(0.95, 0.95);
2167
+ const material = new THREE6.MeshBasicMaterial({
2168
+ color,
2169
+ transparent: true,
2170
+ opacity,
2171
+ side: THREE6.DoubleSide
2172
+ });
2173
+ const mesh = new THREE6.Mesh(geometry, material);
2174
+ mesh.rotation.x = -Math.PI / 2;
2175
+ mesh.position.y = 0.01;
2176
+ return mesh;
3483
2177
  }
3484
- function AtlasImage({
3485
- asset,
3486
- size,
3487
- width,
3488
- height,
3489
- fill = false,
3490
- fit = "contain",
3491
- alt,
3492
- className,
3493
- style,
3494
- "aria-hidden": ariaHidden
3495
- }) {
3496
- const [, bump] = React8.useReducer((x) => x + 1, 0);
3497
- const canvasRef = React8.useRef(null);
3498
- const sliced = isAtlasAsset(asset);
3499
- const atlas = sliced ? getAtlas(asset.atlas, bump) : void 0;
3500
- const img = sliced && asset?.url ? getSheetImage(asset.url, bump) : null;
3501
- const rect = sliced && atlas ? subRectFor(atlas, asset.sprite) : null;
3502
- React8.useEffect(() => {
3503
- const canvas = canvasRef.current;
3504
- if (!canvas || !img || !rect) return;
3505
- canvas.width = rect.sw;
3506
- canvas.height = rect.sh;
3507
- const ctx = canvas.getContext("2d");
3508
- if (!ctx) return;
3509
- ctx.imageSmoothingEnabled = false;
3510
- ctx.clearRect(0, 0, rect.sw, rect.sh);
3511
- ctx.drawImage(img, rect.sx, rect.sy, rect.sw, rect.sh, 0, 0, rect.sw, rect.sh);
3512
- }, [img, rect?.sx, rect?.sy, rect?.sw, rect?.sh]);
3513
- const w = fill ? "100%" : width ?? size;
3514
- const h = fill ? "100%" : height ?? size;
3515
- const boxStyle = {
3516
- ...fill ? { position: "absolute", inset: 0 } : {},
3517
- width: w,
3518
- height: h,
3519
- objectFit: fit,
3520
- imageRendering: "pixelated",
3521
- ...style
2178
+ function normalizeMouseCoordinates(clientX, clientY, element) {
2179
+ const rect = element.getBoundingClientRect();
2180
+ return {
2181
+ x: (clientX - rect.left) / rect.width * 2 - 1,
2182
+ y: -((clientY - rect.top) / rect.height) * 2 + 1
3522
2183
  };
3523
- if (!asset?.url) return null;
3524
- if (sliced) {
3525
- if (!rect || !img) {
3526
- return /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: cn("inline-block flex-shrink-0", className), style: { ...boxStyle, objectFit: void 0 } });
3527
- }
3528
- return /* @__PURE__ */ jsx(
3529
- "canvas",
3530
- {
3531
- ref: canvasRef,
3532
- role: ariaHidden ? void 0 : "img",
3533
- "aria-hidden": ariaHidden,
3534
- "aria-label": ariaHidden ? void 0 : alt ?? asset.name ?? asset.category ?? "",
3535
- className: cn("flex-shrink-0", className),
3536
- style: boxStyle
3537
- }
3538
- );
3539
- }
3540
- return /* @__PURE__ */ jsx(
3541
- "img",
3542
- {
3543
- src: asset.url,
3544
- alt: alt ?? asset.name ?? asset.category ?? "",
3545
- "aria-hidden": ariaHidden,
3546
- ...typeof w === "number" ? { width: w } : {},
3547
- ...typeof h === "number" ? { height: h } : {},
3548
- className: cn("flex-shrink-0", className),
3549
- style: boxStyle
3550
- }
2184
+ }
2185
+ function useTapReveal(options = {}) {
2186
+ const { onReveal, onDismiss, refs, enabled = true } = options;
2187
+ const [revealed, setRevealed] = useState(false);
2188
+ const onRevealRef = useRef(onReveal);
2189
+ const onDismissRef = useRef(onDismiss);
2190
+ onRevealRef.current = onReveal;
2191
+ onDismissRef.current = onDismiss;
2192
+ const reveal = useCallback(() => {
2193
+ setRevealed((wasRevealed) => {
2194
+ if (!wasRevealed) onRevealRef.current?.();
2195
+ return true;
2196
+ });
2197
+ }, []);
2198
+ const dismiss = useCallback(() => {
2199
+ setRevealed((wasRevealed) => {
2200
+ if (wasRevealed) onDismissRef.current?.();
2201
+ return false;
2202
+ });
2203
+ }, []);
2204
+ const onPointerDown = useCallback(
2205
+ (e) => {
2206
+ if (!enabled || e.pointerType === "mouse") return;
2207
+ if (revealed) dismiss();
2208
+ else reveal();
2209
+ },
2210
+ [enabled, revealed, reveal, dismiss]
3551
2211
  );
2212
+ useEffect(() => {
2213
+ if (!revealed || typeof document === "undefined") return;
2214
+ const onDocDown = (ev) => {
2215
+ const target = ev.target;
2216
+ if (!(target instanceof Node)) return;
2217
+ const inside = (refs ?? []).some((r) => r.current?.contains(target));
2218
+ if (!inside) dismiss();
2219
+ };
2220
+ const id = setTimeout(() => document.addEventListener("pointerdown", onDocDown), 0);
2221
+ return () => {
2222
+ clearTimeout(id);
2223
+ document.removeEventListener("pointerdown", onDocDown);
2224
+ };
2225
+ }, [revealed, refs, dismiss]);
2226
+ return { revealed, triggerProps: { onPointerDown }, reveal, dismiss };
3552
2227
  }
3553
- AtlasImage.displayName = "AtlasImage";
3554
- var variantStyles = {
3555
- primary: [
3556
- "bg-primary text-primary-foreground",
3557
- "border-none",
3558
- "shadow-sm",
3559
- "hover:bg-primary-hover hover:shadow-lg",
3560
- "active:scale-[var(--active-scale)] active:shadow-sm"
3561
- ].join(" "),
3562
- secondary: [
3563
- "bg-transparent text-accent",
3564
- "border border-accent",
3565
- "hover:bg-accent hover:text-white hover:border-accent",
3566
- "active:scale-[var(--active-scale)]"
3567
- ].join(" "),
3568
- ghost: [
3569
- "bg-transparent text-muted-foreground",
3570
- "border border-transparent",
3571
- "hover:text-primary-foreground hover:bg-primary hover:border-primary",
3572
- "active:scale-[var(--active-scale)]"
3573
- ].join(" "),
3574
- danger: [
3575
- "bg-surface text-error",
3576
- "border-[length:var(--border-width)] border-error",
3577
- "shadow-sm",
3578
- "hover:bg-error hover:text-error-foreground hover:shadow-lg",
3579
- "active:scale-[var(--active-scale)] active:shadow-sm"
3580
- ].join(" "),
3581
- success: [
3582
- "bg-surface text-success",
3583
- "border-[length:var(--border-width)] border-success",
3584
- "shadow-sm",
3585
- "hover:bg-success hover:text-success-foreground hover:shadow-lg",
3586
- "active:scale-[var(--active-scale)] active:shadow-sm"
3587
- ].join(" "),
3588
- warning: [
3589
- "bg-surface text-warning",
3590
- "border-[length:var(--border-width)] border-warning",
3591
- "shadow-sm",
3592
- "hover:bg-warning hover:text-warning-foreground hover:shadow-lg",
3593
- "active:scale-[var(--active-scale)] active:shadow-sm"
3594
- ].join(" "),
3595
- // "default" is an alias for secondary
3596
- default: [
3597
- "bg-secondary text-secondary-foreground",
3598
- "border-[length:var(--border-width-thin)] border-border",
3599
- "hover:bg-secondary-hover",
3600
- "active:scale-[var(--active-scale)]"
3601
- ].join(" ")
2228
+
2229
+ // components/core/atoms/Box.tsx
2230
+ var paddingStyles = {
2231
+ none: "p-0",
2232
+ xs: "p-1",
2233
+ sm: "p-2",
2234
+ md: "p-4",
2235
+ lg: "p-6",
2236
+ xl: "p-8",
2237
+ "2xl": "p-12"
3602
2238
  };
3603
- variantStyles.destructive = variantStyles.danger;
3604
- var sizeStyles = {
3605
- sm: "h-button-sm px-3 text-sm",
3606
- md: "h-button-md px-4 text-sm",
3607
- lg: "h-button-lg px-6 text-base"
2239
+ var paddingXStyles = {
2240
+ none: "px-0",
2241
+ xs: "px-1",
2242
+ sm: "px-2",
2243
+ md: "px-4",
2244
+ lg: "px-6",
2245
+ xl: "px-8",
2246
+ "2xl": "px-12"
3608
2247
  };
3609
- var iconSizeStyles = {
3610
- sm: "h-icon-default w-icon-default",
3611
- md: "h-icon-default w-icon-default",
3612
- lg: "h-icon-default w-icon-default"
2248
+ var paddingYStyles = {
2249
+ none: "py-0",
2250
+ xs: "py-1",
2251
+ sm: "py-2",
2252
+ md: "py-4",
2253
+ lg: "py-6",
2254
+ xl: "py-8",
2255
+ "2xl": "py-12"
3613
2256
  };
3614
- function isIconLike(v) {
3615
- return typeof v.render === "function";
3616
- }
3617
- function resolveIconProp(value, sizeClass) {
3618
- if (!value) return null;
3619
- if (typeof value === "string") {
3620
- return /* @__PURE__ */ jsx(Icon, { name: value, className: sizeClass });
3621
- }
3622
- if (typeof value === "function") {
3623
- const IconComp = value;
3624
- return /* @__PURE__ */ jsx(IconComp, { className: sizeClass });
3625
- }
3626
- if (React8__default.isValidElement(value)) {
3627
- return value;
3628
- }
3629
- if (typeof value === "object" && value !== null && isIconLike(value)) {
3630
- const IconComp = value;
3631
- return /* @__PURE__ */ jsx(IconComp, { className: sizeClass });
3632
- }
3633
- return value;
3634
- }
3635
- var Button = React8__default.forwardRef(
3636
- ({
3637
- className,
3638
- variant = "primary",
3639
- size = "md",
3640
- isLoading = false,
3641
- disabled,
3642
- leftIcon,
3643
- rightIcon,
3644
- icon: iconProp,
3645
- iconRight: iconRightProp,
3646
- iconAsset,
3647
- action,
3648
- actionPayload,
3649
- label,
3650
- children,
3651
- onClick,
3652
- "data-testid": dataTestId,
3653
- ...props
3654
- }, ref) => {
3655
- const eventBus = useEventBus();
3656
- const leftIconValue = leftIcon || iconProp;
3657
- const rightIconValue = rightIcon || iconRightProp;
3658
- const px = size === "sm" ? 16 : size === "lg" ? 20 : 16;
3659
- const resolvedLeftIcon = iconAsset?.url ? /* @__PURE__ */ jsx(AtlasImage, { asset: iconAsset, size: px, alt: iconAsset.name ?? iconAsset.category ?? "", className: "flex-shrink-0" }) : resolveIconProp(leftIconValue, iconSizeStyles[size]);
3660
- const resolvedRightIcon = resolveIconProp(rightIconValue, iconSizeStyles[size]);
3661
- const handleClick = (e) => {
3662
- if (action) {
3663
- eventBus.emit(`UI:${action}`, actionPayload ?? {});
2257
+ var marginStyles = {
2258
+ none: "m-0",
2259
+ xs: "m-1",
2260
+ sm: "m-2",
2261
+ md: "m-4",
2262
+ lg: "m-6",
2263
+ xl: "m-8",
2264
+ "2xl": "m-12",
2265
+ auto: "m-auto"
2266
+ };
2267
+ var marginXStyles = {
2268
+ none: "mx-0",
2269
+ xs: "mx-1",
2270
+ sm: "mx-2",
2271
+ md: "mx-4",
2272
+ lg: "mx-6",
2273
+ xl: "mx-8",
2274
+ "2xl": "mx-12",
2275
+ auto: "mx-auto"
2276
+ };
2277
+ var marginYStyles = {
2278
+ none: "my-0",
2279
+ xs: "my-1",
2280
+ sm: "my-2",
2281
+ md: "my-4",
2282
+ lg: "my-6",
2283
+ xl: "my-8",
2284
+ "2xl": "my-12",
2285
+ auto: "my-auto"
2286
+ };
2287
+ var bgStyles = {
2288
+ transparent: "bg-transparent",
2289
+ primary: "bg-primary text-primary-foreground",
2290
+ secondary: "bg-secondary text-secondary-foreground",
2291
+ muted: "bg-muted text-foreground",
2292
+ accent: "bg-accent text-accent-foreground",
2293
+ surface: "bg-card",
2294
+ overlay: "bg-card/80 backdrop-blur-sm"
2295
+ };
2296
+ var roundedStyles = {
2297
+ none: "rounded-none",
2298
+ sm: "rounded-sm",
2299
+ md: "rounded-md",
2300
+ lg: "rounded-lg",
2301
+ xl: "rounded-xl",
2302
+ "2xl": "rounded-xl",
2303
+ full: "rounded-full"
2304
+ };
2305
+ var shadowStyles = {
2306
+ none: "shadow-none",
2307
+ sm: "shadow-sm",
2308
+ md: "shadow",
2309
+ lg: "shadow-lg",
2310
+ xl: "shadow-lg"
2311
+ };
2312
+ var displayStyles = {
2313
+ block: "block",
2314
+ inline: "inline",
2315
+ "inline-block": "inline-block",
2316
+ flex: "flex",
2317
+ "inline-flex": "inline-flex",
2318
+ grid: "grid"
2319
+ };
2320
+ var overflowStyles = {
2321
+ auto: "overflow-auto",
2322
+ hidden: "overflow-hidden",
2323
+ visible: "overflow-visible",
2324
+ scroll: "overflow-scroll"
2325
+ };
2326
+ var positionStyles = {
2327
+ relative: "relative",
2328
+ absolute: "absolute",
2329
+ fixed: "fixed",
2330
+ sticky: "sticky"
2331
+ };
2332
+ var Box = React7.forwardRef(
2333
+ ({
2334
+ padding,
2335
+ paddingX,
2336
+ paddingY,
2337
+ margin,
2338
+ marginX,
2339
+ marginY,
2340
+ bg = "transparent",
2341
+ border = false,
2342
+ rounded = "none",
2343
+ shadow = "none",
2344
+ display,
2345
+ fullWidth = false,
2346
+ fullHeight = false,
2347
+ overflow,
2348
+ position,
2349
+ className,
2350
+ children,
2351
+ as: Component2 = "div",
2352
+ action,
2353
+ actionPayload,
2354
+ hoverEvent,
2355
+ tapReveal = true,
2356
+ maxWidth,
2357
+ onClick,
2358
+ onMouseEnter,
2359
+ onMouseLeave,
2360
+ onPointerDown,
2361
+ ...rest
2362
+ }, ref) => {
2363
+ const eventBus = useEventBus();
2364
+ const handleClick = useCallback((e) => {
2365
+ if (action) {
2366
+ e.stopPropagation();
2367
+ eventBus.emit(`UI:${action}`, actionPayload ?? {});
3664
2368
  }
3665
2369
  onClick?.(e);
3666
- };
3667
- return /* @__PURE__ */ jsxs(
3668
- "button",
2370
+ }, [action, actionPayload, eventBus, onClick]);
2371
+ const handleMouseEnter = useCallback((e) => {
2372
+ if (hoverEvent) {
2373
+ eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
2374
+ }
2375
+ onMouseEnter?.(e);
2376
+ }, [hoverEvent, eventBus, onMouseEnter]);
2377
+ const handleMouseLeave = useCallback((e) => {
2378
+ if (hoverEvent) {
2379
+ eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
2380
+ }
2381
+ onMouseLeave?.(e);
2382
+ }, [hoverEvent, eventBus, onMouseLeave]);
2383
+ const { triggerProps } = useTapReveal({
2384
+ enabled: tapReveal && !!hoverEvent,
2385
+ onReveal: useCallback(() => {
2386
+ if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
2387
+ }, [hoverEvent, eventBus]),
2388
+ onDismiss: useCallback(() => {
2389
+ if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
2390
+ }, [hoverEvent, eventBus])
2391
+ });
2392
+ const handlePointerDown = useCallback((e) => {
2393
+ if (hoverEvent && tapReveal) triggerProps.onPointerDown(e);
2394
+ onPointerDown?.(e);
2395
+ }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
2396
+ const isClickable = action || onClick;
2397
+ return React7.createElement(
2398
+ Component2,
3669
2399
  {
3670
2400
  ref,
3671
- disabled: disabled || isLoading,
3672
2401
  className: cn(
3673
- "inline-flex items-center justify-center gap-2",
3674
- "font-medium",
3675
- "rounded-sm",
3676
- "cursor-pointer",
3677
- "transition-all duration-[var(--transition-normal)]",
3678
- "focus:outline-none focus:ring-[length:var(--focus-ring-width)] focus:ring-ring focus:ring-offset-[length:var(--focus-ring-offset)]",
3679
- "disabled:opacity-50 disabled:cursor-not-allowed",
3680
- variantStyles[variant],
3681
- sizeStyles[size],
2402
+ padding && paddingStyles[padding],
2403
+ paddingX && paddingXStyles[paddingX],
2404
+ paddingY && paddingYStyles[paddingY],
2405
+ margin && marginStyles[margin],
2406
+ marginX && marginXStyles[marginX],
2407
+ marginY && marginYStyles[marginY],
2408
+ bgStyles[bg],
2409
+ border && "border-[length:var(--border-width)] border-border",
2410
+ roundedStyles[rounded],
2411
+ shadowStyles[shadow],
2412
+ display && displayStyles[display],
2413
+ fullWidth && "w-full",
2414
+ fullHeight && "h-full",
2415
+ overflow && overflowStyles[overflow],
2416
+ position && positionStyles[position],
2417
+ isClickable && "cursor-pointer",
3682
2418
  className
3683
2419
  ),
3684
- onClick: handleClick,
3685
- ...props,
3686
- "data-testid": dataTestId ?? (action ? `action-${action}` : void 0),
3687
- children: [
3688
- isLoading ? /* @__PURE__ */ jsx(Loader2, { className: "h-icon-default w-icon-default animate-spin" }) : resolvedLeftIcon && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedLeftIcon }),
3689
- children || label,
3690
- resolvedRightIcon && !isLoading && /* @__PURE__ */ jsx("span", { className: "flex-shrink-0", children: resolvedRightIcon })
3691
- ]
3692
- }
2420
+ onClick: isClickable ? handleClick : void 0,
2421
+ onMouseEnter: hoverEvent || onMouseEnter ? handleMouseEnter : void 0,
2422
+ onMouseLeave: hoverEvent || onMouseLeave ? handleMouseLeave : void 0,
2423
+ onPointerDown: hoverEvent && tapReveal || onPointerDown ? handlePointerDown : void 0,
2424
+ style: maxWidth ? { maxWidth, ...rest.style } : rest.style,
2425
+ ...rest
2426
+ },
2427
+ children
3693
2428
  );
3694
2429
  }
3695
2430
  );
3696
- Button.displayName = "Button";
3697
- var variantStyles2 = {
2431
+ Box.displayName = "Box";
2432
+ var variantStyles = {
3698
2433
  h1: "text-4xl font-bold tracking-tight text-foreground",
3699
2434
  h2: "text-3xl font-bold tracking-tight text-foreground",
3700
2435
  h3: "text-2xl font-bold text-foreground",
@@ -3755,7 +2490,7 @@ var typographySizeStyles = {
3755
2490
  "2xl": "text-2xl",
3756
2491
  "3xl": "text-3xl"
3757
2492
  };
3758
- var overflowStyles = {
2493
+ var overflowStyles2 = {
3759
2494
  visible: "overflow-visible",
3760
2495
  hidden: "overflow-hidden",
3761
2496
  wrap: "break-words overflow-hidden",
@@ -3780,18 +2515,18 @@ var Typography = ({
3780
2515
  }) => {
3781
2516
  const variant = variantProp ?? (level ? `h${level}` : "body1");
3782
2517
  const Component2 = as || defaultElements[variant];
3783
- return React8__default.createElement(
2518
+ return React7.createElement(
3784
2519
  Component2,
3785
2520
  {
3786
2521
  id,
3787
2522
  className: cn(
3788
- variantStyles2[variant],
2523
+ variantStyles[variant],
3789
2524
  colorStyles[color],
3790
2525
  weight && weightStyles[weight],
3791
2526
  size && typographySizeStyles[size],
3792
2527
  align && `text-${align}`,
3793
2528
  truncate && "truncate overflow-hidden text-ellipsis",
3794
- overflow && overflowStyles[overflow],
2529
+ overflow && overflowStyles2[overflow],
3795
2530
  className
3796
2531
  ),
3797
2532
  style
@@ -3853,7 +2588,7 @@ var Stack = ({
3853
2588
  };
3854
2589
  const isHorizontal = direction === "horizontal";
3855
2590
  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";
3856
- return React8__default.createElement(
2591
+ return React7.createElement(
3857
2592
  Component2,
3858
2593
  {
3859
2594
  className: cn(
@@ -3878,939 +2613,6 @@ var Stack = ({
3878
2613
  var VStack = (props) => /* @__PURE__ */ jsx(Stack, { direction: "vertical", ...props });
3879
2614
  var HStack = (props) => /* @__PURE__ */ jsx(Stack, { direction: "horizontal", ...props });
3880
2615
 
3881
- // components/game/shared/boardEntity.ts
3882
- function boardEntity(entity) {
3883
- if (!entity) return void 0;
3884
- return Array.isArray(entity) ? entity[0] : entity;
3885
- }
3886
- function str(v) {
3887
- return v == null ? "" : String(v);
3888
- }
3889
- function num(v, fallback = 0) {
3890
- const n = Number(v);
3891
- return Number.isFinite(n) ? n : fallback;
3892
- }
3893
- function rows(v) {
3894
- return Array.isArray(v) ? v : [];
3895
- }
3896
- function vec2(v) {
3897
- const o = v ?? {};
3898
- return { x: num(o.x), y: num(o.y) };
3899
- }
3900
- function unitPosition(u) {
3901
- return vec2(u.position);
3902
- }
3903
- function unitTeam(u) {
3904
- return str(u.team);
3905
- }
3906
- function unitHealth(u) {
3907
- return num(u.health);
3908
- }
3909
- function GameBoard3D({
3910
- entity,
3911
- tiles = [],
3912
- features = [],
3913
- cameraMode = "perspective",
3914
- backgroundColor = "#2a1a1a",
3915
- unitScale = 1,
3916
- scale = 0.45,
3917
- tileClickEvent,
3918
- unitClickEvent,
3919
- attackEvent,
3920
- endTurnEvent,
3921
- cancelEvent,
3922
- playAgainEvent,
3923
- gameEndEvent,
3924
- className
3925
- }) {
3926
- const row = boardEntity(entity);
3927
- const eventBus = useEventBus();
3928
- const entityUnits = row ? rows(row.units) : [];
3929
- const units = entityUnits.map((r) => ({
3930
- id: str(r.id),
3931
- x: num(r.x),
3932
- y: num(r.y),
3933
- z: num(r.z),
3934
- faction: str(r.faction) || "neutral",
3935
- team: str(r.team) || str(r.faction) || "neutral",
3936
- unitType: r.unitType == null ? void 0 : str(r.unitType),
3937
- name: r.name == null ? void 0 : str(r.name),
3938
- health: r.health == null ? void 0 : num(r.health),
3939
- maxHealth: r.maxHealth == null ? void 0 : num(r.maxHealth)
3940
- }));
3941
- const selectedUnitId = row ? str(row.selectedUnitId) || null : null;
3942
- const phase = row ? str(row.phase) : "observation";
3943
- const result = row ? str(row.result) : "none";
3944
- const validMoves = row && Array.isArray(row.validMoves) ? row.validMoves : [];
3945
- const attackTargets = row && Array.isArray(row.attackTargets) ? row.attackTargets : [];
3946
- const turn = row ? num(row.turn) : 0;
3947
- const currentTeam = row ? str(row.currentTeam) : "player";
3948
- const isGameOver = result !== "none";
3949
- const gameEndEmittedRef = useRef(false);
3950
- useEffect(() => {
3951
- if ((result === "victory" || result === "defeat") && gameEndEvent) {
3952
- if (!gameEndEmittedRef.current) {
3953
- gameEndEmittedRef.current = true;
3954
- eventBus.emit(`UI:${gameEndEvent}`, { result });
3955
- }
3956
- } else {
3957
- gameEndEmittedRef.current = false;
3958
- }
3959
- }, [result, gameEndEvent, eventBus]);
3960
- const checkGameEnd = useCallback(() => {
3961
- const alivePlayer = entityUnits.filter((u) => unitTeam(u) === "player" && unitHealth(u) > 0);
3962
- const aliveEnemy = entityUnits.filter((u) => unitTeam(u) === "enemy" && unitHealth(u) > 0);
3963
- if (alivePlayer.length === 0 && gameEndEvent) {
3964
- eventBus.emit(`UI:${gameEndEvent}`, { result: "defeat" });
3965
- } else if (aliveEnemy.length === 0 && gameEndEvent) {
3966
- eventBus.emit(`UI:${gameEndEvent}`, { result: "victory" });
3967
- }
3968
- }, [entityUnits, gameEndEvent, eventBus]);
3969
- const handleUnitClickCallback = useCallback((isoUnit) => {
3970
- if (phase !== "action" || !selectedUnitId || !attackEvent) return;
3971
- const attackerRow = entityUnits.find((u) => str(u.id) === selectedUnitId);
3972
- const targetRow = entityUnits.find((u) => str(u.id) === isoUnit.id);
3973
- if (!attackerRow || !targetRow) return;
3974
- if (unitTeam(targetRow) !== "enemy" || unitHealth(targetRow) <= 0) return;
3975
- const ap = unitPosition(attackerRow);
3976
- const tp = unitPosition(targetRow);
3977
- const dx = Math.abs(ap.x - tp.x);
3978
- const dy = Math.abs(ap.y - tp.y);
3979
- if (dx <= 1 && dy <= 1 && dx + dy > 0) {
3980
- const damage = Math.max(1, num(attackerRow.attack) - num(targetRow.defense));
3981
- eventBus.emit(`UI:${attackEvent}`, {
3982
- attackerId: str(attackerRow.id),
3983
- targetId: str(targetRow.id),
3984
- damage
3985
- });
3986
- setTimeout(checkGameEnd, 100);
3987
- }
3988
- }, [phase, selectedUnitId, entityUnits, attackEvent, eventBus, checkGameEnd]);
3989
- const handleEndTurn = useCallback(() => {
3990
- if (endTurnEvent) eventBus.emit(`UI:${endTurnEvent}`, {});
3991
- }, [endTurnEvent, eventBus]);
3992
- const handleCancel = useCallback(() => {
3993
- if (cancelEvent) eventBus.emit(`UI:${cancelEvent}`, {});
3994
- }, [cancelEvent, eventBus]);
3995
- const handlePlayAgain = useCallback(() => {
3996
- if (playAgainEvent) eventBus.emit(`UI:${playAgainEvent}`, {});
3997
- }, [playAgainEvent, eventBus]);
3998
- return /* @__PURE__ */ jsxs(VStack, { className: cn("game-board-3d block w-full min-h-[85vh] relative", className), children: [
3999
- /* @__PURE__ */ jsxs("div", { className: "game-board-3d__status absolute top-3 left-3 z-10 flex gap-2 items-center", children: [
4000
- /* @__PURE__ */ jsx(Typography, { variant: "small", className: "status__phase capitalize", children: phase.replace("_", " ") }),
4001
- currentTeam && !isGameOver && /* @__PURE__ */ jsxs(Typography, { variant: "small", color: "muted", className: "status__team", children: [
4002
- "\u2014 ",
4003
- currentTeam === "player" ? "Your Turn" : "Enemy's Turn"
4004
- ] }),
4005
- /* @__PURE__ */ jsxs(Typography, { variant: "small", color: "muted", className: "status__turn", children: [
4006
- "Turn ",
4007
- turn
4008
- ] })
4009
- ] }),
4010
- /* @__PURE__ */ jsx(
4011
- GameCanvas3D,
4012
- {
4013
- tiles,
4014
- units,
4015
- features,
4016
- cameraMode,
4017
- showGrid: true,
4018
- showCoordinates: false,
4019
- showTileInfo: false,
4020
- shadows: true,
4021
- backgroundColor,
4022
- tileClickEvent,
4023
- unitClickEvent,
4024
- onUnitClick: handleUnitClickCallback,
4025
- selectedUnitId,
4026
- validMoves,
4027
- attackTargets,
4028
- unitScale,
4029
- scale,
4030
- className: "game-board-3d__canvas w-full min-h-[85vh]"
4031
- }
4032
- ),
4033
- !isGameOver && /* @__PURE__ */ jsxs(HStack, { className: "fixed bottom-6 right-6 z-50", gap: "sm", children: [
4034
- (phase === "selection" || phase === "movement" || phase === "action") && /* @__PURE__ */ jsx(
4035
- Button,
4036
- {
4037
- variant: "secondary",
4038
- className: "shadow-xl",
4039
- onClick: handleCancel,
4040
- children: "Cancel"
4041
- }
4042
- ),
4043
- phase !== "enemy_turn" && /* @__PURE__ */ jsx(
4044
- Button,
4045
- {
4046
- variant: "primary",
4047
- className: "shadow-xl",
4048
- onClick: handleEndTurn,
4049
- children: "End Turn"
4050
- }
4051
- )
4052
- ] }),
4053
- isGameOver && /* @__PURE__ */ jsx("div", { className: "game-board-3d__overlay absolute inset-0 z-20 flex items-center justify-center bg-black/60", children: /* @__PURE__ */ jsxs(VStack, { align: "center", gap: "md", className: "overlay__card p-8 rounded-xl bg-gray-900/90 shadow-2xl", children: [
4054
- /* @__PURE__ */ jsx(
4055
- Typography,
4056
- {
4057
- variant: "h2",
4058
- className: cn(
4059
- "overlay__result",
4060
- result === "victory" ? "text-yellow-400" : "text-red-500"
4061
- ),
4062
- children: result === "victory" ? "Victory!" : "Defeat"
4063
- }
4064
- ),
4065
- /* @__PURE__ */ jsxs(Typography, { variant: "body", color: "muted", className: "overlay__turn-count", children: [
4066
- "Completed in ",
4067
- turn,
4068
- " turn",
4069
- turn !== 1 ? "s" : ""
4070
- ] }),
4071
- /* @__PURE__ */ jsx(
4072
- Button,
4073
- {
4074
- variant: "primary",
4075
- className: "px-8 py-3 font-semibold",
4076
- onClick: handlePlayAgain,
4077
- children: "Play Again"
4078
- }
4079
- )
4080
- ] }) })
4081
- ] });
4082
- }
4083
- GameBoard3D.displayName = "GameBoard3D";
4084
- function useTapReveal(options = {}) {
4085
- const { onReveal, onDismiss, refs, enabled = true } = options;
4086
- const [revealed, setRevealed] = useState(false);
4087
- const onRevealRef = useRef(onReveal);
4088
- const onDismissRef = useRef(onDismiss);
4089
- onRevealRef.current = onReveal;
4090
- onDismissRef.current = onDismiss;
4091
- const reveal = useCallback(() => {
4092
- setRevealed((wasRevealed) => {
4093
- if (!wasRevealed) onRevealRef.current?.();
4094
- return true;
4095
- });
4096
- }, []);
4097
- const dismiss = useCallback(() => {
4098
- setRevealed((wasRevealed) => {
4099
- if (wasRevealed) onDismissRef.current?.();
4100
- return false;
4101
- });
4102
- }, []);
4103
- const onPointerDown = useCallback(
4104
- (e) => {
4105
- if (!enabled || e.pointerType === "mouse") return;
4106
- if (revealed) dismiss();
4107
- else reveal();
4108
- },
4109
- [enabled, revealed, reveal, dismiss]
4110
- );
4111
- useEffect(() => {
4112
- if (!revealed || typeof document === "undefined") return;
4113
- const onDocDown = (ev) => {
4114
- const target = ev.target;
4115
- if (!(target instanceof Node)) return;
4116
- const inside = (refs ?? []).some((r) => r.current?.contains(target));
4117
- if (!inside) dismiss();
4118
- };
4119
- const id = setTimeout(() => document.addEventListener("pointerdown", onDocDown), 0);
4120
- return () => {
4121
- clearTimeout(id);
4122
- document.removeEventListener("pointerdown", onDocDown);
4123
- };
4124
- }, [revealed, refs, dismiss]);
4125
- return { revealed, triggerProps: { onPointerDown }, reveal, dismiss };
4126
- }
4127
-
4128
- // components/core/atoms/Box.tsx
4129
- var paddingStyles = {
4130
- none: "p-0",
4131
- xs: "p-1",
4132
- sm: "p-2",
4133
- md: "p-4",
4134
- lg: "p-6",
4135
- xl: "p-8",
4136
- "2xl": "p-12"
4137
- };
4138
- var paddingXStyles = {
4139
- none: "px-0",
4140
- xs: "px-1",
4141
- sm: "px-2",
4142
- md: "px-4",
4143
- lg: "px-6",
4144
- xl: "px-8",
4145
- "2xl": "px-12"
4146
- };
4147
- var paddingYStyles = {
4148
- none: "py-0",
4149
- xs: "py-1",
4150
- sm: "py-2",
4151
- md: "py-4",
4152
- lg: "py-6",
4153
- xl: "py-8",
4154
- "2xl": "py-12"
4155
- };
4156
- var marginStyles = {
4157
- none: "m-0",
4158
- xs: "m-1",
4159
- sm: "m-2",
4160
- md: "m-4",
4161
- lg: "m-6",
4162
- xl: "m-8",
4163
- "2xl": "m-12",
4164
- auto: "m-auto"
4165
- };
4166
- var marginXStyles = {
4167
- none: "mx-0",
4168
- xs: "mx-1",
4169
- sm: "mx-2",
4170
- md: "mx-4",
4171
- lg: "mx-6",
4172
- xl: "mx-8",
4173
- "2xl": "mx-12",
4174
- auto: "mx-auto"
4175
- };
4176
- var marginYStyles = {
4177
- none: "my-0",
4178
- xs: "my-1",
4179
- sm: "my-2",
4180
- md: "my-4",
4181
- lg: "my-6",
4182
- xl: "my-8",
4183
- "2xl": "my-12",
4184
- auto: "my-auto"
4185
- };
4186
- var bgStyles = {
4187
- transparent: "bg-transparent",
4188
- primary: "bg-primary text-primary-foreground",
4189
- secondary: "bg-secondary text-secondary-foreground",
4190
- muted: "bg-muted text-foreground",
4191
- accent: "bg-accent text-accent-foreground",
4192
- surface: "bg-card",
4193
- overlay: "bg-card/80 backdrop-blur-sm"
4194
- };
4195
- var roundedStyles = {
4196
- none: "rounded-none",
4197
- sm: "rounded-sm",
4198
- md: "rounded-md",
4199
- lg: "rounded-lg",
4200
- xl: "rounded-xl",
4201
- "2xl": "rounded-xl",
4202
- full: "rounded-full"
4203
- };
4204
- var shadowStyles = {
4205
- none: "shadow-none",
4206
- sm: "shadow-sm",
4207
- md: "shadow",
4208
- lg: "shadow-lg",
4209
- xl: "shadow-lg"
4210
- };
4211
- var displayStyles = {
4212
- block: "block",
4213
- inline: "inline",
4214
- "inline-block": "inline-block",
4215
- flex: "flex",
4216
- "inline-flex": "inline-flex",
4217
- grid: "grid"
4218
- };
4219
- var overflowStyles2 = {
4220
- auto: "overflow-auto",
4221
- hidden: "overflow-hidden",
4222
- visible: "overflow-visible",
4223
- scroll: "overflow-scroll"
4224
- };
4225
- var positionStyles = {
4226
- relative: "relative",
4227
- absolute: "absolute",
4228
- fixed: "fixed",
4229
- sticky: "sticky"
4230
- };
4231
- var Box = React8__default.forwardRef(
4232
- ({
4233
- padding,
4234
- paddingX,
4235
- paddingY,
4236
- margin,
4237
- marginX,
4238
- marginY,
4239
- bg = "transparent",
4240
- border = false,
4241
- rounded = "none",
4242
- shadow = "none",
4243
- display,
4244
- fullWidth = false,
4245
- fullHeight = false,
4246
- overflow,
4247
- position,
4248
- className,
4249
- children,
4250
- as: Component2 = "div",
4251
- action,
4252
- actionPayload,
4253
- hoverEvent,
4254
- tapReveal = true,
4255
- maxWidth,
4256
- onClick,
4257
- onMouseEnter,
4258
- onMouseLeave,
4259
- onPointerDown,
4260
- ...rest
4261
- }, ref) => {
4262
- const eventBus = useEventBus();
4263
- const handleClick = useCallback((e) => {
4264
- if (action) {
4265
- e.stopPropagation();
4266
- eventBus.emit(`UI:${action}`, actionPayload ?? {});
4267
- }
4268
- onClick?.(e);
4269
- }, [action, actionPayload, eventBus, onClick]);
4270
- const handleMouseEnter = useCallback((e) => {
4271
- if (hoverEvent) {
4272
- eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
4273
- }
4274
- onMouseEnter?.(e);
4275
- }, [hoverEvent, eventBus, onMouseEnter]);
4276
- const handleMouseLeave = useCallback((e) => {
4277
- if (hoverEvent) {
4278
- eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
4279
- }
4280
- onMouseLeave?.(e);
4281
- }, [hoverEvent, eventBus, onMouseLeave]);
4282
- const { triggerProps } = useTapReveal({
4283
- enabled: tapReveal && !!hoverEvent,
4284
- onReveal: useCallback(() => {
4285
- if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: true });
4286
- }, [hoverEvent, eventBus]),
4287
- onDismiss: useCallback(() => {
4288
- if (hoverEvent) eventBus.emit(`UI:${hoverEvent}`, { hovered: false });
4289
- }, [hoverEvent, eventBus])
4290
- });
4291
- const handlePointerDown = useCallback((e) => {
4292
- if (hoverEvent && tapReveal) triggerProps.onPointerDown(e);
4293
- onPointerDown?.(e);
4294
- }, [hoverEvent, tapReveal, triggerProps, onPointerDown]);
4295
- const isClickable = action || onClick;
4296
- return React8__default.createElement(
4297
- Component2,
4298
- {
4299
- ref,
4300
- className: cn(
4301
- padding && paddingStyles[padding],
4302
- paddingX && paddingXStyles[paddingX],
4303
- paddingY && paddingYStyles[paddingY],
4304
- margin && marginStyles[margin],
4305
- marginX && marginXStyles[marginX],
4306
- marginY && marginYStyles[marginY],
4307
- bgStyles[bg],
4308
- border && "border-[length:var(--border-width)] border-border",
4309
- roundedStyles[rounded],
4310
- shadowStyles[shadow],
4311
- display && displayStyles[display],
4312
- fullWidth && "w-full",
4313
- fullHeight && "h-full",
4314
- overflow && overflowStyles2[overflow],
4315
- position && positionStyles[position],
4316
- isClickable && "cursor-pointer",
4317
- className
4318
- ),
4319
- onClick: isClickable ? handleClick : void 0,
4320
- onMouseEnter: hoverEvent || onMouseEnter ? handleMouseEnter : void 0,
4321
- onMouseLeave: hoverEvent || onMouseLeave ? handleMouseLeave : void 0,
4322
- onPointerDown: hoverEvent && tapReveal || onPointerDown ? handlePointerDown : void 0,
4323
- style: maxWidth ? { maxWidth, ...rest.style } : rest.style,
4324
- ...rest
4325
- },
4326
- children
4327
- );
4328
- }
4329
- );
4330
- Box.displayName = "Box";
4331
-
4332
- // components/game/shared/makeAsset.ts
4333
- var ROLE_DEFAULTS = {
4334
- tile: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4335
- decoration: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4336
- effect: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4337
- item: { dimension: "2d", animations: ["static"], aspect: "5:7" },
4338
- player: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4339
- enemy: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4340
- npc: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4341
- projectile: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4342
- vehicle: { dimension: "2d", animations: ["idle", "walk"], aspect: "1:1" },
4343
- ui: { dimension: "2d", animations: ["static"], aspect: "1:1" }
4344
- };
4345
- function makeAsset(url, role, overrides = {}) {
4346
- const defaults = ROLE_DEFAULTS[role] ?? { dimension: "2d", animations: ["static"], aspect: "1:1" };
4347
- return {
4348
- url,
4349
- role,
4350
- category: url.split("/").pop()?.replace(/\.[^.]+$/, "") ?? role,
4351
- ...defaults,
4352
- ...overrides
4353
- };
4354
- }
4355
-
4356
- // components/game/shared/lib/game3dAssetManifest.ts
4357
- function tileKind(type, passable) {
4358
- if (passable === false) return "wall";
4359
- if (type === "dirt" || type === "road") return "dirt";
4360
- return "open";
4361
- }
4362
- function resolveTilesWithModels(layout, manifest) {
4363
- return layout.map((t) => {
4364
- const kind = t.kind || tileKind(t.type, t.passable);
4365
- return {
4366
- id: t.id,
4367
- x: t.x,
4368
- y: t.y,
4369
- z: t.z,
4370
- type: t.type,
4371
- passable: t.passable,
4372
- modelUrl: manifest?.models?.[kind]
4373
- };
4374
- });
4375
- }
4376
- function resolvePropTiles(tiles, manifest) {
4377
- return tiles.map((t) => {
4378
- const kind = tileKind(t.type ?? "", t.passable);
4379
- return {
4380
- ...t,
4381
- modelUrl: t.modelUrl ?? manifest?.models?.[kind]
4382
- };
4383
- });
4384
- }
4385
- function resolveEntityTiles(entity, manifest) {
4386
- const row = boardEntity(entity);
4387
- if (!row) return [];
4388
- return rows(row.tiles).map((r) => {
4389
- const type = str(r.type) || str(r.terrain);
4390
- const passable = r.passable !== false;
4391
- const kind = tileKind(type, passable);
4392
- const explicitModelUrl = r.modelUrl == null ? void 0 : str(r.modelUrl);
4393
- const explicitModel = explicitModelUrl ? makeAsset(explicitModelUrl, "decoration", { dimension: "3d" }) : void 0;
4394
- return {
4395
- id: r.id == null ? void 0 : str(r.id),
4396
- x: num(r.x),
4397
- y: num(r.y),
4398
- z: r.z == null ? void 0 : num(r.z),
4399
- type,
4400
- passable,
4401
- modelUrl: explicitModel ?? manifest?.models?.[kind]
4402
- };
4403
- });
4404
- }
4405
- function resolveFeaturesWithModels(features, manifest) {
4406
- return features.map((f) => ({
4407
- ...f,
4408
- assetUrl: f.assetUrl ?? manifest?.features?.[f.type]
4409
- }));
4410
- }
4411
- function resolveEntityFeatures(entity, manifest) {
4412
- const row = boardEntity(entity);
4413
- if (!row) return [];
4414
- return rows(row.features).map((r) => {
4415
- const type = str(r.type);
4416
- const explicitUrl = r.assetUrl == null ? void 0 : str(r.assetUrl);
4417
- const explicit = explicitUrl ? makeAsset(explicitUrl, "decoration", { dimension: "3d" }) : void 0;
4418
- return {
4419
- id: r.id == null ? void 0 : str(r.id),
4420
- x: num(r.x),
4421
- y: num(r.y),
4422
- z: r.z == null ? void 0 : num(r.z),
4423
- type,
4424
- color: r.color == null ? void 0 : str(r.color),
4425
- assetUrl: explicit ?? manifest?.features?.[type]
4426
- };
4427
- });
4428
- }
4429
- var DEFAULT_3D_BATTLE_LAYOUT = [
4430
- { id: "t00", x: 0, y: 0, z: 0, type: "stone", passable: false, kind: "wall" },
4431
- { id: "t10", x: 1, y: 0, z: 0, type: "stone", passable: false, kind: "wall" },
4432
- { id: "t20", x: 2, y: 0, z: 0, type: "stone", passable: false, kind: "wall" },
4433
- { id: "t30", x: 3, y: 0, z: 0, type: "stone", passable: false, kind: "wall" },
4434
- { id: "t40", x: 4, y: 0, z: 0, type: "stone", passable: false, kind: "wall" },
4435
- { id: "t01", x: 0, y: 1, z: 1, type: "stone", passable: false, kind: "wall" },
4436
- { id: "t11", x: 1, y: 1, z: 1, type: "dirt", passable: true, kind: "dirt" },
4437
- { id: "t21", x: 2, y: 1, z: 1, type: "grass", passable: true, kind: "open" },
4438
- { id: "t31", x: 3, y: 1, z: 1, type: "grass", passable: true, kind: "open" },
4439
- { id: "t41", x: 4, y: 1, z: 1, type: "stone", passable: false, kind: "wall" },
4440
- { id: "t02", x: 0, y: 2, z: 2, type: "stone", passable: false, kind: "wall" },
4441
- { id: "t12", x: 1, y: 2, z: 2, type: "grass", passable: true, kind: "open" },
4442
- { id: "t22", x: 2, y: 2, z: 2, type: "dirt", passable: true, kind: "dirt" },
4443
- { id: "t32", x: 3, y: 2, z: 2, type: "grass", passable: true, kind: "open" },
4444
- { id: "t42", x: 4, y: 2, z: 2, type: "stone", passable: false, kind: "wall" },
4445
- { id: "t03", x: 0, y: 3, z: 3, type: "stone", passable: false, kind: "wall" },
4446
- { id: "t13", x: 1, y: 3, z: 3, type: "grass", passable: true, kind: "open" },
4447
- { id: "t23", x: 2, y: 3, z: 3, type: "grass", passable: true, kind: "open" },
4448
- { id: "t33", x: 3, y: 3, z: 3, type: "dirt", passable: true, kind: "dirt" },
4449
- { id: "t43", x: 4, y: 3, z: 3, type: "stone", passable: false, kind: "wall" },
4450
- { id: "t04", x: 0, y: 4, z: 4, type: "stone", passable: false, kind: "wall" },
4451
- { id: "t14", x: 1, y: 4, z: 4, type: "stone", passable: false, kind: "wall" },
4452
- { id: "t24", x: 2, y: 4, z: 4, type: "stone", passable: false, kind: "wall" },
4453
- { id: "t34", x: 3, y: 4, z: 4, type: "stone", passable: false, kind: "wall" },
4454
- { id: "t44", x: 4, y: 4, z: 4, type: "stone", passable: false, kind: "wall" }
4455
- ];
4456
- var DEFAULT_3D_BATTLE_FEATURES = [
4457
- { id: "f1", x: 2, y: 2, z: 2, type: "gold_mine", color: "#f4c542" },
4458
- { id: "f2", x: 3, y: 1, z: 1, type: "portal", color: "#8b5cf6" }
4459
- ];
4460
- function GameCanvas3DBattleTemplate({
4461
- entity,
4462
- tiles: propTiles,
4463
- features: propFeatures = DEFAULT_3D_BATTLE_FEATURES,
4464
- assetManifest,
4465
- cameraMode = "perspective",
4466
- backgroundColor = "#2a1a1a",
4467
- tileClickEvent,
4468
- unitClickEvent,
4469
- endTurnEvent,
4470
- cancelEvent,
4471
- attackEvent,
4472
- playAgainEvent,
4473
- gameEndEvent,
4474
- unitScale,
4475
- scale,
4476
- className
4477
- }) {
4478
- const row = boardEntity(entity);
4479
- const hasEntityTiles = !!row && Array.isArray(row.tiles) && row.tiles.length > 0;
4480
- const tiles = hasEntityTiles ? resolveEntityTiles(entity, assetManifest) : propTiles ? resolvePropTiles(propTiles, assetManifest) : resolveTilesWithModels(DEFAULT_3D_BATTLE_LAYOUT, assetManifest);
4481
- const features = row && Array.isArray(row.features) && row.features.length > 0 ? resolveEntityFeatures(entity, assetManifest) : resolveFeaturesWithModels(propFeatures, assetManifest);
4482
- const currentTurn = row?.currentTeam == null ? void 0 : String(row.currentTeam);
4483
- const round = row?.turn == null ? void 0 : Number(row.turn);
4484
- return /* @__PURE__ */ jsxs(
4485
- Box,
4486
- {
4487
- className: cn("game-canvas-3d-battle-template block relative w-full min-h-[85vh]", className),
4488
- children: [
4489
- /* @__PURE__ */ jsx(
4490
- GameBoard3D,
4491
- {
4492
- entity,
4493
- tiles,
4494
- features,
4495
- cameraMode,
4496
- backgroundColor,
4497
- unitScale,
4498
- scale,
4499
- tileClickEvent,
4500
- unitClickEvent,
4501
- endTurnEvent,
4502
- cancelEvent,
4503
- attackEvent,
4504
- playAgainEvent,
4505
- gameEndEvent,
4506
- className: "game-canvas-3d-battle-template__board"
4507
- }
4508
- ),
4509
- currentTurn && /* @__PURE__ */ jsxs(
4510
- HStack,
4511
- {
4512
- gap: "sm",
4513
- align: "center",
4514
- className: cn("battle-template__turn-indicator absolute top-3 right-3 z-10", `battle-template__turn-indicator--${currentTurn}`),
4515
- children: [
4516
- /* @__PURE__ */ jsx(Typography, { variant: "body", className: "turn-indicator__label", children: currentTurn === "player" ? "Your Turn" : "Enemy's Turn" }),
4517
- round != null && /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "turn-indicator__round", children: [
4518
- "Round ",
4519
- round
4520
- ] })
4521
- ]
4522
- }
4523
- )
4524
- ]
4525
- }
4526
- );
4527
- }
4528
- GameCanvas3DBattleTemplate.displayName = "GameCanvas3DBattleTemplate";
4529
- var DEFAULT_3D_CASTLE_LAYOUT = [
4530
- { id: "t00", x: 0, y: 0, z: 0, type: "wall", passable: false, kind: "wall" },
4531
- { id: "t10", x: 1, y: 0, z: 0, type: "wall", passable: false, kind: "wall" },
4532
- { id: "t20", x: 2, y: 0, z: 0, type: "wall", passable: false, kind: "wall" },
4533
- { id: "t30", x: 3, y: 0, z: 0, type: "wall", passable: false, kind: "wall" },
4534
- { id: "t40", x: 4, y: 0, z: 0, type: "wall", passable: false, kind: "wall" },
4535
- { id: "t01", x: 0, y: 1, z: 1, type: "wall", passable: false, kind: "wall" },
4536
- { id: "t11", x: 1, y: 1, z: 1, type: "floor", passable: true, kind: "dirt" },
4537
- { id: "t21", x: 2, y: 1, z: 1, type: "floor", passable: true, kind: "open" },
4538
- { id: "t31", x: 3, y: 1, z: 1, type: "floor", passable: true, kind: "open" },
4539
- { id: "t41", x: 4, y: 1, z: 1, type: "wall", passable: false, kind: "wall" },
4540
- { id: "t02", x: 0, y: 2, z: 2, type: "wall", passable: false, kind: "wall" },
4541
- { id: "t12", x: 1, y: 2, z: 2, type: "floor", passable: true, kind: "open" },
4542
- { id: "t22", x: 2, y: 2, z: 2, type: "floor", passable: true, kind: "dirt" },
4543
- { id: "t32", x: 3, y: 2, z: 2, type: "floor", passable: true, kind: "open" },
4544
- { id: "t42", x: 4, y: 2, z: 2, type: "wall", passable: false, kind: "wall" },
4545
- { id: "t03", x: 0, y: 3, z: 3, type: "wall", passable: false, kind: "wall" },
4546
- { id: "t13", x: 1, y: 3, z: 3, type: "floor", passable: true, kind: "open" },
4547
- { id: "t23", x: 2, y: 3, z: 3, type: "floor", passable: true, kind: "open" },
4548
- { id: "t33", x: 3, y: 3, z: 3, type: "floor", passable: true, kind: "dirt" },
4549
- { id: "t43", x: 4, y: 3, z: 3, type: "wall", passable: false, kind: "wall" },
4550
- { id: "t04", x: 0, y: 4, z: 4, type: "wall", passable: false, kind: "wall" },
4551
- { id: "t14", x: 1, y: 4, z: 4, type: "wall", passable: false, kind: "wall" },
4552
- { id: "t24", x: 2, y: 4, z: 4, type: "wall", passable: false, kind: "wall" },
4553
- { id: "t34", x: 3, y: 4, z: 4, type: "wall", passable: false, kind: "wall" },
4554
- { id: "t44", x: 4, y: 4, z: 4, type: "wall", passable: false, kind: "wall" }
4555
- ];
4556
- var DEFAULT_3D_CASTLE_FEATURES = [
4557
- { id: "f1", x: 2, y: 2, z: 2, type: "chest", color: "#f4c542" },
4558
- { id: "f2", x: 3, y: 1, z: 1, type: "crystal", color: "#8b5cf6" }
4559
- ];
4560
- function GameCanvas3DCastleTemplate({
4561
- entity,
4562
- tiles: propTiles,
4563
- features: propFeatures = DEFAULT_3D_CASTLE_FEATURES,
4564
- assetManifest,
4565
- cameraMode = "isometric",
4566
- backgroundColor = "#1e1e2e",
4567
- buildingClickEvent,
4568
- unitClickEvent,
4569
- endTurnEvent,
4570
- cancelEvent,
4571
- attackEvent,
4572
- playAgainEvent,
4573
- gameEndEvent,
4574
- showHeader = true,
4575
- unitScale,
4576
- scale,
4577
- className
4578
- }) {
4579
- const row = boardEntity(entity);
4580
- const hasEntityTiles = !!row && Array.isArray(row.tiles) && row.tiles.length > 0;
4581
- const tiles = hasEntityTiles ? resolveEntityTiles(entity, assetManifest) : propTiles ? resolvePropTiles(propTiles, assetManifest) : resolveTilesWithModels(DEFAULT_3D_CASTLE_LAYOUT, assetManifest);
4582
- const features = row && Array.isArray(row.features) && row.features.length > 0 ? resolveEntityFeatures(entity, assetManifest) : resolveFeaturesWithModels(propFeatures, assetManifest);
4583
- const name = row?.name == null ? void 0 : str(row.name);
4584
- const level = row?.level == null ? void 0 : num(row.level);
4585
- const owner = row?.owner == null ? void 0 : str(row.owner);
4586
- const unitCount = row && Array.isArray(row.units) ? row.units.length : 0;
4587
- return /* @__PURE__ */ jsxs(
4588
- VStack,
4589
- {
4590
- className: cn("game-canvas-3d-castle-template block w-full min-h-[85vh]", className),
4591
- children: [
4592
- showHeader && name && /* @__PURE__ */ jsxs(HStack, { gap: "md", align: "center", className: "castle-template__header", children: [
4593
- /* @__PURE__ */ jsx(Typography, { variant: "h2", className: "header__name", children: name }),
4594
- level != null && /* @__PURE__ */ jsxs(Typography, { variant: "small", className: "header__level", children: [
4595
- "Level ",
4596
- level
4597
- ] }),
4598
- owner && /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", className: "header__owner", children: owner })
4599
- ] }),
4600
- /* @__PURE__ */ jsx(
4601
- GameBoard3D,
4602
- {
4603
- entity,
4604
- tiles,
4605
- features,
4606
- cameraMode,
4607
- backgroundColor,
4608
- unitScale,
4609
- scale,
4610
- tileClickEvent: buildingClickEvent,
4611
- unitClickEvent,
4612
- endTurnEvent,
4613
- cancelEvent,
4614
- attackEvent,
4615
- playAgainEvent,
4616
- gameEndEvent,
4617
- className: "game-canvas-3d-castle-template__board"
4618
- }
4619
- ),
4620
- unitCount > 0 && /* @__PURE__ */ jsxs(HStack, { gap: "sm", align: "center", className: "castle-template__garrison-info", children: [
4621
- /* @__PURE__ */ jsx(Typography, { variant: "small", className: "garrison-info__label", children: "Garrison:" }),
4622
- /* @__PURE__ */ jsxs(Typography, { variant: "small", weight: "bold", className: "garrison-info__count", children: [
4623
- unitCount,
4624
- " units"
4625
- ] })
4626
- ] })
4627
- ]
4628
- }
4629
- );
4630
- }
4631
- GameCanvas3DCastleTemplate.displayName = "GameCanvas3DCastleTemplate";
4632
- var DEFAULT_3D_WORLDMAP_LAYOUT = [
4633
- { id: "t00", x: 0, y: 0, z: 0, type: "mountain", passable: false, kind: "wall" },
4634
- { id: "t10", x: 1, y: 0, z: 0, type: "mountain", passable: false, kind: "wall" },
4635
- { id: "t20", x: 2, y: 0, z: 0, type: "water", passable: false, kind: "wall" },
4636
- { id: "t30", x: 3, y: 0, z: 0, type: "mountain", passable: false, kind: "wall" },
4637
- { id: "t40", x: 4, y: 0, z: 0, type: "mountain", passable: false, kind: "wall" },
4638
- { id: "t01", x: 0, y: 1, z: 1, type: "mountain", passable: false, kind: "wall" },
4639
- { id: "t11", x: 1, y: 1, z: 1, type: "grass", passable: true, kind: "open" },
4640
- { id: "t21", x: 2, y: 1, z: 1, type: "grass", passable: true, kind: "open" },
4641
- { id: "t31", x: 3, y: 1, z: 1, type: "grass", passable: true, kind: "open" },
4642
- { id: "t41", x: 4, y: 1, z: 1, type: "water", passable: false, kind: "wall" },
4643
- { id: "t02", x: 0, y: 2, z: 2, type: "water", passable: false, kind: "wall" },
4644
- { id: "t12", x: 1, y: 2, z: 2, type: "grass", passable: true, kind: "open" },
4645
- { id: "t22", x: 2, y: 2, z: 2, type: "road", passable: true, kind: "dirt" },
4646
- { id: "t32", x: 3, y: 2, z: 2, type: "grass", passable: true, kind: "open" },
4647
- { id: "t42", x: 4, y: 2, z: 2, type: "mountain", passable: false, kind: "wall" },
4648
- { id: "t03", x: 0, y: 3, z: 3, type: "mountain", passable: false, kind: "wall" },
4649
- { id: "t13", x: 1, y: 3, z: 3, type: "grass", passable: true, kind: "open" },
4650
- { id: "t23", x: 2, y: 3, z: 3, type: "grass", passable: true, kind: "open" },
4651
- { id: "t33", x: 3, y: 3, z: 3, type: "road", passable: true, kind: "dirt" },
4652
- { id: "t43", x: 4, y: 3, z: 3, type: "mountain", passable: false, kind: "wall" },
4653
- { id: "t04", x: 0, y: 4, z: 4, type: "mountain", passable: false, kind: "wall" },
4654
- { id: "t14", x: 1, y: 4, z: 4, type: "water", passable: false, kind: "wall" },
4655
- { id: "t24", x: 2, y: 4, z: 4, type: "grass", passable: true, kind: "open" },
4656
- { id: "t34", x: 3, y: 4, z: 4, type: "mountain", passable: false, kind: "wall" },
4657
- { id: "t44", x: 4, y: 4, z: 4, type: "mountain", passable: false, kind: "wall" }
4658
- ];
4659
- var DEFAULT_3D_WORLDMAP_FEATURES = [
4660
- { id: "f1", x: 2, y: 2, z: 2, type: "capital", color: "#f4c542" },
4661
- { id: "f2", x: 4, y: 2, z: 2, type: "power_node", color: "#8b5cf6" }
4662
- ];
4663
- function GameCanvas3DWorldMapTemplate({
4664
- entity,
4665
- tiles: propTiles,
4666
- features: propFeatures = DEFAULT_3D_WORLDMAP_FEATURES,
4667
- assetManifest,
4668
- cameraMode = "isometric",
4669
- backgroundColor = "#1a1a2e",
4670
- tileClickEvent,
4671
- unitClickEvent,
4672
- endTurnEvent,
4673
- cancelEvent,
4674
- attackEvent,
4675
- playAgainEvent,
4676
- gameEndEvent,
4677
- unitScale,
4678
- scale,
4679
- className
4680
- }) {
4681
- const row = boardEntity(entity);
4682
- const hasEntityTiles = !!row && Array.isArray(row.tiles) && row.tiles.length > 0;
4683
- const tiles = hasEntityTiles ? resolveEntityTiles(entity, assetManifest) : propTiles ? resolvePropTiles(propTiles, assetManifest) : resolveTilesWithModels(DEFAULT_3D_WORLDMAP_LAYOUT, assetManifest);
4684
- const features = row && Array.isArray(row.features) && row.features.length > 0 ? resolveEntityFeatures(entity, assetManifest) : resolveFeaturesWithModels(propFeatures, assetManifest);
4685
- return (
4686
- /* GameBoard3D reads selectedUnitId/validMoves/attackTargets from entity */
4687
- /* @__PURE__ */ jsx(
4688
- GameBoard3D,
4689
- {
4690
- entity,
4691
- tiles,
4692
- features,
4693
- cameraMode,
4694
- backgroundColor,
4695
- unitScale,
4696
- scale,
4697
- tileClickEvent,
4698
- unitClickEvent,
4699
- endTurnEvent,
4700
- cancelEvent,
4701
- attackEvent,
4702
- playAgainEvent,
4703
- gameEndEvent,
4704
- className
4705
- }
4706
- )
4707
- );
4708
- }
4709
- GameCanvas3DWorldMapTemplate.displayName = "GameCanvas3DWorldMapTemplate";
4710
- var DEFAULT_CONFIG = {
4711
- cellSize: 1,
4712
- offsetX: 0,
4713
- offsetZ: 0,
4714
- elevation: 0
4715
- };
4716
- function gridToWorld(gridX, gridZ, config = DEFAULT_CONFIG) {
4717
- const opts = { ...DEFAULT_CONFIG, ...config };
4718
- return new THREE6.Vector3(
4719
- gridX * opts.cellSize + opts.offsetX,
4720
- opts.elevation,
4721
- gridZ * opts.cellSize + opts.offsetZ
4722
- );
4723
- }
4724
- function worldToGrid(worldX, worldZ, config = DEFAULT_CONFIG) {
4725
- const opts = { ...DEFAULT_CONFIG, ...config };
4726
- return {
4727
- x: Math.round((worldX - opts.offsetX) / opts.cellSize),
4728
- z: Math.round((worldZ - opts.offsetZ) / opts.cellSize)
4729
- };
4730
- }
4731
- function raycastToPlane(camera, mouseX, mouseY, planeY = 0) {
4732
- const raycaster = new THREE6.Raycaster();
4733
- const mouse = new THREE6.Vector2(mouseX, mouseY);
4734
- raycaster.setFromCamera(mouse, camera);
4735
- const plane = new THREE6.Plane(new THREE6.Vector3(0, 1, 0), -planeY);
4736
- const target = new THREE6.Vector3();
4737
- const intersection = raycaster.ray.intersectPlane(plane, target);
4738
- return intersection ? target : null;
4739
- }
4740
- function raycastToObjects(camera, mouseX, mouseY, objects) {
4741
- const raycaster = new THREE6.Raycaster();
4742
- const mouse = new THREE6.Vector2(mouseX, mouseY);
4743
- raycaster.setFromCamera(mouse, camera);
4744
- const intersects = raycaster.intersectObjects(objects, true);
4745
- return intersects.length > 0 ? intersects[0] : null;
4746
- }
4747
- function gridDistance(a, b) {
4748
- const dx = b.x - a.x;
4749
- const dz = b.z - a.z;
4750
- return Math.sqrt(dx * dx + dz * dz);
4751
- }
4752
- function gridManhattanDistance(a, b) {
4753
- return Math.abs(b.x - a.x) + Math.abs(b.z - a.z);
4754
- }
4755
- function getNeighbors(x, z, includeDiagonal = false) {
4756
- const neighbors = [
4757
- { x: x + 1, z },
4758
- { x: x - 1, z },
4759
- { x, z: z + 1 },
4760
- { x, z: z - 1 }
4761
- ];
4762
- if (includeDiagonal) {
4763
- neighbors.push(
4764
- { x: x + 1, z: z + 1 },
4765
- { x: x - 1, z: z - 1 },
4766
- { x: x + 1, z: z - 1 },
4767
- { x: x - 1, z: z + 1 }
4768
- );
4769
- }
4770
- return neighbors;
4771
- }
4772
- function isInBounds(x, z, bounds) {
4773
- return x >= bounds.minX && x <= bounds.maxX && z >= bounds.minZ && z <= bounds.maxZ;
4774
- }
4775
- function getCellsInRadius(centerX, centerZ, radius) {
4776
- const cells = [];
4777
- const radiusSquared = radius * radius;
4778
- const minX = Math.floor(centerX - radius);
4779
- const maxX = Math.ceil(centerX + radius);
4780
- const minZ = Math.floor(centerZ - radius);
4781
- const maxZ = Math.ceil(centerZ + radius);
4782
- for (let x = minX; x <= maxX; x++) {
4783
- for (let z = minZ; z <= maxZ; z++) {
4784
- const dx = x - centerX;
4785
- const dz = z - centerZ;
4786
- if (dx * dx + dz * dz <= radiusSquared) {
4787
- cells.push({ x, z });
4788
- }
4789
- }
4790
- }
4791
- return cells;
4792
- }
4793
- function createGridHighlight(color = 16776960, opacity = 0.3) {
4794
- const geometry = new THREE6.PlaneGeometry(0.95, 0.95);
4795
- const material = new THREE6.MeshBasicMaterial({
4796
- color,
4797
- transparent: true,
4798
- opacity,
4799
- side: THREE6.DoubleSide
4800
- });
4801
- const mesh = new THREE6.Mesh(geometry, material);
4802
- mesh.rotation.x = -Math.PI / 2;
4803
- mesh.position.y = 0.01;
4804
- return mesh;
4805
- }
4806
- function normalizeMouseCoordinates(clientX, clientY, element) {
4807
- const rect = element.getBoundingClientRect();
4808
- return {
4809
- x: (clientX - rect.left) / rect.width * 2 - 1,
4810
- y: -((clientY - rect.top) / rect.height) * 2 + 1
4811
- };
4812
- }
4813
-
4814
2616
  // components/avl/lib/avl-schema-parser.ts
4815
2617
  function getEntity(orbital) {
4816
2618
  const entity = orbital.entity;
@@ -4905,10 +2707,10 @@ function parseApplicationLevel(schema) {
4905
2707
  }
4906
2708
  const count = schema.orbitals.length;
4907
2709
  const cols = Math.ceil(Math.sqrt(count));
4908
- const rows2 = Math.ceil(count / cols);
2710
+ const rows = Math.ceil(count / cols);
4909
2711
  const spacing = 200;
4910
2712
  const gridW = cols * spacing;
4911
- const gridH = rows2 * spacing;
2713
+ const gridH = rows * spacing;
4912
2714
  const originX = (600 - gridW) / 2 + spacing / 2;
4913
2715
  const originY = (400 - gridH) / 2 + spacing / 2;
4914
2716
  schema.orbitals.forEach((orbital, i) => {
@@ -5441,7 +3243,7 @@ Avl3DLabel.displayName = "Avl3DLabel";
5441
3243
  var Avl3DTooltip = ({
5442
3244
  position,
5443
3245
  title,
5444
- rows: rows2,
3246
+ rows,
5445
3247
  accentColor = "#5b9bd5"
5446
3248
  }) => {
5447
3249
  return /* @__PURE__ */ jsx(
@@ -5479,7 +3281,7 @@ var Avl3DTooltip = ({
5479
3281
  children: title
5480
3282
  }
5481
3283
  ),
5482
- rows2.map((row) => /* @__PURE__ */ jsxs(
3284
+ rows.map((row) => /* @__PURE__ */ jsxs(
5483
3285
  Box,
5484
3286
  {
5485
3287
  style: {
@@ -6971,7 +4773,7 @@ var Avl3DContext = createContext(DEFAULT_CONFIG2);
6971
4773
  function useAvl3DConfig() {
6972
4774
  return useContext(Avl3DContext);
6973
4775
  }
6974
- function CameraController2({ targetPosition, targetLookAt, animated }) {
4776
+ function CameraController({ targetPosition, targetLookAt, animated }) {
6975
4777
  const { camera } = useThree();
6976
4778
  const targetPosVec = useRef(new THREE6.Vector3(...targetPosition));
6977
4779
  const targetLookVec = useRef(new THREE6.Vector3(...targetLookAt));
@@ -7081,7 +4883,7 @@ var Avl3DViewer = ({
7081
4883
  const handleTraitClick = useCallback((name) => {
7082
4884
  dispatch({ type: "ZOOM_INTO_TRAIT", trait: name, targetPosition: { x: 0, y: 0 } });
7083
4885
  }, []);
7084
- const [highlightedTrait, setHighlightedTrait] = React8__default.useState(null);
4886
+ const [highlightedTrait, setHighlightedTrait] = React7.useState(null);
7085
4887
  const handleTransitionClick = useCallback((index) => {
7086
4888
  dispatch({ type: "ZOOM_INTO_TRANSITION", transitionIndex: index, targetPosition: { x: 0, y: 0 } });
7087
4889
  }, []);
@@ -7168,7 +4970,7 @@ var Avl3DViewer = ({
7168
4970
  gap: "xs",
7169
4971
  align: "center",
7170
4972
  className: "absolute top-2 left-2 z-10 bg-surface/80 backdrop-blur rounded-md px-3 py-1.5",
7171
- children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React8__default.Fragment, { children: [
4973
+ children: breadcrumbs.map((crumb, i) => /* @__PURE__ */ jsxs(React7.Fragment, { children: [
7172
4974
  i > 0 && /* @__PURE__ */ jsx(Typography, { variant: "small", color: "muted", className: "mx-1", children: "/" }),
7173
4975
  i < breadcrumbs.length - 1 ? /* @__PURE__ */ jsx(
7174
4976
  Box,
@@ -7243,7 +5045,7 @@ var Avl3DViewer = ({
7243
5045
  }
7244
5046
  ),
7245
5047
  /* @__PURE__ */ jsx(
7246
- CameraController2,
5048
+ CameraController,
7247
5049
  {
7248
5050
  targetPosition: cameraConfig.position,
7249
5051
  targetLookAt: cameraConfig.target,
@@ -7444,4 +5246,4 @@ var SpatialHashGrid = class {
7444
5246
  }
7445
5247
  };
7446
5248
 
7447
- 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 };
5249
+ export { AVL_3D_COLORS, AssetLoader, Avl3DApplicationScene, Avl3DContext, Avl3DEffects, Avl3DOrbitalScene, Avl3DTraitScene, Avl3DTransitionScene, Avl3DViewer, CAMERA_POSITIONS, Camera3D, Canvas3DErrorBoundary, Canvas3DLoadingState, GameCanvas3D, 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 };