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