@almadar/ui 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,15 @@
1
- import { useDesignTheme } from '../chunk-4FRUCUO5.js';
2
- import { useQuerySingleton, usePaginatedEntityList, useEntityList, useSelectedEntity, useEntityDetail, useAuthContext } from '../chunk-VE4ZELYZ.js';
3
- export { ENTITY_EVENTS, entityDataKeys, parseQueryBinding, useAgentChat, useAuthContext, useCompile, useConnectGitHub, useCreateEntity, useDeepAgentGeneration, useDeleteEntity, useDisconnectGitHub, useEntities, useEntitiesByType, useEntity, useEntity2 as useEntityById, useEntityDetail, useEntityList, useEntityMutations, useExtensions, useFileEditor, useFileSystem, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useInput, useOrbitalHistory, useOrbitalMutations, usePhysics, usePlayer, usePreview, useQuerySingleton, useSelectedEntity, useSendOrbitalEvent, useSingletonEntity, useUIEvents, useUpdateEntity, useValidation } from '../chunk-VE4ZELYZ.js';
1
+ import { useUISlots } from '../chunk-W5YTXLXL.js';
2
+ import { useQuerySingleton, useSelectedEntity, useAuthContext } from '../chunk-ITRZURGQ.js';
3
+ export { ENTITY_EVENTS, parseQueryBinding, useAgentChat, useAuthContext, useCompile, useConnectGitHub, useCreateEntity, useDeepAgentGeneration, useDeleteEntity, useDisconnectGitHub, useEntities, useEntitiesByType, useEntity as useEntityById, useEntityMutations, useExtensions, useFileEditor, useFileSystem, useGitHubBranches, useGitHubRepo, useGitHubRepos, useGitHubStatus, useInput, useOrbitalHistory, useOrbitalMutations, usePhysics, usePlayer, usePreview, useQuerySingleton, useSelectedEntity, useSendOrbitalEvent, useSingletonEntity, useUIEvents, useUpdateEntity, useValidation } from '../chunk-ITRZURGQ.js';
4
+ export { DEFAULT_SLOTS, useUISlotManager } from '../chunk-7NEWMNNU.js';
4
5
  import { cn, debugGroup, debug, debugGroupEnd, getNestedValue, isDebugEnabled } from '../chunk-KKCVDUK7.js';
5
6
  export { cn } from '../chunk-KKCVDUK7.js';
6
7
  import '../chunk-XSEDIUM6.js';
7
- import { useTheme, useUISlots } from '../chunk-I5RSZIOE.js';
8
- import { useEventBus } from '../chunk-TTXKOHDO.js';
9
- export { useEmitEvent, useEventBus, useEventListener } from '../chunk-TTXKOHDO.js';
10
- export { DEFAULT_SLOTS, useUISlotManager } from '../chunk-7NEWMNNU.js';
8
+ import { useTheme } from '../chunk-4UFNDD6B.js';
9
+ import { useEventBus, usePaginatedEntityList, useEntityList, useEntityDetail } from '../chunk-WXFQV3ZP.js';
10
+ export { EntityDataProvider, entityDataKeys, useEmitEvent, useEntity, useEntityDataAdapter, useEntityDetail, useEntityList, useEventBus, useEventListener } from '../chunk-WXFQV3ZP.js';
11
11
  export { clearEntities, getAllEntities, getByType, getEntity, getSingleton, removeEntity, spawnEntity, updateEntity, updateSingleton } from '../chunk-N7MVUW4R.js';
12
- import '../chunk-PKBMQBKP.js';
12
+ import { __publicField } from '../chunk-PKBMQBKP.js';
13
13
  import * as React37 from 'react';
14
14
  import React37__default, { useCallback, useMemo, useState, useEffect, useRef } from 'react';
15
15
  import * as LucideIcons from 'lucide-react';
@@ -1816,47 +1816,95 @@ ThemeToggle.displayName = "ThemeToggle";
1816
1816
  var THEME_LABELS = {
1817
1817
  wireframe: {
1818
1818
  label: "Wireframe",
1819
- icon: "\u{1F4D0}",
1820
- description: "Sharp corners, thick borders"
1819
+ description: "Sharp corners, thick borders, brutalist"
1821
1820
  },
1822
1821
  minimalist: {
1823
1822
  label: "Minimalist",
1824
- icon: "\u2728",
1825
1823
  description: "Clean, subtle, refined"
1826
1824
  },
1827
1825
  almadar: {
1828
1826
  label: "Almadar",
1829
- icon: "\u{1F48E}",
1830
1827
  description: "Teal gradients, glowing accents"
1828
+ },
1829
+ "trait-wars": {
1830
+ label: "Trait Wars",
1831
+ description: "Gold parchment, game manuscript"
1832
+ },
1833
+ ocean: {
1834
+ label: "Ocean",
1835
+ description: "Deep sea calm, ocean blues"
1836
+ },
1837
+ forest: {
1838
+ label: "Forest",
1839
+ description: "Woodland serenity, earthy greens"
1840
+ },
1841
+ sunset: {
1842
+ label: "Sunset",
1843
+ description: "Golden hour, warm coral and amber"
1844
+ },
1845
+ lavender: {
1846
+ label: "Lavender",
1847
+ description: "Creative studio, soft violet"
1848
+ },
1849
+ rose: {
1850
+ label: "Rose",
1851
+ description: "Elegant bloom, warm pink"
1852
+ },
1853
+ slate: {
1854
+ label: "Slate",
1855
+ description: "Corporate edge, cool gray"
1856
+ },
1857
+ ember: {
1858
+ label: "Ember",
1859
+ description: "Fire and energy, bold red"
1860
+ },
1861
+ midnight: {
1862
+ label: "Midnight",
1863
+ description: "Noir elegance, deep indigo"
1864
+ },
1865
+ sand: {
1866
+ label: "Sand",
1867
+ description: "Desert minimal, warm earth"
1868
+ },
1869
+ neon: {
1870
+ label: "Neon",
1871
+ description: "Cyberpunk, glowing cyan and pink"
1872
+ },
1873
+ arctic: {
1874
+ label: "Arctic",
1875
+ description: "Ice crystal, cool blue"
1876
+ },
1877
+ copper: {
1878
+ label: "Copper",
1879
+ description: "Warm industrial, metallic bronze"
1831
1880
  }
1832
1881
  };
1882
+ function getThemeLabel(name) {
1883
+ return THEME_LABELS[name] || { label: name, description: name };
1884
+ }
1833
1885
  var ThemeSelector = ({
1834
1886
  className = "",
1835
1887
  variant = "dropdown",
1836
1888
  showLabels = true
1837
1889
  }) => {
1838
- const { designTheme, setDesignTheme, availableThemes } = useDesignTheme();
1890
+ const { theme, setTheme, availableThemes } = useTheme();
1839
1891
  if (variant === "buttons") {
1840
- return /* @__PURE__ */ jsx("div", { className: `flex gap-2 ${className}`, children: availableThemes.map((theme) => {
1841
- const { label, icon } = THEME_LABELS[theme];
1842
- const isActive = designTheme === theme;
1843
- return /* @__PURE__ */ jsxs(
1892
+ return /* @__PURE__ */ jsx("div", { className: `flex gap-2 flex-wrap ${className}`, children: availableThemes.map((t) => {
1893
+ const { label } = getThemeLabel(t.name);
1894
+ const isActive = theme === t.name;
1895
+ return /* @__PURE__ */ jsx(
1844
1896
  "button",
1845
1897
  {
1846
- onClick: () => setDesignTheme(theme),
1898
+ onClick: () => setTheme(t.name),
1847
1899
  className: `
1848
1900
  px-3 py-2 text-sm font-medium transition-all
1849
1901
  border-[length:var(--border-width)] rounded-[var(--radius-sm)]
1850
1902
  ${isActive ? "bg-[var(--color-primary)] text-[var(--color-primary-foreground)] border-[var(--color-primary)]" : "bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)] border-[var(--color-border)] hover:bg-[var(--color-secondary-hover)]"}
1851
1903
  `,
1852
- title: THEME_LABELS[theme].description,
1853
- children: [
1854
- icon,
1855
- " ",
1856
- showLabels && label
1857
- ]
1904
+ title: getThemeLabel(t.name).description,
1905
+ children: showLabels && label
1858
1906
  },
1859
- theme
1907
+ t.name
1860
1908
  );
1861
1909
  }) });
1862
1910
  }
@@ -1864,8 +1912,8 @@ var ThemeSelector = ({
1864
1912
  /* @__PURE__ */ jsx(
1865
1913
  "select",
1866
1914
  {
1867
- value: designTheme,
1868
- onChange: (e) => setDesignTheme(e.target.value),
1915
+ value: theme,
1916
+ onChange: (e) => setTheme(e.target.value),
1869
1917
  className: `
1870
1918
  px-3 py-2 pr-8 text-sm font-medium
1871
1919
  bg-[var(--color-secondary)] text-[var(--color-secondary-foreground)]
@@ -1874,13 +1922,9 @@ var ThemeSelector = ({
1874
1922
  cursor-pointer appearance-none
1875
1923
  focus:outline-none focus:ring-2 focus:ring-[var(--color-ring)]
1876
1924
  `,
1877
- children: availableThemes.map((theme) => {
1878
- const { label, icon } = THEME_LABELS[theme];
1879
- return /* @__PURE__ */ jsxs("option", { value: theme, children: [
1880
- icon,
1881
- " ",
1882
- label
1883
- ] }, theme);
1925
+ children: availableThemes.map((t) => {
1926
+ const { label } = getThemeLabel(t.name);
1927
+ return /* @__PURE__ */ jsx("option", { value: t.name, children: label }, t.name);
1884
1928
  })
1885
1929
  }
1886
1930
  ),
@@ -10163,8 +10207,9 @@ function useCamera() {
10163
10207
 
10164
10208
  // components/organisms/game/utils/isometric.ts
10165
10209
  var TILE_WIDTH = 256;
10166
- var TILE_HEIGHT = 384;
10167
- var FLOOR_HEIGHT = 149;
10210
+ var TILE_HEIGHT = 512;
10211
+ var FLOOR_HEIGHT = 128;
10212
+ var DIAMOND_TOP_Y = 374;
10168
10213
  var FEATURE_COLORS = {
10169
10214
  goldMine: "#fbbf24",
10170
10215
  resonanceCrystal: "#a78bfa",
@@ -10230,6 +10275,8 @@ function IsometricCanvas({
10230
10275
  effectSpriteUrls = [],
10231
10276
  onDrawEffects,
10232
10277
  hasActiveEffects: hasActiveEffects2 = false,
10278
+ // Tuning
10279
+ diamondTopY: diamondTopYProp,
10233
10280
  // Remote asset loading
10234
10281
  assetBaseUrl,
10235
10282
  assetManifest
@@ -10276,6 +10323,8 @@ function IsometricCanvas({
10276
10323
  const scaledTileWidth = TILE_WIDTH * scale;
10277
10324
  const scaledTileHeight = TILE_HEIGHT * scale;
10278
10325
  const scaledFloorHeight = FLOOR_HEIGHT * scale;
10326
+ const effectiveDiamondTopY = diamondTopYProp ?? DIAMOND_TOP_Y;
10327
+ const scaledDiamondTopY = effectiveDiamondTopY * scale;
10279
10328
  const baseOffsetX = useMemo(() => {
10280
10329
  return (gridHeight - 1) * (scaledTileWidth / 2);
10281
10330
  }, [gridHeight, scaledTileWidth]);
@@ -10295,10 +10344,10 @@ function IsometricCanvas({
10295
10344
  for (const tile of sortedTiles) {
10296
10345
  if (tile.terrainSprite) urls.push(tile.terrainSprite);
10297
10346
  else if (getTerrainSprite) {
10298
- const url = getTerrainSprite(tile.terrain);
10347
+ const url = getTerrainSprite(tile.terrain ?? "");
10299
10348
  if (url) urls.push(url);
10300
10349
  } else {
10301
- const url = resolveManifestUrl(assetManifest?.terrains?.[tile.terrain]);
10350
+ const url = resolveManifestUrl(assetManifest?.terrains?.[tile.terrain ?? ""]);
10302
10351
  if (url) urls.push(url);
10303
10352
  }
10304
10353
  }
@@ -10347,7 +10396,7 @@ function IsometricCanvas({
10347
10396
  lerpToTarget
10348
10397
  } = useCamera();
10349
10398
  const resolveTerrainSpriteUrl = useCallback((tile) => {
10350
- return tile.terrainSprite || getTerrainSprite?.(tile.terrain) || resolveManifestUrl(assetManifest?.terrains?.[tile.terrain]);
10399
+ return tile.terrainSprite || getTerrainSprite?.(tile.terrain ?? "") || resolveManifestUrl(assetManifest?.terrains?.[tile.terrain ?? ""]);
10351
10400
  }, [getTerrainSprite, assetManifest, resolveManifestUrl]);
10352
10401
  const resolveFeatureSpriteUrl = useCallback((featureType) => {
10353
10402
  return getFeatureSprite?.(featureType) || resolveManifestUrl(assetManifest?.features?.[featureType]);
@@ -10392,6 +10441,7 @@ function IsometricCanvas({
10392
10441
  mCtx.fill();
10393
10442
  }
10394
10443
  for (const unit of units) {
10444
+ if (!unit.position) continue;
10395
10445
  const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
10396
10446
  const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
10397
10447
  const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
@@ -10458,7 +10508,7 @@ function IsometricCanvas({
10458
10508
  ctx.drawImage(img, pos.x, pos.y, scaledTileWidth, scaledTileHeight);
10459
10509
  } else {
10460
10510
  const centerX = pos.x + scaledTileWidth / 2;
10461
- const topY = pos.y + (scaledTileHeight - scaledFloorHeight);
10511
+ const topY = pos.y + scaledDiamondTopY;
10462
10512
  ctx.fillStyle = tile.terrain === "water" ? "#3b82f6" : tile.terrain === "mountain" ? "#78716c" : tile.terrain === "stone" ? "#9ca3af" : "#4ade80";
10463
10513
  ctx.beginPath();
10464
10514
  ctx.moveTo(centerX, topY);
@@ -10473,7 +10523,7 @@ function IsometricCanvas({
10473
10523
  }
10474
10524
  if (hoveredTile && hoveredTile.x === tile.x && hoveredTile.y === tile.y) {
10475
10525
  const centerX = pos.x + scaledTileWidth / 2;
10476
- const topY = pos.y + (scaledTileHeight - scaledFloorHeight);
10526
+ const topY = pos.y + scaledDiamondTopY;
10477
10527
  ctx.fillStyle = "rgba(255, 255, 255, 0.15)";
10478
10528
  ctx.beginPath();
10479
10529
  ctx.moveTo(centerX, topY);
@@ -10486,7 +10536,7 @@ function IsometricCanvas({
10486
10536
  const tileKey = `${tile.x},${tile.y}`;
10487
10537
  if (validMoveSet.has(tileKey)) {
10488
10538
  const centerX = pos.x + scaledTileWidth / 2;
10489
- const topY = pos.y + (scaledTileHeight - scaledFloorHeight);
10539
+ const topY = pos.y + scaledDiamondTopY;
10490
10540
  const pulse = 0.15 + 0.1 * Math.sin(animTime * 4e-3);
10491
10541
  ctx.fillStyle = `rgba(74, 222, 128, ${pulse})`;
10492
10542
  ctx.beginPath();
@@ -10499,7 +10549,7 @@ function IsometricCanvas({
10499
10549
  }
10500
10550
  if (attackTargetSet.has(tileKey)) {
10501
10551
  const centerX = pos.x + scaledTileWidth / 2;
10502
- const topY = pos.y + (scaledTileHeight - scaledFloorHeight);
10552
+ const topY = pos.y + scaledDiamondTopY;
10503
10553
  const pulse = 0.2 + 0.15 * Math.sin(animTime * 5e-3);
10504
10554
  ctx.fillStyle = `rgba(239, 68, 68, ${pulse})`;
10505
10555
  ctx.beginPath();
@@ -10512,7 +10562,7 @@ function IsometricCanvas({
10512
10562
  }
10513
10563
  if (debug2) {
10514
10564
  const centerX = pos.x + scaledTileWidth / 2;
10515
- const centerY = pos.y + scaledFloorHeight / 2 + (scaledTileHeight - scaledFloorHeight);
10565
+ const centerY = pos.y + scaledFloorHeight / 2 + scaledDiamondTopY;
10516
10566
  ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
10517
10567
  ctx.font = `${12 * scale * 2}px monospace`;
10518
10568
  ctx.textAlign = "center";
@@ -10532,7 +10582,7 @@ function IsometricCanvas({
10532
10582
  const spriteUrl = feature.sprite || resolveFeatureSpriteUrl(feature.type);
10533
10583
  const img = spriteUrl ? getImage(spriteUrl) : null;
10534
10584
  const centerX = pos.x + scaledTileWidth / 2;
10535
- const featureGroundY = pos.y + (scaledTileHeight - scaledFloorHeight) + scaledFloorHeight * 0.5;
10585
+ const featureGroundY = pos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
10536
10586
  const isCastle = feature.type === "castle";
10537
10587
  const featureDrawH = isCastle ? scaledFloorHeight * 3.5 : scaledFloorHeight * 1.6;
10538
10588
  const maxFeatureW = isCastle ? scaledTileWidth * 1.8 : scaledTileWidth * 0.7;
@@ -10558,7 +10608,8 @@ function IsometricCanvas({
10558
10608
  ctx.stroke();
10559
10609
  }
10560
10610
  }
10561
- const sortedUnits = [...units].sort((a, b) => {
10611
+ const unitsWithPosition = units.filter((u) => !!u.position);
10612
+ const sortedUnits = [...unitsWithPosition].sort((a, b) => {
10562
10613
  const depthA = a.position.x + a.position.y;
10563
10614
  const depthB = b.position.x + b.position.y;
10564
10615
  return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
@@ -10570,7 +10621,7 @@ function IsometricCanvas({
10570
10621
  }
10571
10622
  const isSelected = unit.id === selectedUnitId;
10572
10623
  const centerX = pos.x + scaledTileWidth / 2;
10573
- const groundY = pos.y + (scaledTileHeight - scaledFloorHeight) + scaledFloorHeight * 0.5;
10624
+ const groundY = pos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
10574
10625
  const breatheOffset = 0.8 * scale * (1 + Math.sin(animTime * 2e-3 + (unit.position.x * 3.7 + unit.position.y * 5.3)));
10575
10626
  const unitSpriteUrl = resolveUnitSpriteUrl(unit);
10576
10627
  const img = unitSpriteUrl ? getImage(unitSpriteUrl) : null;
@@ -10586,7 +10637,7 @@ function IsometricCanvas({
10586
10637
  if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
10587
10638
  const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
10588
10639
  const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
10589
- const ghostGroundY = ghostPos.y + (scaledTileHeight - scaledFloorHeight) + scaledFloorHeight * 0.5;
10640
+ const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
10590
10641
  ctx.save();
10591
10642
  ctx.globalAlpha = 0.25;
10592
10643
  if (img) {
@@ -10726,6 +10777,7 @@ function IsometricCanvas({
10726
10777
  scaledTileWidth,
10727
10778
  scaledTileHeight,
10728
10779
  scaledFloorHeight,
10780
+ scaledDiamondTopY,
10729
10781
  validMoveSet,
10730
10782
  attackTargetSet,
10731
10783
  hoveredTile,
@@ -10739,15 +10791,15 @@ function IsometricCanvas({
10739
10791
  useEffect(() => {
10740
10792
  if (!selectedUnitId) return;
10741
10793
  const unit = units.find((u) => u.id === selectedUnitId);
10742
- if (!unit) return;
10794
+ if (!unit?.position) return;
10743
10795
  const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
10744
10796
  const centerX = pos.x + scaledTileWidth / 2;
10745
- const centerY = pos.y + (scaledTileHeight - scaledFloorHeight) + scaledFloorHeight / 2;
10797
+ const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
10746
10798
  targetCameraRef.current = {
10747
10799
  x: centerX - viewportSize.width / 2,
10748
10800
  y: centerY - viewportSize.height / 2
10749
10801
  };
10750
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, targetCameraRef]);
10802
+ }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
10751
10803
  useEffect(() => {
10752
10804
  const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2;
10753
10805
  draw(animTimeRef.current);
@@ -10774,14 +10826,14 @@ function IsometricCanvas({
10774
10826
  if (!onTileHover && !tileHoverEvent || !canvasRef.current) return;
10775
10827
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10776
10828
  const adjustedX = world.x - scaledTileWidth / 2;
10777
- const adjustedY = world.y - (scaledTileHeight - scaledFloorHeight) - scaledFloorHeight / 2;
10829
+ const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10778
10830
  const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
10779
10831
  const tileExists = tilesProp.some((t) => t.x === isoPos.x && t.y === isoPos.y);
10780
10832
  if (tileExists) {
10781
10833
  if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
10782
10834
  onTileHover?.(isoPos.x, isoPos.y);
10783
10835
  }
10784
- }, [enableCamera, handleMouseMove, draw, onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledTileHeight, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
10836
+ }, [enableCamera, handleMouseMove, draw, onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
10785
10837
  const handleMouseLeaveWithCamera = useCallback(() => {
10786
10838
  handleMouseLeave();
10787
10839
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -10797,9 +10849,9 @@ function IsometricCanvas({
10797
10849
  if (!canvasRef.current) return;
10798
10850
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10799
10851
  const adjustedX = world.x - scaledTileWidth / 2;
10800
- const adjustedY = world.y - (scaledTileHeight - scaledFloorHeight) - scaledFloorHeight / 2;
10852
+ const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10801
10853
  const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
10802
- const clickedUnit = units.find((u) => u.position.x === isoPos.x && u.position.y === isoPos.y);
10854
+ const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
10803
10855
  if (clickedUnit && (onUnitClick || unitClickEvent)) {
10804
10856
  if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
10805
10857
  onUnitClick?.(clickedUnit.id);
@@ -10810,7 +10862,7 @@ function IsometricCanvas({
10810
10862
  onTileClick?.(isoPos.x, isoPos.y);
10811
10863
  }
10812
10864
  }
10813
- }, [dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledTileHeight, scaledFloorHeight, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
10865
+ }, [dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
10814
10866
  if (error) {
10815
10867
  return /* @__PURE__ */ jsx(ErrorState, { title: "Canvas Error", message: error.message, className });
10816
10868
  }
@@ -12229,7 +12281,7 @@ function useSpriteAnimations(getSheetUrls, getFrameDims, options = {}) {
12229
12281
  const sheetUrls = getSheetUrls(unit);
12230
12282
  if (!sheetUrls) continue;
12231
12283
  const prev = prevPos.get(unit.id);
12232
- if (prev) {
12284
+ if (prev && unit.position) {
12233
12285
  const dx = unit.position.x - prev.x;
12234
12286
  const dy = unit.position.y - prev.y;
12235
12287
  if (dx !== 0 || dy !== 0) {
@@ -12248,7 +12300,9 @@ function useSpriteAnimations(getSheetUrls, getFrameDims, options = {}) {
12248
12300
  }
12249
12301
  }
12250
12302
  }
12251
- prevPos.set(unit.id, { ...unit.position });
12303
+ if (unit.position) {
12304
+ prevPos.set(unit.id, { x: unit.position.x, y: unit.position.y });
12305
+ }
12252
12306
  state = tickAnimationState(state, scaledDelta);
12253
12307
  states.set(unit.id, state);
12254
12308
  }
@@ -12287,6 +12341,283 @@ function useSpriteAnimations(getSheetUrls, getFrameDims, options = {}) {
12287
12341
  }, [getSheetUrls, getFrameDims]);
12288
12342
  return { syncUnits, setUnitAnimation, resolveUnitFrame };
12289
12343
  }
12344
+
12345
+ // components/organisms/game/managers/PhysicsManager.ts
12346
+ var PhysicsManager = class {
12347
+ constructor(config = {}) {
12348
+ __publicField(this, "entities", /* @__PURE__ */ new Map());
12349
+ __publicField(this, "config");
12350
+ this.config = {
12351
+ gravity: 0.5,
12352
+ friction: 0.8,
12353
+ airResistance: 0.99,
12354
+ maxVelocityY: 20,
12355
+ groundY: 500,
12356
+ // Default ground position in pixels
12357
+ ...config
12358
+ };
12359
+ }
12360
+ /**
12361
+ * Register an entity for physics simulation
12362
+ */
12363
+ registerEntity(entityId, initialState = {}) {
12364
+ const state = {
12365
+ id: entityId,
12366
+ x: initialState.x ?? 0,
12367
+ y: initialState.y ?? 0,
12368
+ vx: initialState.vx ?? 0,
12369
+ vy: initialState.vy ?? 0,
12370
+ isGrounded: initialState.isGrounded ?? false,
12371
+ gravity: initialState.gravity ?? this.config.gravity,
12372
+ friction: initialState.friction ?? this.config.friction,
12373
+ airResistance: initialState.airResistance ?? this.config.airResistance,
12374
+ maxVelocityY: initialState.maxVelocityY ?? this.config.maxVelocityY,
12375
+ mass: initialState.mass ?? 1,
12376
+ restitution: initialState.restitution ?? 0.8,
12377
+ state: initialState.state ?? "Active"
12378
+ };
12379
+ this.entities.set(entityId, state);
12380
+ return state;
12381
+ }
12382
+ /**
12383
+ * Unregister an entity from physics simulation
12384
+ */
12385
+ unregisterEntity(entityId) {
12386
+ this.entities.delete(entityId);
12387
+ }
12388
+ /**
12389
+ * Get physics state for an entity
12390
+ */
12391
+ getState(entityId) {
12392
+ return this.entities.get(entityId);
12393
+ }
12394
+ /**
12395
+ * Get all registered entities
12396
+ */
12397
+ getAllEntities() {
12398
+ return Array.from(this.entities.values());
12399
+ }
12400
+ /**
12401
+ * Apply a force to an entity (impulse)
12402
+ */
12403
+ applyForce(entityId, fx, fy) {
12404
+ const state = this.entities.get(entityId);
12405
+ if (!state || state.state !== "Active") return;
12406
+ state.vx += fx;
12407
+ state.vy += fy;
12408
+ }
12409
+ /**
12410
+ * Set velocity directly
12411
+ */
12412
+ setVelocity(entityId, vx, vy) {
12413
+ const state = this.entities.get(entityId);
12414
+ if (!state) return;
12415
+ state.vx = vx;
12416
+ state.vy = vy;
12417
+ }
12418
+ /**
12419
+ * Set position directly
12420
+ */
12421
+ setPosition(entityId, x, y) {
12422
+ const state = this.entities.get(entityId);
12423
+ if (!state) return;
12424
+ state.x = x;
12425
+ state.y = y;
12426
+ }
12427
+ /**
12428
+ * Freeze/unfreeze an entity
12429
+ */
12430
+ setFrozen(entityId, frozen) {
12431
+ const state = this.entities.get(entityId);
12432
+ if (!state) return;
12433
+ state.state = frozen ? "Frozen" : "Active";
12434
+ }
12435
+ /**
12436
+ * Main tick function - call this every frame
12437
+ * Implements the logic from std-physics2d ticks
12438
+ */
12439
+ tick(deltaTime = 16) {
12440
+ for (const state of this.entities.values()) {
12441
+ if (state.state !== "Active") continue;
12442
+ this.applyGravity(state, deltaTime);
12443
+ this.applyVelocity(state, deltaTime);
12444
+ this.checkGroundCollision(state);
12445
+ }
12446
+ }
12447
+ /**
12448
+ * ApplyGravity tick implementation
12449
+ */
12450
+ applyGravity(state, deltaTime) {
12451
+ if (state.isGrounded) return;
12452
+ const gravityForce = state.gravity * (deltaTime / 16);
12453
+ state.vy = Math.min(state.maxVelocityY, state.vy + gravityForce);
12454
+ }
12455
+ /**
12456
+ * ApplyVelocity tick implementation
12457
+ */
12458
+ applyVelocity(state, deltaTime) {
12459
+ const dt = deltaTime / 16;
12460
+ state.vx *= Math.pow(state.airResistance, dt);
12461
+ state.x += state.vx * dt;
12462
+ state.y += state.vy * dt;
12463
+ }
12464
+ /**
12465
+ * Check and handle ground collision
12466
+ */
12467
+ checkGroundCollision(state) {
12468
+ const groundY = this.config.groundY;
12469
+ if (state.y >= groundY) {
12470
+ state.y = groundY;
12471
+ state.isGrounded = true;
12472
+ state.vy = 0;
12473
+ state.vx *= state.friction;
12474
+ if (Math.abs(state.vx) < 0.01) {
12475
+ state.vx = 0;
12476
+ }
12477
+ } else {
12478
+ state.isGrounded = false;
12479
+ }
12480
+ }
12481
+ /**
12482
+ * Check AABB collision between two entities
12483
+ */
12484
+ checkCollision(entityIdA, entityIdB, boundsA, boundsB) {
12485
+ const stateA = this.entities.get(entityIdA);
12486
+ const stateB = this.entities.get(entityIdB);
12487
+ if (!stateA || !stateB) return false;
12488
+ const absBoundsA = {
12489
+ x: stateA.x + boundsA.x,
12490
+ y: stateA.y + boundsA.y,
12491
+ width: boundsA.width,
12492
+ height: boundsA.height
12493
+ };
12494
+ const absBoundsB = {
12495
+ x: stateB.x + boundsB.x,
12496
+ y: stateB.y + boundsB.y,
12497
+ width: boundsB.width,
12498
+ height: boundsB.height
12499
+ };
12500
+ return absBoundsA.x < absBoundsB.x + absBoundsB.width && absBoundsA.x + absBoundsA.width > absBoundsB.x && absBoundsA.y < absBoundsB.y + absBoundsB.height && absBoundsA.y + absBoundsA.height > absBoundsB.y;
12501
+ }
12502
+ /**
12503
+ * Resolve collision with bounce
12504
+ */
12505
+ resolveCollision(entityIdA, entityIdB) {
12506
+ const stateA = this.entities.get(entityIdA);
12507
+ const stateB = this.entities.get(entityIdB);
12508
+ if (!stateA || !stateB) return;
12509
+ const restitution = Math.min(stateA.restitution ?? 0.8, stateB.restitution ?? 0.8);
12510
+ const tempVx = stateA.vx;
12511
+ const tempVy = stateA.vy;
12512
+ stateA.vx = stateB.vx * restitution;
12513
+ stateA.vy = stateB.vy * restitution;
12514
+ stateB.vx = tempVx * restitution;
12515
+ stateB.vy = tempVy * restitution;
12516
+ const dx = stateB.x - stateA.x;
12517
+ const dy = stateB.y - stateA.y;
12518
+ const distance = Math.sqrt(dx * dx + dy * dy);
12519
+ if (distance > 0) {
12520
+ const overlap = 1;
12521
+ const nx = dx / distance;
12522
+ const ny = dy / distance;
12523
+ stateA.x -= nx * overlap * 0.5;
12524
+ stateA.y -= ny * overlap * 0.5;
12525
+ stateB.x += nx * overlap * 0.5;
12526
+ stateB.y += ny * overlap * 0.5;
12527
+ }
12528
+ }
12529
+ /**
12530
+ * Reset all physics state
12531
+ */
12532
+ reset() {
12533
+ this.entities.clear();
12534
+ }
12535
+ };
12536
+
12537
+ // components/organisms/game/hooks/usePhysics2D.ts
12538
+ function usePhysics2D(options = {}) {
12539
+ const physicsManagerRef = useRef(null);
12540
+ const collisionCallbacksRef = useRef(/* @__PURE__ */ new Set());
12541
+ if (!physicsManagerRef.current) {
12542
+ physicsManagerRef.current = new PhysicsManager({
12543
+ gravity: options.gravity,
12544
+ friction: options.friction,
12545
+ airResistance: options.airResistance,
12546
+ maxVelocityY: options.maxVelocityY,
12547
+ groundY: options.groundY
12548
+ });
12549
+ }
12550
+ const manager = physicsManagerRef.current;
12551
+ useEffect(() => {
12552
+ if (options.onCollision) {
12553
+ collisionCallbacksRef.current.add(options.onCollision);
12554
+ }
12555
+ return () => {
12556
+ if (options.onCollision) {
12557
+ collisionCallbacksRef.current.delete(options.onCollision);
12558
+ }
12559
+ };
12560
+ }, [options.onCollision]);
12561
+ const registerUnit = useCallback((unitId, initialState = {}) => {
12562
+ manager.registerEntity(unitId, initialState);
12563
+ }, [manager]);
12564
+ const unregisterUnit = useCallback((unitId) => {
12565
+ manager.unregisterEntity(unitId);
12566
+ }, [manager]);
12567
+ const getPosition = useCallback((unitId) => {
12568
+ const state = manager.getState(unitId);
12569
+ if (!state) return null;
12570
+ return { x: state.x, y: state.y };
12571
+ }, [manager]);
12572
+ const getState = useCallback((unitId) => {
12573
+ return manager.getState(unitId);
12574
+ }, [manager]);
12575
+ const applyForce = useCallback((unitId, fx, fy) => {
12576
+ manager.applyForce(unitId, fx, fy);
12577
+ }, [manager]);
12578
+ const setVelocity = useCallback((unitId, vx, vy) => {
12579
+ manager.setVelocity(unitId, vx, vy);
12580
+ }, [manager]);
12581
+ const setPosition = useCallback((unitId, x, y) => {
12582
+ manager.setPosition(unitId, x, y);
12583
+ }, [manager]);
12584
+ const tick = useCallback((deltaTime = 16) => {
12585
+ manager.tick(deltaTime);
12586
+ }, [manager]);
12587
+ const checkCollision = useCallback((unitIdA, unitIdB, boundsA, boundsB) => {
12588
+ return manager.checkCollision(unitIdA, unitIdB, boundsA, boundsB);
12589
+ }, [manager]);
12590
+ const resolveCollision = useCallback((unitIdA, unitIdB) => {
12591
+ manager.resolveCollision(unitIdA, unitIdB);
12592
+ collisionCallbacksRef.current.forEach((callback) => {
12593
+ callback(unitIdA, unitIdB);
12594
+ });
12595
+ }, [manager]);
12596
+ const setFrozen = useCallback((unitId, frozen) => {
12597
+ manager.setFrozen(unitId, frozen);
12598
+ }, [manager]);
12599
+ const getAllUnits = useCallback(() => {
12600
+ return manager.getAllEntities();
12601
+ }, [manager]);
12602
+ const reset = useCallback(() => {
12603
+ manager.reset();
12604
+ }, [manager]);
12605
+ return {
12606
+ registerUnit,
12607
+ unregisterUnit,
12608
+ getPosition,
12609
+ getState,
12610
+ applyForce,
12611
+ setVelocity,
12612
+ setPosition,
12613
+ tick,
12614
+ checkCollision,
12615
+ resolveCollision,
12616
+ setFrozen,
12617
+ getAllUnits,
12618
+ reset
12619
+ };
12620
+ }
12290
12621
  var sizeMap4 = {
12291
12622
  sm: "text-xs px-2 py-1",
12292
12623
  md: "text-sm px-3 py-1.5",
@@ -12981,172 +13312,1032 @@ function DialogueBox({
12981
13312
  }
12982
13313
  );
12983
13314
  }
12984
- function VStackPattern({
12985
- gap = "md",
12986
- align = "stretch",
12987
- justify = "start",
12988
- className,
12989
- style,
12990
- children
12991
- }) {
12992
- return /* @__PURE__ */ jsx(VStack, { gap, align, justify, className, style, children });
12993
- }
12994
- VStackPattern.displayName = "VStackPattern";
12995
- function HStackPattern({
12996
- gap = "md",
12997
- align = "center",
12998
- justify = "start",
12999
- wrap = false,
13000
- className,
13001
- style,
13002
- children
13003
- }) {
13004
- return /* @__PURE__ */ jsx(HStack, { gap, align, justify, wrap, className, style, children });
13005
- }
13006
- HStackPattern.displayName = "HStackPattern";
13007
- function BoxPattern({
13008
- p: p2,
13009
- m,
13010
- bg = "transparent",
13011
- border = false,
13012
- radius = "none",
13013
- shadow = "none",
13014
- className,
13015
- style,
13016
- children
13017
- }) {
13018
- return /* @__PURE__ */ jsx(
13019
- Box,
13020
- {
13021
- padding: p2,
13022
- margin: m,
13023
- bg,
13024
- border,
13025
- rounded: radius,
13026
- shadow,
13027
- className,
13028
- style,
13029
- children
13030
- }
13031
- );
13032
- }
13033
- BoxPattern.displayName = "BoxPattern";
13034
- function GridPattern({
13035
- cols = 1,
13036
- gap = "md",
13037
- rowGap,
13038
- colGap,
13039
- className,
13040
- style,
13041
- children
13042
- }) {
13043
- return /* @__PURE__ */ jsx(Grid2, { cols, gap, rowGap, colGap, className, style, children });
13044
- }
13045
- GridPattern.displayName = "GridPattern";
13046
- function CenterPattern({
13047
- minHeight,
13048
- className,
13049
- style,
13050
- children
13051
- }) {
13052
- const mergedStyle = minHeight ? { minHeight, ...style } : style;
13053
- return /* @__PURE__ */ jsx(Center, { className, style: mergedStyle, children });
13054
- }
13055
- CenterPattern.displayName = "CenterPattern";
13056
- function SpacerPattern({ size = "flex" }) {
13057
- if (size === "flex") {
13058
- return /* @__PURE__ */ jsx(Spacer, {});
13059
- }
13060
- const sizeMap5 = {
13061
- xs: "0.25rem",
13062
- sm: "0.5rem",
13063
- md: "1rem",
13064
- lg: "1.5rem",
13065
- xl: "2rem"
13066
- };
13067
- return /* @__PURE__ */ jsx("div", { style: { width: sizeMap5[size], height: sizeMap5[size], flexShrink: 0 } });
13068
- }
13069
- SpacerPattern.displayName = "SpacerPattern";
13070
- function DividerPattern({
13071
- orientation = "horizontal",
13072
- variant = "solid",
13073
- spacing = "md"
13074
- }) {
13075
- const spacingMap = {
13076
- xs: "my-1",
13077
- sm: "my-2",
13078
- md: "my-4",
13079
- lg: "my-6"
13080
- };
13081
- const verticalSpacingMap = {
13082
- xs: "mx-1",
13083
- sm: "mx-2",
13084
- md: "mx-4",
13085
- lg: "mx-6"
13086
- };
13087
- return /* @__PURE__ */ jsx(
13088
- Divider,
13089
- {
13090
- orientation,
13091
- variant,
13092
- className: orientation === "horizontal" ? spacingMap[spacing] : verticalSpacingMap[spacing]
13093
- }
13094
- );
13095
- }
13096
- DividerPattern.displayName = "DividerPattern";
13097
- function ButtonPattern({
13098
- label,
13099
- variant = "primary",
13100
- size = "md",
13101
- disabled = false,
13102
- onClick,
13103
- icon,
13104
- iconPosition = "left",
13315
+ function BattleBoard({
13316
+ entity,
13317
+ scale = 0.45,
13318
+ unitScale = 1,
13319
+ header,
13320
+ sidebar,
13321
+ actions,
13322
+ overlay,
13323
+ gameOverOverlay,
13324
+ onAttack,
13325
+ onGameEnd,
13326
+ onUnitMove,
13327
+ calculateDamage,
13328
+ onDrawEffects,
13329
+ hasActiveEffects: hasActiveEffects2 = false,
13330
+ effectSpriteUrls = [],
13331
+ resolveUnitFrame,
13332
+ tileClickEvent,
13333
+ unitClickEvent,
13334
+ endTurnEvent,
13335
+ cancelEvent,
13336
+ gameEndEvent,
13337
+ playAgainEvent,
13338
+ attackEvent,
13105
13339
  className
13106
13340
  }) {
13107
- const { emit } = useEventBus();
13108
- const handleClick = () => {
13109
- if (onClick && !disabled) {
13110
- emit(`UI:${onClick}`, {});
13111
- }
13112
- };
13113
- return /* @__PURE__ */ jsxs(
13114
- Button,
13115
- {
13116
- variant,
13117
- size,
13118
- disabled,
13119
- onClick: handleClick,
13120
- className,
13121
- children: [
13122
- icon && iconPosition === "left" && /* @__PURE__ */ jsx(Icon, { name: icon, size: "sm" }),
13123
- label,
13124
- icon && iconPosition === "right" && /* @__PURE__ */ jsx(Icon, { name: icon, size: "sm" })
13125
- ]
13126
- }
13341
+ const initialUnits = entity.initialUnits;
13342
+ const tiles = entity.tiles;
13343
+ const features = entity.features ?? [];
13344
+ const boardWidth = entity.boardWidth ?? 8;
13345
+ const boardHeight = entity.boardHeight ?? 6;
13346
+ const assetManifest = entity.assetManifest;
13347
+ const backgroundImage = entity.backgroundImage;
13348
+ const eventBus = useEventBus();
13349
+ const [units, setUnits] = useState(initialUnits);
13350
+ const [selectedUnitId, setSelectedUnitId] = useState(null);
13351
+ const [hoveredTile, setHoveredTile] = useState(null);
13352
+ const [currentPhase, setCurrentPhase] = useState("observation");
13353
+ const [currentTurn, setCurrentTurn] = useState(1);
13354
+ const [gameResult, setGameResult] = useState(null);
13355
+ const [isShaking, setIsShaking] = useState(false);
13356
+ const selectedUnit = useMemo(
13357
+ () => units.find((u) => u.id === selectedUnitId) ?? null,
13358
+ [units, selectedUnitId]
13127
13359
  );
13128
- }
13129
- ButtonPattern.displayName = "ButtonPattern";
13130
- function IconButtonPattern({
13131
- icon,
13132
- variant = "ghost",
13133
- size = "md",
13134
- onClick,
13135
- ariaLabel,
13136
- className
13137
- }) {
13138
- const { emit } = useEventBus();
13139
- const handleClick = () => {
13140
- if (onClick) {
13141
- emit(`UI:${onClick}`, {});
13142
- }
13143
- };
13144
- return /* @__PURE__ */ jsx(
13145
- Button,
13146
- {
13147
- variant,
13148
- size,
13149
- onClick: handleClick,
13360
+ const hoveredUnit = useMemo(() => {
13361
+ if (!hoveredTile) return null;
13362
+ return units.find(
13363
+ (u) => u.position.x === hoveredTile.x && u.position.y === hoveredTile.y && u.health > 0
13364
+ ) ?? null;
13365
+ }, [hoveredTile, units]);
13366
+ const playerUnits = useMemo(() => units.filter((u) => u.team === "player" && u.health > 0), [units]);
13367
+ const enemyUnits = useMemo(() => units.filter((u) => u.team === "enemy" && u.health > 0), [units]);
13368
+ const validMoves = useMemo(() => {
13369
+ if (!selectedUnit || currentPhase !== "movement") return [];
13370
+ const moves = [];
13371
+ const range = selectedUnit.movement;
13372
+ for (let dy = -range; dy <= range; dy++) {
13373
+ for (let dx = -range; dx <= range; dx++) {
13374
+ const nx = selectedUnit.position.x + dx;
13375
+ const ny = selectedUnit.position.y + dy;
13376
+ const dist = Math.abs(dx) + Math.abs(dy);
13377
+ if (dist > 0 && dist <= range && nx >= 0 && nx < boardWidth && ny >= 0 && ny < boardHeight && !units.some((u) => u.position.x === nx && u.position.y === ny && u.health > 0)) {
13378
+ moves.push({ x: nx, y: ny });
13379
+ }
13380
+ }
13381
+ }
13382
+ return moves;
13383
+ }, [selectedUnit, currentPhase, units, boardWidth, boardHeight]);
13384
+ const attackTargets = useMemo(() => {
13385
+ if (!selectedUnit || currentPhase !== "action") return [];
13386
+ return units.filter((u) => u.team !== selectedUnit.team && u.health > 0).filter((u) => {
13387
+ const dx = Math.abs(u.position.x - selectedUnit.position.x);
13388
+ const dy = Math.abs(u.position.y - selectedUnit.position.y);
13389
+ return dx <= 1 && dy <= 1 && dx + dy > 0;
13390
+ }).map((u) => u.position);
13391
+ }, [selectedUnit, currentPhase, units]);
13392
+ const MOVE_SPEED_MS_PER_TILE = 300;
13393
+ const movementAnimRef = useRef(null);
13394
+ const [movingPositions, setMovingPositions] = useState(/* @__PURE__ */ new Map());
13395
+ const startMoveAnimation = useCallback((unitId, from, to, onComplete) => {
13396
+ const dx = to.x - from.x;
13397
+ const dy = to.y - from.y;
13398
+ const dist = Math.max(Math.abs(dx), Math.abs(dy));
13399
+ const duration = dist * MOVE_SPEED_MS_PER_TILE;
13400
+ movementAnimRef.current = { unitId, from, to, elapsed: 0, duration, onComplete };
13401
+ }, []);
13402
+ useEffect(() => {
13403
+ const interval = setInterval(() => {
13404
+ const anim2 = movementAnimRef.current;
13405
+ if (!anim2) return;
13406
+ anim2.elapsed += 16;
13407
+ const t = Math.min(anim2.elapsed / anim2.duration, 1);
13408
+ const eased = 1 - (1 - t) * (1 - t);
13409
+ const cx = anim2.from.x + (anim2.to.x - anim2.from.x) * eased;
13410
+ const cy = anim2.from.y + (anim2.to.y - anim2.from.y) * eased;
13411
+ if (t >= 1) {
13412
+ movementAnimRef.current = null;
13413
+ setMovingPositions((prev) => {
13414
+ const next = new Map(prev);
13415
+ next.delete(anim2.unitId);
13416
+ return next;
13417
+ });
13418
+ anim2.onComplete();
13419
+ } else {
13420
+ setMovingPositions((prev) => {
13421
+ const next = new Map(prev);
13422
+ next.set(anim2.unitId, { x: cx, y: cy });
13423
+ return next;
13424
+ });
13425
+ }
13426
+ }, 16);
13427
+ return () => clearInterval(interval);
13428
+ }, []);
13429
+ const isoUnits = useMemo(() => {
13430
+ return units.filter((u) => u.health > 0).map((unit) => {
13431
+ const pos = movingPositions.get(unit.id) ?? unit.position;
13432
+ return {
13433
+ id: unit.id,
13434
+ position: pos,
13435
+ name: unit.name,
13436
+ team: unit.team,
13437
+ health: unit.health,
13438
+ maxHealth: unit.maxHealth,
13439
+ unitType: unit.unitType,
13440
+ heroId: unit.heroId,
13441
+ sprite: unit.sprite,
13442
+ traits: unit.traits?.map((t) => ({
13443
+ name: t.name,
13444
+ currentState: t.currentState,
13445
+ states: t.states,
13446
+ cooldown: t.cooldown ?? 0
13447
+ }))
13448
+ };
13449
+ });
13450
+ }, [units, movingPositions]);
13451
+ const maxY = Math.max(...tiles.map((t) => t.y), 0);
13452
+ const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
13453
+ const tileToScreen = useCallback(
13454
+ (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
13455
+ [scale, baseOffsetX]
13456
+ );
13457
+ const checkGameEnd = useCallback(() => {
13458
+ const pa = units.filter((u) => u.team === "player" && u.health > 0);
13459
+ const ea = units.filter((u) => u.team === "enemy" && u.health > 0);
13460
+ if (pa.length === 0) {
13461
+ setGameResult("defeat");
13462
+ setCurrentPhase("game_over");
13463
+ onGameEnd?.("defeat");
13464
+ if (gameEndEvent) {
13465
+ eventBus.emit(`UI:${gameEndEvent}`, { result: "defeat" });
13466
+ }
13467
+ } else if (ea.length === 0) {
13468
+ setGameResult("victory");
13469
+ setCurrentPhase("game_over");
13470
+ onGameEnd?.("victory");
13471
+ if (gameEndEvent) {
13472
+ eventBus.emit(`UI:${gameEndEvent}`, { result: "victory" });
13473
+ }
13474
+ }
13475
+ }, [units, onGameEnd, gameEndEvent, eventBus]);
13476
+ const handleUnitClick = useCallback((unitId) => {
13477
+ const unit = units.find((u) => u.id === unitId);
13478
+ if (!unit) return;
13479
+ if (unitClickEvent) {
13480
+ eventBus.emit(`UI:${unitClickEvent}`, { unitId });
13481
+ }
13482
+ if (currentPhase === "observation" || currentPhase === "selection") {
13483
+ if (unit.team === "player") {
13484
+ setSelectedUnitId(unitId);
13485
+ setCurrentPhase("movement");
13486
+ }
13487
+ } else if (currentPhase === "action" && selectedUnit) {
13488
+ if (unit.team === "enemy" && attackTargets.some((t) => t.x === unit.position.x && t.y === unit.position.y)) {
13489
+ const damage = calculateDamage ? calculateDamage(selectedUnit, unit) : Math.max(1, selectedUnit.attack - unit.defense);
13490
+ const newHealth = Math.max(0, unit.health - damage);
13491
+ setUnits((prev) => prev.map((u) => u.id === unit.id ? { ...u, health: newHealth } : u));
13492
+ setIsShaking(true);
13493
+ setTimeout(() => setIsShaking(false), 300);
13494
+ onAttack?.(selectedUnit, unit, damage);
13495
+ if (attackEvent) {
13496
+ eventBus.emit(`UI:${attackEvent}`, {
13497
+ attackerId: selectedUnit.id,
13498
+ targetId: unit.id,
13499
+ damage
13500
+ });
13501
+ }
13502
+ setSelectedUnitId(null);
13503
+ setCurrentPhase("observation");
13504
+ setCurrentTurn((t) => t + 1);
13505
+ setTimeout(checkGameEnd, 100);
13506
+ }
13507
+ }
13508
+ }, [currentPhase, selectedUnit, attackTargets, units, checkGameEnd, onAttack, calculateDamage, unitClickEvent, attackEvent, eventBus]);
13509
+ const handleTileClick = useCallback((x, y) => {
13510
+ if (tileClickEvent) {
13511
+ eventBus.emit(`UI:${tileClickEvent}`, { x, y });
13512
+ }
13513
+ if (currentPhase === "movement" && selectedUnit) {
13514
+ if (movementAnimRef.current) return;
13515
+ if (validMoves.some((m) => m.x === x && m.y === y)) {
13516
+ const from = { ...selectedUnit.position };
13517
+ const to = { x, y };
13518
+ startMoveAnimation(selectedUnit.id, from, to, () => {
13519
+ setUnits(
13520
+ (prev) => prev.map((u) => u.id === selectedUnitId ? { ...u, position: { x, y } } : u)
13521
+ );
13522
+ onUnitMove?.(selectedUnit, to);
13523
+ setCurrentPhase("action");
13524
+ });
13525
+ }
13526
+ }
13527
+ }, [currentPhase, selectedUnit, selectedUnitId, validMoves, startMoveAnimation, onUnitMove, tileClickEvent, eventBus]);
13528
+ const handleEndTurn = useCallback(() => {
13529
+ setSelectedUnitId(null);
13530
+ setCurrentPhase("observation");
13531
+ setCurrentTurn((t) => t + 1);
13532
+ if (endTurnEvent) {
13533
+ eventBus.emit(`UI:${endTurnEvent}`, {});
13534
+ }
13535
+ }, [endTurnEvent, eventBus]);
13536
+ const handleCancel = useCallback(() => {
13537
+ setSelectedUnitId(null);
13538
+ setCurrentPhase("observation");
13539
+ if (cancelEvent) {
13540
+ eventBus.emit(`UI:${cancelEvent}`, {});
13541
+ }
13542
+ }, [cancelEvent, eventBus]);
13543
+ const handleReset = useCallback(() => {
13544
+ setUnits(initialUnits);
13545
+ setSelectedUnitId(null);
13546
+ setCurrentPhase("observation");
13547
+ setCurrentTurn(1);
13548
+ setGameResult(null);
13549
+ if (playAgainEvent) {
13550
+ eventBus.emit(`UI:${playAgainEvent}`, {});
13551
+ }
13552
+ }, [initialUnits, playAgainEvent, eventBus]);
13553
+ const ctx = useMemo(
13554
+ () => ({
13555
+ phase: currentPhase,
13556
+ turn: currentTurn,
13557
+ selectedUnit,
13558
+ hoveredUnit,
13559
+ playerUnits,
13560
+ enemyUnits,
13561
+ gameResult,
13562
+ onEndTurn: handleEndTurn,
13563
+ onCancel: handleCancel,
13564
+ onReset: handleReset,
13565
+ attackTargets,
13566
+ tileToScreen
13567
+ }),
13568
+ [
13569
+ currentPhase,
13570
+ currentTurn,
13571
+ selectedUnit,
13572
+ hoveredUnit,
13573
+ playerUnits,
13574
+ enemyUnits,
13575
+ gameResult,
13576
+ handleEndTurn,
13577
+ handleCancel,
13578
+ handleReset,
13579
+ attackTargets,
13580
+ tileToScreen
13581
+ ]
13582
+ );
13583
+ const shakeStyle = isShaking ? { animation: "battle-shake 0.3s ease-in-out" } : {};
13584
+ return /* @__PURE__ */ jsxs("div", { className: cn("battle-board relative flex flex-col min-h-[600px] bg-[var(--color-background)]", className), children: [
13585
+ /* @__PURE__ */ jsx("style", { children: `
13586
+ @keyframes battle-shake {
13587
+ 0%, 100% { transform: translate(0, 0); }
13588
+ 10% { transform: translate(-3px, -2px); }
13589
+ 20% { transform: translate(3px, 1px); }
13590
+ 30% { transform: translate(-2px, 3px); }
13591
+ 40% { transform: translate(2px, -1px); }
13592
+ 50% { transform: translate(-3px, 2px); }
13593
+ 60% { transform: translate(3px, -2px); }
13594
+ 70% { transform: translate(-1px, 3px); }
13595
+ 80% { transform: translate(2px, -3px); }
13596
+ 90% { transform: translate(-2px, 1px); }
13597
+ }
13598
+ ` }),
13599
+ header && /* @__PURE__ */ jsx("div", { className: "p-4", children: header(ctx) }),
13600
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 gap-4 p-4 pt-0", children: [
13601
+ /* @__PURE__ */ jsxs("div", { className: "relative flex-1", style: shakeStyle, children: [
13602
+ /* @__PURE__ */ jsx(
13603
+ IsometricCanvas_default,
13604
+ {
13605
+ tiles,
13606
+ units: isoUnits,
13607
+ features,
13608
+ selectedUnitId,
13609
+ validMoves,
13610
+ attackTargets,
13611
+ hoveredTile,
13612
+ onTileClick: handleTileClick,
13613
+ onUnitClick: handleUnitClick,
13614
+ onTileHover: (x, y) => setHoveredTile({ x, y }),
13615
+ onTileLeave: () => setHoveredTile(null),
13616
+ scale,
13617
+ assetBaseUrl: assetManifest?.baseUrl,
13618
+ assetManifest,
13619
+ backgroundImage,
13620
+ onDrawEffects,
13621
+ hasActiveEffects: hasActiveEffects2,
13622
+ effectSpriteUrls,
13623
+ resolveUnitFrame,
13624
+ unitScale
13625
+ }
13626
+ ),
13627
+ overlay && overlay(ctx)
13628
+ ] }),
13629
+ sidebar && /* @__PURE__ */ jsx("div", { className: "w-80 shrink-0", children: sidebar(ctx) })
13630
+ ] }),
13631
+ actions ? actions(ctx) : currentPhase !== "game_over" && /* @__PURE__ */ jsxs("div", { className: "fixed bottom-6 right-6 z-50 flex gap-2", children: [
13632
+ (currentPhase === "movement" || currentPhase === "action") && /* @__PURE__ */ jsx(
13633
+ "button",
13634
+ {
13635
+ className: "px-4 py-2 rounded-lg bg-[var(--color-surface)] text-[var(--color-foreground)] border border-[var(--color-border)] shadow-xl hover:opacity-90",
13636
+ onClick: handleCancel,
13637
+ children: "Cancel"
13638
+ }
13639
+ ),
13640
+ /* @__PURE__ */ jsx(
13641
+ "button",
13642
+ {
13643
+ className: "px-4 py-2 rounded-lg bg-[var(--color-primary)] text-white shadow-xl hover:opacity-90",
13644
+ onClick: handleEndTurn,
13645
+ children: "End Turn"
13646
+ }
13647
+ )
13648
+ ] }),
13649
+ gameResult && (gameOverOverlay ? gameOverOverlay(ctx) : /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl", children: /* @__PURE__ */ jsxs("div", { className: "text-center space-y-6 p-8", children: [
13650
+ /* @__PURE__ */ jsx(
13651
+ "h2",
13652
+ {
13653
+ className: cn(
13654
+ "text-4xl font-black tracking-widest uppercase",
13655
+ gameResult === "victory" ? "text-yellow-400" : "text-red-500"
13656
+ ),
13657
+ children: gameResult === "victory" ? "Victory!" : "Defeat"
13658
+ }
13659
+ ),
13660
+ /* @__PURE__ */ jsxs("p", { className: "text-gray-300", children: [
13661
+ "Turns played: ",
13662
+ currentTurn
13663
+ ] }),
13664
+ /* @__PURE__ */ jsx(
13665
+ "button",
13666
+ {
13667
+ className: "px-8 py-3 rounded-lg bg-[var(--color-primary)] text-white font-semibold hover:opacity-90",
13668
+ onClick: handleReset,
13669
+ children: "Play Again"
13670
+ }
13671
+ )
13672
+ ] }) }))
13673
+ ] });
13674
+ }
13675
+ BattleBoard.displayName = "BattleBoard";
13676
+ function defaultIsInRange(from, to, range) {
13677
+ return Math.abs(from.x - to.x) + Math.abs(from.y - to.y) <= range;
13678
+ }
13679
+ function WorldMapBoard({
13680
+ entity,
13681
+ scale = 0.4,
13682
+ unitScale = 2.5,
13683
+ allowMoveAllHeroes = false,
13684
+ isInRange = defaultIsInRange,
13685
+ heroSelectEvent,
13686
+ heroMoveEvent,
13687
+ battleEncounterEvent,
13688
+ featureEnterEvent,
13689
+ tileClickEvent,
13690
+ header,
13691
+ sidePanel,
13692
+ overlay,
13693
+ footer,
13694
+ onHeroSelect,
13695
+ onHeroMove,
13696
+ onBattleEncounter,
13697
+ onFeatureEnter,
13698
+ effectSpriteUrls = [],
13699
+ resolveUnitFrame,
13700
+ className
13701
+ }) {
13702
+ const eventBus = useEventBus();
13703
+ const hexes = entity.hexes;
13704
+ const heroes = entity.heroes;
13705
+ const features = entity.features ?? [];
13706
+ const selectedHeroId = entity.selectedHeroId;
13707
+ const assetManifest = entity.assetManifest;
13708
+ const backgroundImage = entity.backgroundImage;
13709
+ const [hoveredTile, setHoveredTile] = useState(null);
13710
+ const selectedHero = useMemo(
13711
+ () => heroes.find((h) => h.id === selectedHeroId) ?? null,
13712
+ [heroes, selectedHeroId]
13713
+ );
13714
+ const tiles = useMemo(
13715
+ () => hexes.map((hex) => ({
13716
+ x: hex.x,
13717
+ y: hex.y,
13718
+ terrain: hex.terrain,
13719
+ terrainSprite: hex.terrainSprite
13720
+ })),
13721
+ [hexes]
13722
+ );
13723
+ const baseUnits = useMemo(
13724
+ () => heroes.map((hero) => ({
13725
+ id: hero.id,
13726
+ position: hero.position,
13727
+ name: hero.name,
13728
+ team: hero.owner === "enemy" ? "enemy" : "player",
13729
+ health: 100,
13730
+ maxHealth: 100,
13731
+ sprite: hero.sprite
13732
+ })),
13733
+ [heroes]
13734
+ );
13735
+ const MOVE_SPEED_MS_PER_TILE = 300;
13736
+ const movementAnimRef = useRef(null);
13737
+ const [movingPositions, setMovingPositions] = useState(/* @__PURE__ */ new Map());
13738
+ const startMoveAnimation = useCallback((heroId, from, to, onComplete) => {
13739
+ const dist = Math.max(Math.abs(to.x - from.x), Math.abs(to.y - from.y));
13740
+ movementAnimRef.current = { heroId, from, to, elapsed: 0, duration: dist * MOVE_SPEED_MS_PER_TILE, onComplete };
13741
+ }, []);
13742
+ useEffect(() => {
13743
+ const interval = setInterval(() => {
13744
+ const anim2 = movementAnimRef.current;
13745
+ if (!anim2) return;
13746
+ anim2.elapsed += 16;
13747
+ const t = Math.min(anim2.elapsed / anim2.duration, 1);
13748
+ const eased = 1 - (1 - t) * (1 - t);
13749
+ const cx = anim2.from.x + (anim2.to.x - anim2.from.x) * eased;
13750
+ const cy = anim2.from.y + (anim2.to.y - anim2.from.y) * eased;
13751
+ if (t >= 1) {
13752
+ movementAnimRef.current = null;
13753
+ setMovingPositions((prev) => {
13754
+ const n = new Map(prev);
13755
+ n.delete(anim2.heroId);
13756
+ return n;
13757
+ });
13758
+ anim2.onComplete();
13759
+ } else {
13760
+ setMovingPositions((prev) => {
13761
+ const n = new Map(prev);
13762
+ n.set(anim2.heroId, { x: cx, y: cy });
13763
+ return n;
13764
+ });
13765
+ }
13766
+ }, 16);
13767
+ return () => clearInterval(interval);
13768
+ }, []);
13769
+ const isoUnits = useMemo(() => {
13770
+ if (movingPositions.size === 0) return baseUnits;
13771
+ return baseUnits.map((u) => {
13772
+ const pos = movingPositions.get(u.id);
13773
+ return pos ? { ...u, position: pos } : u;
13774
+ });
13775
+ }, [baseUnits, movingPositions]);
13776
+ const validMoves = useMemo(() => {
13777
+ if (!selectedHero || selectedHero.movement <= 0) return [];
13778
+ const moves = [];
13779
+ hexes.forEach((hex) => {
13780
+ if (hex.passable === false) return;
13781
+ if (hex.x === selectedHero.position.x && hex.y === selectedHero.position.y) return;
13782
+ if (!isInRange(selectedHero.position, { x: hex.x, y: hex.y }, selectedHero.movement)) return;
13783
+ if (heroes.some((h) => h.position.x === hex.x && h.position.y === hex.y && h.owner === selectedHero.owner)) return;
13784
+ moves.push({ x: hex.x, y: hex.y });
13785
+ });
13786
+ return moves;
13787
+ }, [selectedHero, hexes, heroes, isInRange]);
13788
+ const attackTargets = useMemo(() => {
13789
+ if (!selectedHero || selectedHero.movement <= 0) return [];
13790
+ return heroes.filter((h) => h.owner !== selectedHero.owner).filter((h) => isInRange(selectedHero.position, h.position, selectedHero.movement)).map((h) => h.position);
13791
+ }, [selectedHero, heroes, isInRange]);
13792
+ const maxY = Math.max(...hexes.map((h) => h.y), 0);
13793
+ const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
13794
+ const tileToScreen = useCallback(
13795
+ (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
13796
+ [scale, baseOffsetX]
13797
+ );
13798
+ const hoveredHex = useMemo(
13799
+ () => hoveredTile ? hexes.find((h) => h.x === hoveredTile.x && h.y === hoveredTile.y) ?? null : null,
13800
+ [hoveredTile, hexes]
13801
+ );
13802
+ const hoveredHero = useMemo(
13803
+ () => hoveredTile ? heroes.find((h) => h.position.x === hoveredTile.x && h.position.y === hoveredTile.y) ?? null : null,
13804
+ [hoveredTile, heroes]
13805
+ );
13806
+ const handleTileClick = useCallback((x, y) => {
13807
+ if (movementAnimRef.current) return;
13808
+ const hex = hexes.find((h) => h.x === x && h.y === y);
13809
+ if (!hex) return;
13810
+ if (tileClickEvent) {
13811
+ eventBus.emit(`UI:${tileClickEvent}`, { x, y });
13812
+ }
13813
+ if (selectedHero && validMoves.some((m) => m.x === x && m.y === y)) {
13814
+ startMoveAnimation(selectedHero.id, { ...selectedHero.position }, { x, y }, () => {
13815
+ onHeroMove?.(selectedHero.id, x, y);
13816
+ if (heroMoveEvent) {
13817
+ eventBus.emit(`UI:${heroMoveEvent}`, { heroId: selectedHero.id, toX: x, toY: y });
13818
+ }
13819
+ if (hex.feature && hex.feature !== "none") {
13820
+ onFeatureEnter?.(selectedHero.id, hex);
13821
+ if (featureEnterEvent) {
13822
+ eventBus.emit(`UI:${featureEnterEvent}`, { heroId: selectedHero.id, feature: hex.feature, hex });
13823
+ }
13824
+ }
13825
+ });
13826
+ return;
13827
+ }
13828
+ const enemy = heroes.find((h) => h.position.x === x && h.position.y === y && h.owner === "enemy");
13829
+ if (selectedHero && enemy && attackTargets.some((t) => t.x === x && t.y === y)) {
13830
+ onBattleEncounter?.(selectedHero.id, enemy.id);
13831
+ if (battleEncounterEvent) {
13832
+ eventBus.emit(`UI:${battleEncounterEvent}`, { attackerId: selectedHero.id, defenderId: enemy.id });
13833
+ }
13834
+ }
13835
+ }, [hexes, heroes, selectedHero, validMoves, attackTargets, startMoveAnimation, onHeroMove, onFeatureEnter, onBattleEncounter, eventBus, tileClickEvent, heroMoveEvent, featureEnterEvent, battleEncounterEvent]);
13836
+ const handleUnitClick = useCallback((unitId) => {
13837
+ const hero = heroes.find((h) => h.id === unitId);
13838
+ if (hero && (hero.owner === "player" || allowMoveAllHeroes)) {
13839
+ onHeroSelect?.(unitId);
13840
+ if (heroSelectEvent) {
13841
+ eventBus.emit(`UI:${heroSelectEvent}`, { heroId: unitId });
13842
+ }
13843
+ }
13844
+ }, [heroes, onHeroSelect, allowMoveAllHeroes, eventBus, heroSelectEvent]);
13845
+ const selectHero = useCallback((id) => {
13846
+ onHeroSelect?.(id);
13847
+ if (heroSelectEvent) {
13848
+ eventBus.emit(`UI:${heroSelectEvent}`, { heroId: id });
13849
+ }
13850
+ }, [onHeroSelect, eventBus, heroSelectEvent]);
13851
+ const ctx = useMemo(
13852
+ () => ({
13853
+ hoveredTile,
13854
+ hoveredHex,
13855
+ hoveredHero,
13856
+ selectedHero,
13857
+ validMoves,
13858
+ selectHero,
13859
+ tileToScreen,
13860
+ scale
13861
+ }),
13862
+ [hoveredTile, hoveredHex, hoveredHero, selectedHero, validMoves, selectHero, tileToScreen, scale]
13863
+ );
13864
+ return /* @__PURE__ */ jsxs("div", { className: cn("world-map-board min-h-screen flex flex-col bg-[var(--color-background)]", className), children: [
13865
+ header && header(ctx),
13866
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
13867
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto p-4 relative", children: [
13868
+ /* @__PURE__ */ jsx(
13869
+ IsometricCanvas_default,
13870
+ {
13871
+ tiles,
13872
+ units: isoUnits,
13873
+ features,
13874
+ selectedUnitId: selectedHeroId,
13875
+ validMoves,
13876
+ attackTargets,
13877
+ hoveredTile,
13878
+ onTileClick: handleTileClick,
13879
+ onUnitClick: handleUnitClick,
13880
+ onTileHover: (x, y) => setHoveredTile({ x, y }),
13881
+ onTileLeave: () => setHoveredTile(null),
13882
+ scale,
13883
+ assetBaseUrl: assetManifest?.baseUrl,
13884
+ assetManifest,
13885
+ backgroundImage,
13886
+ effectSpriteUrls,
13887
+ resolveUnitFrame,
13888
+ unitScale
13889
+ }
13890
+ ),
13891
+ overlay && overlay(ctx)
13892
+ ] }),
13893
+ sidePanel && /* @__PURE__ */ jsx("div", { className: "w-80 shrink-0 border-l border-[var(--color-border)] bg-[var(--color-surface)] overflow-y-auto p-4", children: sidePanel(ctx) })
13894
+ ] }),
13895
+ footer && footer(ctx)
13896
+ ] });
13897
+ }
13898
+ WorldMapBoard.displayName = "WorldMapBoard";
13899
+ function CastleBoard({
13900
+ entity,
13901
+ scale = 0.45,
13902
+ header,
13903
+ sidePanel,
13904
+ overlay,
13905
+ footer,
13906
+ onFeatureClick,
13907
+ onUnitClick,
13908
+ onTileClick,
13909
+ featureClickEvent,
13910
+ unitClickEvent,
13911
+ tileClickEvent,
13912
+ className
13913
+ }) {
13914
+ const eventBus = useEventBus();
13915
+ const tiles = entity.tiles;
13916
+ const features = entity.features ?? [];
13917
+ const units = entity.units ?? [];
13918
+ const assetManifest = entity.assetManifest;
13919
+ const backgroundImage = entity.backgroundImage;
13920
+ const [hoveredTile, setHoveredTile] = useState(null);
13921
+ const [selectedFeature, setSelectedFeature] = useState(null);
13922
+ const hoveredFeature = useMemo(() => {
13923
+ if (!hoveredTile) return null;
13924
+ return features.find((f) => f.x === hoveredTile.x && f.y === hoveredTile.y) ?? null;
13925
+ }, [hoveredTile, features]);
13926
+ const hoveredUnit = useMemo(() => {
13927
+ if (!hoveredTile) return null;
13928
+ return units.find(
13929
+ (u) => u.position?.x === hoveredTile.x && u.position?.y === hoveredTile.y
13930
+ ) ?? null;
13931
+ }, [hoveredTile, units]);
13932
+ const maxY = Math.max(...tiles.map((t) => t.y), 0);
13933
+ const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
13934
+ const tileToScreen = useCallback(
13935
+ (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
13936
+ [scale, baseOffsetX]
13937
+ );
13938
+ const handleTileClick = useCallback((x, y) => {
13939
+ const feature = features.find((f) => f.x === x && f.y === y);
13940
+ if (feature) {
13941
+ setSelectedFeature(feature);
13942
+ onFeatureClick?.(feature);
13943
+ if (featureClickEvent) {
13944
+ eventBus.emit(`UI:${featureClickEvent}`, {
13945
+ featureId: feature.id,
13946
+ featureType: feature.type,
13947
+ x: feature.x,
13948
+ y: feature.y
13949
+ });
13950
+ }
13951
+ }
13952
+ onTileClick?.(x, y);
13953
+ if (tileClickEvent) {
13954
+ eventBus.emit(`UI:${tileClickEvent}`, { x, y });
13955
+ }
13956
+ }, [features, onFeatureClick, onTileClick, featureClickEvent, tileClickEvent, eventBus]);
13957
+ const handleUnitClick = useCallback((unitId) => {
13958
+ const unit = units.find((u) => u.id === unitId);
13959
+ if (unit) {
13960
+ onUnitClick?.(unit);
13961
+ if (unitClickEvent) {
13962
+ eventBus.emit(`UI:${unitClickEvent}`, { unitId: unit.id });
13963
+ }
13964
+ }
13965
+ }, [units, onUnitClick, unitClickEvent, eventBus]);
13966
+ const clearSelection = useCallback(() => setSelectedFeature(null), []);
13967
+ const ctx = useMemo(
13968
+ () => ({
13969
+ hoveredTile,
13970
+ hoveredFeature,
13971
+ hoveredUnit,
13972
+ selectedFeature,
13973
+ clearSelection,
13974
+ tileToScreen,
13975
+ scale
13976
+ }),
13977
+ [hoveredTile, hoveredFeature, hoveredUnit, selectedFeature, clearSelection, tileToScreen, scale]
13978
+ );
13979
+ return /* @__PURE__ */ jsxs("div", { className: cn("castle-board min-h-screen flex flex-col bg-[var(--color-background)]", className), children: [
13980
+ header && header(ctx),
13981
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
13982
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto p-4 relative", children: [
13983
+ /* @__PURE__ */ jsx(
13984
+ IsometricCanvas_default,
13985
+ {
13986
+ tiles,
13987
+ units,
13988
+ features,
13989
+ hoveredTile,
13990
+ onTileClick: handleTileClick,
13991
+ onUnitClick: handleUnitClick,
13992
+ onTileHover: (x, y) => setHoveredTile({ x, y }),
13993
+ onTileLeave: () => setHoveredTile(null),
13994
+ scale,
13995
+ assetBaseUrl: assetManifest?.baseUrl,
13996
+ assetManifest,
13997
+ backgroundImage
13998
+ }
13999
+ ),
14000
+ overlay && overlay(ctx)
14001
+ ] }),
14002
+ sidePanel && /* @__PURE__ */ jsx("div", { className: "w-96 shrink-0 border-l border-[var(--color-border)] bg-[var(--color-surface)] overflow-y-auto", children: sidePanel(ctx) })
14003
+ ] }),
14004
+ footer && footer(ctx)
14005
+ ] });
14006
+ }
14007
+ CastleBoard.displayName = "CastleBoard";
14008
+ var TERRAIN_COLORS = {
14009
+ grass: "#4a7c3f",
14010
+ dirt: "#8b6c42",
14011
+ stone: "#7a7a7a",
14012
+ sand: "#c4a84d",
14013
+ water: "#3a6ea5",
14014
+ forest: "#2d5a1e",
14015
+ mountain: "#5a4a3a",
14016
+ lava: "#c44b2b",
14017
+ ice: "#a0d2e8",
14018
+ plains: "#6b8e4e",
14019
+ fortress: "#4a4a5a",
14020
+ castle: "#5a5a6a"
14021
+ };
14022
+ var FEATURE_TYPES = [
14023
+ "goldMine",
14024
+ "resonanceCrystal",
14025
+ "traitCache",
14026
+ "salvageYard",
14027
+ "portal",
14028
+ "battleMarker",
14029
+ "treasure",
14030
+ "castle"
14031
+ ];
14032
+ function CollapsibleSection({ title, expanded, onToggle, children, className }) {
14033
+ const Icon2 = expanded ? ChevronDown : ChevronRight;
14034
+ return /* @__PURE__ */ jsxs(VStack, { gap: "xs", className, children: [
14035
+ /* @__PURE__ */ jsx(
14036
+ Button,
14037
+ {
14038
+ variant: "ghost",
14039
+ size: "sm",
14040
+ onClick: onToggle,
14041
+ className: "w-full justify-start text-left",
14042
+ children: /* @__PURE__ */ jsxs(HStack, { gap: "xs", align: "center", children: [
14043
+ /* @__PURE__ */ jsx(Icon2, { size: 14 }),
14044
+ /* @__PURE__ */ jsx(Typography, { variant: "label", weight: "semibold", children: title })
14045
+ ] })
14046
+ }
14047
+ ),
14048
+ expanded && /* @__PURE__ */ jsx(Box, { padding: "xs", paddingX: "sm", children })
14049
+ ] });
14050
+ }
14051
+ CollapsibleSection.displayName = "CollapsibleSection";
14052
+ function EditorSlider({ label, value, min, max, step = 0.1, onChange, className }) {
14053
+ return /* @__PURE__ */ jsxs(HStack, { gap: "sm", align: "center", className, children: [
14054
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "min-w-[80px] text-gray-300", children: label }),
14055
+ /* @__PURE__ */ jsx(Box, { className: "flex-1", children: /* @__PURE__ */ jsx(
14056
+ "input",
14057
+ {
14058
+ type: "range",
14059
+ min,
14060
+ max,
14061
+ step,
14062
+ value,
14063
+ onChange: (e) => onChange(parseFloat(e.target.value)),
14064
+ className: "w-full h-1.5 bg-gray-600 rounded-lg appearance-none cursor-pointer accent-blue-500"
14065
+ }
14066
+ ) }),
14067
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "min-w-[40px] text-right text-gray-400", children: typeof step === "number" && step < 1 ? value.toFixed(1) : value })
14068
+ ] });
14069
+ }
14070
+ EditorSlider.displayName = "EditorSlider";
14071
+ function EditorSelect({ label, value, options, onChange, className }) {
14072
+ return /* @__PURE__ */ jsxs(HStack, { gap: "sm", align: "center", className, children: [
14073
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "min-w-[80px] text-gray-300", children: label }),
14074
+ /* @__PURE__ */ jsx(Box, { className: "flex-1", children: /* @__PURE__ */ jsx(
14075
+ "select",
14076
+ {
14077
+ value,
14078
+ onChange: (e) => onChange(e.target.value),
14079
+ className: "w-full px-2 py-1 text-xs bg-gray-700 text-gray-200 border border-gray-600 rounded cursor-pointer",
14080
+ children: options.map((opt) => /* @__PURE__ */ jsx("option", { value: opt.value, children: opt.label }, opt.value))
14081
+ }
14082
+ ) })
14083
+ ] });
14084
+ }
14085
+ EditorSelect.displayName = "EditorSelect";
14086
+ function EditorCheckbox({ label, checked, onChange, className }) {
14087
+ return /* @__PURE__ */ jsxs(HStack, { gap: "sm", align: "center", className, children: [
14088
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "min-w-[80px] text-gray-300", children: label }),
14089
+ /* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsx(
14090
+ "input",
14091
+ {
14092
+ type: "checkbox",
14093
+ checked,
14094
+ onChange: (e) => onChange(e.target.checked),
14095
+ className: "w-4 h-4 accent-blue-500 cursor-pointer"
14096
+ }
14097
+ ) })
14098
+ ] });
14099
+ }
14100
+ EditorCheckbox.displayName = "EditorCheckbox";
14101
+ function EditorTextInput({ label, value, onChange, placeholder, className }) {
14102
+ return /* @__PURE__ */ jsxs(HStack, { gap: "sm", align: "center", className, children: [
14103
+ /* @__PURE__ */ jsx(Typography, { variant: "caption", className: "min-w-[80px] text-gray-300", children: label }),
14104
+ /* @__PURE__ */ jsx(Box, { className: "flex-1", children: /* @__PURE__ */ jsx(
14105
+ "input",
14106
+ {
14107
+ type: "text",
14108
+ value,
14109
+ onChange: (e) => onChange(e.target.value),
14110
+ placeholder,
14111
+ className: "w-full px-2 py-1 text-xs bg-gray-700 text-gray-200 border border-gray-600 rounded"
14112
+ }
14113
+ ) })
14114
+ ] });
14115
+ }
14116
+ EditorTextInput.displayName = "EditorTextInput";
14117
+ function StatusBar({ hoveredTile, mode, gridSize, unitCount, featureCount, className }) {
14118
+ return /* @__PURE__ */ jsxs(HStack, { gap: "sm", align: "center", className: `px-3 py-1.5 bg-gray-800 border-t border-gray-700 ${className ?? ""}`, children: [
14119
+ /* @__PURE__ */ jsx(Badge, { variant: "info", size: "sm", children: mode }),
14120
+ /* @__PURE__ */ jsxs(Typography, { variant: "caption", className: "text-gray-400", children: [
14121
+ "Tile: ",
14122
+ hoveredTile ? `(${hoveredTile.x}, ${hoveredTile.y})` : "\u2014"
14123
+ ] }),
14124
+ gridSize && /* @__PURE__ */ jsxs(Typography, { variant: "caption", className: "text-gray-500", children: [
14125
+ "Grid: ",
14126
+ gridSize.width,
14127
+ "x",
14128
+ gridSize.height
14129
+ ] }),
14130
+ unitCount !== void 0 && /* @__PURE__ */ jsxs(Typography, { variant: "caption", className: "text-gray-500", children: [
14131
+ "Units: ",
14132
+ unitCount
14133
+ ] }),
14134
+ featureCount !== void 0 && /* @__PURE__ */ jsxs(Typography, { variant: "caption", className: "text-gray-500", children: [
14135
+ "Features: ",
14136
+ featureCount
14137
+ ] })
14138
+ ] });
14139
+ }
14140
+ StatusBar.displayName = "StatusBar";
14141
+ function TerrainPalette({ terrains, selectedTerrain, onSelect, className }) {
14142
+ return /* @__PURE__ */ jsx(HStack, { gap: "xs", wrap: true, className, children: terrains.map((terrain) => /* @__PURE__ */ jsx(
14143
+ Box,
14144
+ {
14145
+ onClick: () => onSelect(terrain),
14146
+ className: `w-8 h-8 rounded cursor-pointer border-2 transition-all ${selectedTerrain === terrain ? "border-white scale-110 shadow-lg" : "border-gray-600 hover:border-gray-400"}`,
14147
+ style: { backgroundColor: TERRAIN_COLORS[terrain] || "#555" },
14148
+ title: terrain
14149
+ },
14150
+ terrain
14151
+ )) });
14152
+ }
14153
+ TerrainPalette.displayName = "TerrainPalette";
14154
+ var MODE_LABELS = {
14155
+ select: "Select",
14156
+ paint: "Paint",
14157
+ unit: "Unit",
14158
+ feature: "Feature",
14159
+ erase: "Erase"
14160
+ };
14161
+ function EditorToolbar({ mode, onModeChange, className }) {
14162
+ const modes = ["select", "paint", "unit", "feature", "erase"];
14163
+ return /* @__PURE__ */ jsx(HStack, { gap: "xs", wrap: true, className, children: modes.map((m) => /* @__PURE__ */ jsx(
14164
+ Button,
14165
+ {
14166
+ variant: mode === m ? "primary" : "ghost",
14167
+ size: "sm",
14168
+ onClick: () => onModeChange(m),
14169
+ children: MODE_LABELS[m]
14170
+ },
14171
+ m
14172
+ )) });
14173
+ }
14174
+ EditorToolbar.displayName = "EditorToolbar";
14175
+ function VStackPattern({
14176
+ gap = "md",
14177
+ align = "stretch",
14178
+ justify = "start",
14179
+ className,
14180
+ style,
14181
+ children
14182
+ }) {
14183
+ return /* @__PURE__ */ jsx(VStack, { gap, align, justify, className, style, children });
14184
+ }
14185
+ VStackPattern.displayName = "VStackPattern";
14186
+ function HStackPattern({
14187
+ gap = "md",
14188
+ align = "center",
14189
+ justify = "start",
14190
+ wrap = false,
14191
+ className,
14192
+ style,
14193
+ children
14194
+ }) {
14195
+ return /* @__PURE__ */ jsx(HStack, { gap, align, justify, wrap, className, style, children });
14196
+ }
14197
+ HStackPattern.displayName = "HStackPattern";
14198
+ function BoxPattern({
14199
+ p: p2,
14200
+ m,
14201
+ bg = "transparent",
14202
+ border = false,
14203
+ radius = "none",
14204
+ shadow = "none",
14205
+ className,
14206
+ style,
14207
+ children
14208
+ }) {
14209
+ return /* @__PURE__ */ jsx(
14210
+ Box,
14211
+ {
14212
+ padding: p2,
14213
+ margin: m,
14214
+ bg,
14215
+ border,
14216
+ rounded: radius,
14217
+ shadow,
14218
+ className,
14219
+ style,
14220
+ children
14221
+ }
14222
+ );
14223
+ }
14224
+ BoxPattern.displayName = "BoxPattern";
14225
+ function GridPattern({
14226
+ cols = 1,
14227
+ gap = "md",
14228
+ rowGap,
14229
+ colGap,
14230
+ className,
14231
+ style,
14232
+ children
14233
+ }) {
14234
+ return /* @__PURE__ */ jsx(Grid2, { cols, gap, rowGap, colGap, className, style, children });
14235
+ }
14236
+ GridPattern.displayName = "GridPattern";
14237
+ function CenterPattern({
14238
+ minHeight,
14239
+ className,
14240
+ style,
14241
+ children
14242
+ }) {
14243
+ const mergedStyle = minHeight ? { minHeight, ...style } : style;
14244
+ return /* @__PURE__ */ jsx(Center, { className, style: mergedStyle, children });
14245
+ }
14246
+ CenterPattern.displayName = "CenterPattern";
14247
+ function SpacerPattern({ size = "flex" }) {
14248
+ if (size === "flex") {
14249
+ return /* @__PURE__ */ jsx(Spacer, {});
14250
+ }
14251
+ const sizeMap5 = {
14252
+ xs: "0.25rem",
14253
+ sm: "0.5rem",
14254
+ md: "1rem",
14255
+ lg: "1.5rem",
14256
+ xl: "2rem"
14257
+ };
14258
+ return /* @__PURE__ */ jsx("div", { style: { width: sizeMap5[size], height: sizeMap5[size], flexShrink: 0 } });
14259
+ }
14260
+ SpacerPattern.displayName = "SpacerPattern";
14261
+ function DividerPattern({
14262
+ orientation = "horizontal",
14263
+ variant = "solid",
14264
+ spacing = "md"
14265
+ }) {
14266
+ const spacingMap = {
14267
+ xs: "my-1",
14268
+ sm: "my-2",
14269
+ md: "my-4",
14270
+ lg: "my-6"
14271
+ };
14272
+ const verticalSpacingMap = {
14273
+ xs: "mx-1",
14274
+ sm: "mx-2",
14275
+ md: "mx-4",
14276
+ lg: "mx-6"
14277
+ };
14278
+ return /* @__PURE__ */ jsx(
14279
+ Divider,
14280
+ {
14281
+ orientation,
14282
+ variant,
14283
+ className: orientation === "horizontal" ? spacingMap[spacing] : verticalSpacingMap[spacing]
14284
+ }
14285
+ );
14286
+ }
14287
+ DividerPattern.displayName = "DividerPattern";
14288
+ function ButtonPattern({
14289
+ label,
14290
+ variant = "primary",
14291
+ size = "md",
14292
+ disabled = false,
14293
+ onClick,
14294
+ icon,
14295
+ iconPosition = "left",
14296
+ className
14297
+ }) {
14298
+ const { emit } = useEventBus();
14299
+ const handleClick = () => {
14300
+ if (onClick && !disabled) {
14301
+ emit(`UI:${onClick}`, {});
14302
+ }
14303
+ };
14304
+ return /* @__PURE__ */ jsxs(
14305
+ Button,
14306
+ {
14307
+ variant,
14308
+ size,
14309
+ disabled,
14310
+ onClick: handleClick,
14311
+ className,
14312
+ children: [
14313
+ icon && iconPosition === "left" && /* @__PURE__ */ jsx(Icon, { name: icon, size: "sm" }),
14314
+ label,
14315
+ icon && iconPosition === "right" && /* @__PURE__ */ jsx(Icon, { name: icon, size: "sm" })
14316
+ ]
14317
+ }
14318
+ );
14319
+ }
14320
+ ButtonPattern.displayName = "ButtonPattern";
14321
+ function IconButtonPattern({
14322
+ icon,
14323
+ variant = "ghost",
14324
+ size = "md",
14325
+ onClick,
14326
+ ariaLabel,
14327
+ className
14328
+ }) {
14329
+ const { emit } = useEventBus();
14330
+ const handleClick = () => {
14331
+ if (onClick) {
14332
+ emit(`UI:${onClick}`, {});
14333
+ }
14334
+ };
14335
+ return /* @__PURE__ */ jsx(
14336
+ Button,
14337
+ {
14338
+ variant,
14339
+ size,
14340
+ onClick: handleClick,
13150
14341
  "aria-label": ariaLabel,
13151
14342
  className,
13152
14343
  children: /* @__PURE__ */ jsx(Icon, { name: icon, size: size === "sm" ? "sm" : "md" })
@@ -17637,707 +18828,164 @@ var GenericAppTemplate = ({
17637
18828
  {
17638
18829
  padding: "md",
17639
18830
  border: true,
17640
- className: "border-b-2 border-x-0 border-t-0 border-[var(--color-border)] flex items-center justify-between flex-shrink-0",
17641
- children: [
17642
- /* @__PURE__ */ jsxs("div", { children: [
17643
- /* @__PURE__ */ jsx(Typography, { variant: "h3", children: title }),
17644
- subtitle && /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "secondary", className: "mt-1", children: subtitle })
17645
- ] }),
17646
- headerActions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: headerActions })
17647
- ]
17648
- }
17649
- ),
17650
- /* @__PURE__ */ jsx(Box, { fullWidth: true, overflow: "auto", className: "flex-1", children: /* @__PURE__ */ jsx(Box, { padding: "lg", children }) }),
17651
- footer && /* @__PURE__ */ jsx(
17652
- Box,
17653
- {
17654
- padding: "md",
17655
- border: true,
17656
- bg: "muted",
17657
- className: "border-t-2 border-x-0 border-b-0 border-[var(--color-border)] flex-shrink-0",
17658
- children: footer
17659
- }
17660
- )
17661
- ] });
17662
- };
17663
- GenericAppTemplate.displayName = "GenericAppTemplate";
17664
- var GameShell = ({
17665
- appName = "Game",
17666
- hud,
17667
- className,
17668
- showTopBar = true
17669
- }) => {
17670
- return /* @__PURE__ */ jsxs(
17671
- "div",
17672
- {
17673
- className: cn(
17674
- "game-shell",
17675
- "flex flex-col w-full h-screen overflow-hidden",
17676
- className
17677
- ),
17678
- style: {
17679
- width: "100vw",
17680
- height: "100vh",
17681
- display: "flex",
17682
- flexDirection: "column",
17683
- overflow: "hidden",
17684
- background: "var(--color-background, #0a0a0f)",
17685
- color: "var(--color-text, #e0e0e0)"
17686
- },
17687
- children: [
17688
- showTopBar && /* @__PURE__ */ jsxs(
17689
- "header",
17690
- {
17691
- className: "game-shell__header",
17692
- style: {
17693
- display: "flex",
17694
- alignItems: "center",
17695
- justifyContent: "space-between",
17696
- padding: "0.5rem 1rem",
17697
- borderBottom: "1px solid var(--color-border, #2a2a3a)",
17698
- background: "var(--color-surface, #12121f)",
17699
- flexShrink: 0
17700
- },
17701
- children: [
17702
- /* @__PURE__ */ jsx(
17703
- "span",
17704
- {
17705
- style: {
17706
- fontWeight: 700,
17707
- fontSize: "1.1rem",
17708
- letterSpacing: "0.02em"
17709
- },
17710
- children: appName
17711
- }
17712
- ),
17713
- hud && /* @__PURE__ */ jsx("div", { className: "game-shell__hud", children: hud })
17714
- ]
17715
- }
17716
- ),
17717
- /* @__PURE__ */ jsx(
17718
- "main",
17719
- {
17720
- className: "game-shell__content",
17721
- style: {
17722
- flex: 1,
17723
- overflow: "hidden",
17724
- position: "relative"
17725
- },
17726
- children: /* @__PURE__ */ jsx(Outlet, {})
17727
- }
17728
- )
17729
- ]
17730
- }
17731
- );
17732
- };
17733
- GameShell.displayName = "GameShell";
17734
- function BattleTemplate({
17735
- initialUnits,
17736
- tiles,
17737
- scale = 0.45,
17738
- boardWidth = 8,
17739
- boardHeight = 6,
17740
- assetManifest,
17741
- backgroundImage,
17742
- unitScale = 1,
17743
- header,
17744
- sidebar,
17745
- actions,
17746
- overlay,
17747
- gameOverOverlay,
17748
- features = [],
17749
- onAttack,
17750
- onGameEnd,
17751
- onUnitMove,
17752
- calculateDamage,
17753
- onDrawEffects,
17754
- hasActiveEffects: hasActiveEffects2 = false,
17755
- effectSpriteUrls = [],
17756
- resolveUnitFrame,
17757
- className
17758
- }) {
17759
- const [units, setUnits] = useState(initialUnits);
17760
- const [selectedUnitId, setSelectedUnitId] = useState(null);
17761
- const [hoveredTile, setHoveredTile] = useState(null);
17762
- const [currentPhase, setCurrentPhase] = useState("observation");
17763
- const [currentTurn, setCurrentTurn] = useState(1);
17764
- const [gameResult, setGameResult] = useState(null);
17765
- const [isShaking, setIsShaking] = useState(false);
17766
- const selectedUnit = useMemo(
17767
- () => units.find((u) => u.id === selectedUnitId) ?? null,
17768
- [units, selectedUnitId]
17769
- );
17770
- const hoveredUnit = useMemo(() => {
17771
- if (!hoveredTile) return null;
17772
- return units.find(
17773
- (u) => u.position.x === hoveredTile.x && u.position.y === hoveredTile.y && u.health > 0
17774
- ) ?? null;
17775
- }, [hoveredTile, units]);
17776
- const playerUnits = useMemo(() => units.filter((u) => u.team === "player" && u.health > 0), [units]);
17777
- const enemyUnits = useMemo(() => units.filter((u) => u.team === "enemy" && u.health > 0), [units]);
17778
- const validMoves = useMemo(() => {
17779
- if (!selectedUnit || currentPhase !== "movement") return [];
17780
- const moves = [];
17781
- const range = selectedUnit.movement;
17782
- for (let dy = -range; dy <= range; dy++) {
17783
- for (let dx = -range; dx <= range; dx++) {
17784
- const nx = selectedUnit.position.x + dx;
17785
- const ny = selectedUnit.position.y + dy;
17786
- const dist = Math.abs(dx) + Math.abs(dy);
17787
- if (dist > 0 && dist <= range && nx >= 0 && nx < boardWidth && ny >= 0 && ny < boardHeight && !units.some((u) => u.position.x === nx && u.position.y === ny && u.health > 0)) {
17788
- moves.push({ x: nx, y: ny });
17789
- }
17790
- }
17791
- }
17792
- return moves;
17793
- }, [selectedUnit, currentPhase, units, boardWidth, boardHeight]);
17794
- const attackTargets = useMemo(() => {
17795
- if (!selectedUnit || currentPhase !== "action") return [];
17796
- return units.filter((u) => u.team !== selectedUnit.team && u.health > 0).filter((u) => {
17797
- const dx = Math.abs(u.position.x - selectedUnit.position.x);
17798
- const dy = Math.abs(u.position.y - selectedUnit.position.y);
17799
- return dx <= 1 && dy <= 1 && dx + dy > 0;
17800
- }).map((u) => u.position);
17801
- }, [selectedUnit, currentPhase, units]);
17802
- const MOVE_SPEED_MS_PER_TILE = 300;
17803
- const movementAnimRef = useRef(null);
17804
- const [movingPositions, setMovingPositions] = useState(/* @__PURE__ */ new Map());
17805
- const startMoveAnimation = useCallback((unitId, from, to, onComplete) => {
17806
- const dx = to.x - from.x;
17807
- const dy = to.y - from.y;
17808
- const dist = Math.max(Math.abs(dx), Math.abs(dy));
17809
- const duration = dist * MOVE_SPEED_MS_PER_TILE;
17810
- movementAnimRef.current = { unitId, from, to, elapsed: 0, duration, onComplete };
17811
- }, []);
17812
- useEffect(() => {
17813
- const interval = setInterval(() => {
17814
- const anim2 = movementAnimRef.current;
17815
- if (!anim2) return;
17816
- anim2.elapsed += 16;
17817
- const t = Math.min(anim2.elapsed / anim2.duration, 1);
17818
- const eased = 1 - (1 - t) * (1 - t);
17819
- const cx = anim2.from.x + (anim2.to.x - anim2.from.x) * eased;
17820
- const cy = anim2.from.y + (anim2.to.y - anim2.from.y) * eased;
17821
- if (t >= 1) {
17822
- movementAnimRef.current = null;
17823
- setMovingPositions((prev) => {
17824
- const next = new Map(prev);
17825
- next.delete(anim2.unitId);
17826
- return next;
17827
- });
17828
- anim2.onComplete();
17829
- } else {
17830
- setMovingPositions((prev) => {
17831
- const next = new Map(prev);
17832
- next.set(anim2.unitId, { x: cx, y: cy });
17833
- return next;
17834
- });
17835
- }
17836
- }, 16);
17837
- return () => clearInterval(interval);
17838
- }, []);
17839
- const isoUnits = useMemo(() => {
17840
- return units.filter((u) => u.health > 0).map((unit) => {
17841
- const pos = movingPositions.get(unit.id) ?? unit.position;
17842
- return {
17843
- id: unit.id,
17844
- position: pos,
17845
- name: unit.name,
17846
- team: unit.team,
17847
- health: unit.health,
17848
- maxHealth: unit.maxHealth,
17849
- unitType: unit.unitType,
17850
- heroId: unit.heroId,
17851
- sprite: unit.sprite,
17852
- traits: unit.traits?.map((t) => ({
17853
- name: t.name,
17854
- currentState: t.currentState,
17855
- states: t.states,
17856
- cooldown: t.cooldown ?? 0
17857
- }))
17858
- };
17859
- });
17860
- }, [units, movingPositions]);
17861
- const maxY = Math.max(...tiles.map((t) => t.y), 0);
17862
- const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
17863
- const tileToScreen = useCallback(
17864
- (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
17865
- [scale, baseOffsetX]
17866
- );
17867
- const checkGameEnd = useCallback(() => {
17868
- const pa = units.filter((u) => u.team === "player" && u.health > 0);
17869
- const ea = units.filter((u) => u.team === "enemy" && u.health > 0);
17870
- if (pa.length === 0) {
17871
- setGameResult("defeat");
17872
- setCurrentPhase("game_over");
17873
- onGameEnd?.("defeat");
17874
- } else if (ea.length === 0) {
17875
- setGameResult("victory");
17876
- setCurrentPhase("game_over");
17877
- onGameEnd?.("victory");
17878
- }
17879
- }, [units, onGameEnd]);
17880
- const handleUnitClick = useCallback((unitId) => {
17881
- const unit = units.find((u) => u.id === unitId);
17882
- if (!unit) return;
17883
- if (currentPhase === "observation" || currentPhase === "selection") {
17884
- if (unit.team === "player") {
17885
- setSelectedUnitId(unitId);
17886
- setCurrentPhase("movement");
17887
- }
17888
- } else if (currentPhase === "action" && selectedUnit) {
17889
- if (unit.team === "enemy" && attackTargets.some((t) => t.x === unit.position.x && t.y === unit.position.y)) {
17890
- const damage = calculateDamage ? calculateDamage(selectedUnit, unit) : Math.max(1, selectedUnit.attack - unit.defense);
17891
- const newHealth = Math.max(0, unit.health - damage);
17892
- setUnits((prev) => prev.map((u) => u.id === unit.id ? { ...u, health: newHealth } : u));
17893
- setIsShaking(true);
17894
- setTimeout(() => setIsShaking(false), 300);
17895
- onAttack?.(selectedUnit, unit, damage);
17896
- setSelectedUnitId(null);
17897
- setCurrentPhase("observation");
17898
- setCurrentTurn((t) => t + 1);
17899
- setTimeout(checkGameEnd, 100);
18831
+ className: "border-b-2 border-x-0 border-t-0 border-[var(--color-border)] flex items-center justify-between flex-shrink-0",
18832
+ children: [
18833
+ /* @__PURE__ */ jsxs("div", { children: [
18834
+ /* @__PURE__ */ jsx(Typography, { variant: "h3", children: title }),
18835
+ subtitle && /* @__PURE__ */ jsx(Typography, { variant: "body2", color: "secondary", className: "mt-1", children: subtitle })
18836
+ ] }),
18837
+ headerActions && /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2", children: headerActions })
18838
+ ]
17900
18839
  }
17901
- }
17902
- }, [currentPhase, selectedUnit, attackTargets, units, checkGameEnd, onAttack, calculateDamage]);
17903
- const handleTileClick = useCallback((x, y) => {
17904
- if (currentPhase === "movement" && selectedUnit) {
17905
- if (movementAnimRef.current) return;
17906
- if (validMoves.some((m) => m.x === x && m.y === y)) {
17907
- const from = { ...selectedUnit.position };
17908
- const to = { x, y };
17909
- startMoveAnimation(selectedUnit.id, from, to, () => {
17910
- setUnits(
17911
- (prev) => prev.map((u) => u.id === selectedUnitId ? { ...u, position: { x, y } } : u)
17912
- );
17913
- onUnitMove?.(selectedUnit, to);
17914
- setCurrentPhase("action");
17915
- });
18840
+ ),
18841
+ /* @__PURE__ */ jsx(Box, { fullWidth: true, overflow: "auto", className: "flex-1", children: /* @__PURE__ */ jsx(Box, { padding: "lg", children }) }),
18842
+ footer && /* @__PURE__ */ jsx(
18843
+ Box,
18844
+ {
18845
+ padding: "md",
18846
+ border: true,
18847
+ bg: "muted",
18848
+ className: "border-t-2 border-x-0 border-b-0 border-[var(--color-border)] flex-shrink-0",
18849
+ children: footer
17916
18850
  }
17917
- }
17918
- }, [currentPhase, selectedUnit, selectedUnitId, validMoves, startMoveAnimation, onUnitMove]);
17919
- const handleEndTurn = useCallback(() => {
17920
- setSelectedUnitId(null);
17921
- setCurrentPhase("observation");
17922
- setCurrentTurn((t) => t + 1);
17923
- }, []);
17924
- const handleCancel = useCallback(() => {
17925
- setSelectedUnitId(null);
17926
- setCurrentPhase("observation");
17927
- }, []);
17928
- const handleReset = useCallback(() => {
17929
- setUnits(initialUnits);
17930
- setSelectedUnitId(null);
17931
- setCurrentPhase("observation");
17932
- setCurrentTurn(1);
17933
- setGameResult(null);
17934
- }, [initialUnits]);
17935
- const ctx = useMemo(
17936
- () => ({
17937
- phase: currentPhase,
17938
- turn: currentTurn,
17939
- selectedUnit,
17940
- hoveredUnit,
17941
- playerUnits,
17942
- enemyUnits,
17943
- gameResult,
17944
- onEndTurn: handleEndTurn,
17945
- onCancel: handleCancel,
17946
- onReset: handleReset,
17947
- attackTargets,
17948
- tileToScreen
17949
- }),
17950
- [
17951
- currentPhase,
17952
- currentTurn,
17953
- selectedUnit,
17954
- hoveredUnit,
17955
- playerUnits,
17956
- enemyUnits,
17957
- gameResult,
17958
- handleEndTurn,
17959
- handleCancel,
17960
- handleReset,
17961
- attackTargets,
17962
- tileToScreen
17963
- ]
17964
- );
17965
- const shakeStyle = isShaking ? { animation: "battle-shake 0.3s ease-in-out" } : {};
17966
- return /* @__PURE__ */ jsxs("div", { className: cn("battle-template relative flex flex-col min-h-[600px] bg-[var(--color-background)]", className), children: [
17967
- /* @__PURE__ */ jsx("style", { children: `
17968
- @keyframes battle-shake {
17969
- 0%, 100% { transform: translate(0, 0); }
17970
- 10% { transform: translate(-3px, -2px); }
17971
- 20% { transform: translate(3px, 1px); }
17972
- 30% { transform: translate(-2px, 3px); }
17973
- 40% { transform: translate(2px, -1px); }
17974
- 50% { transform: translate(-3px, 2px); }
17975
- 60% { transform: translate(3px, -2px); }
17976
- 70% { transform: translate(-1px, 3px); }
17977
- 80% { transform: translate(2px, -3px); }
17978
- 90% { transform: translate(-2px, 1px); }
18851
+ )
18852
+ ] });
18853
+ };
18854
+ GenericAppTemplate.displayName = "GenericAppTemplate";
18855
+ var GameShell = ({
18856
+ appName = "Game",
18857
+ hud,
18858
+ className,
18859
+ showTopBar = true
18860
+ }) => {
18861
+ return /* @__PURE__ */ jsxs(
18862
+ "div",
18863
+ {
18864
+ className: cn(
18865
+ "game-shell",
18866
+ "flex flex-col w-full h-screen overflow-hidden",
18867
+ className
18868
+ ),
18869
+ style: {
18870
+ width: "100vw",
18871
+ height: "100vh",
18872
+ display: "flex",
18873
+ flexDirection: "column",
18874
+ overflow: "hidden",
18875
+ background: "var(--color-background, #0a0a0f)",
18876
+ color: "var(--color-text, #e0e0e0)"
18877
+ },
18878
+ children: [
18879
+ showTopBar && /* @__PURE__ */ jsxs(
18880
+ "header",
18881
+ {
18882
+ className: "game-shell__header",
18883
+ style: {
18884
+ display: "flex",
18885
+ alignItems: "center",
18886
+ justifyContent: "space-between",
18887
+ padding: "0.5rem 1rem",
18888
+ borderBottom: "1px solid var(--color-border, #2a2a3a)",
18889
+ background: "var(--color-surface, #12121f)",
18890
+ flexShrink: 0
18891
+ },
18892
+ children: [
18893
+ /* @__PURE__ */ jsx(
18894
+ "span",
18895
+ {
18896
+ style: {
18897
+ fontWeight: 700,
18898
+ fontSize: "1.1rem",
18899
+ letterSpacing: "0.02em"
18900
+ },
18901
+ children: appName
17979
18902
  }
17980
- ` }),
17981
- header && /* @__PURE__ */ jsx("div", { className: "p-4", children: header(ctx) }),
17982
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 gap-4 p-4 pt-0", children: [
17983
- /* @__PURE__ */ jsxs("div", { className: "relative flex-1", style: shakeStyle, children: [
18903
+ ),
18904
+ hud && /* @__PURE__ */ jsx("div", { className: "game-shell__hud", children: hud })
18905
+ ]
18906
+ }
18907
+ ),
17984
18908
  /* @__PURE__ */ jsx(
17985
- IsometricCanvas_default,
18909
+ "main",
17986
18910
  {
17987
- tiles,
17988
- units: isoUnits,
17989
- features,
17990
- selectedUnitId,
17991
- validMoves,
17992
- attackTargets,
17993
- hoveredTile,
17994
- onTileClick: handleTileClick,
17995
- onUnitClick: handleUnitClick,
17996
- onTileHover: (x, y) => setHoveredTile({ x, y }),
17997
- onTileLeave: () => setHoveredTile(null),
17998
- scale,
17999
- assetBaseUrl: assetManifest?.baseUrl,
18000
- assetManifest,
18001
- backgroundImage,
18002
- onDrawEffects,
18003
- hasActiveEffects: hasActiveEffects2,
18004
- effectSpriteUrls,
18005
- resolveUnitFrame,
18006
- unitScale
18911
+ className: "game-shell__content",
18912
+ style: {
18913
+ flex: 1,
18914
+ overflow: "hidden",
18915
+ position: "relative"
18916
+ },
18917
+ children: /* @__PURE__ */ jsx(Outlet, {})
18007
18918
  }
18008
- ),
18009
- overlay && overlay(ctx)
18010
- ] }),
18011
- sidebar && /* @__PURE__ */ jsx("div", { className: "w-80 shrink-0", children: sidebar(ctx) })
18012
- ] }),
18013
- actions ? actions(ctx) : currentPhase !== "game_over" && /* @__PURE__ */ jsxs("div", { className: "fixed bottom-6 right-6 z-50 flex gap-2", children: [
18014
- (currentPhase === "movement" || currentPhase === "action") && /* @__PURE__ */ jsx(
18015
- "button",
18016
- {
18017
- className: "px-4 py-2 rounded-lg bg-[var(--color-surface)] text-[var(--color-foreground)] border border-[var(--color-border)] shadow-xl hover:opacity-90",
18018
- onClick: handleCancel,
18019
- children: "Cancel"
18020
- }
18021
- ),
18022
- /* @__PURE__ */ jsx(
18023
- "button",
18024
- {
18025
- className: "px-4 py-2 rounded-lg bg-[var(--color-primary)] text-white shadow-xl hover:opacity-90",
18026
- onClick: handleEndTurn,
18027
- children: "End Turn"
18028
- }
18029
- )
18030
- ] }),
18031
- gameResult && (gameOverOverlay ? gameOverOverlay(ctx) : /* @__PURE__ */ jsx("div", { className: "absolute inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm rounded-xl", children: /* @__PURE__ */ jsxs("div", { className: "text-center space-y-6 p-8", children: [
18032
- /* @__PURE__ */ jsx(
18033
- "h2",
18034
- {
18035
- className: cn(
18036
- "text-4xl font-black tracking-widest uppercase",
18037
- gameResult === "victory" ? "text-yellow-400" : "text-red-500"
18038
- ),
18039
- children: gameResult === "victory" ? "Victory!" : "Defeat"
18040
- }
18041
- ),
18042
- /* @__PURE__ */ jsxs("p", { className: "text-gray-300", children: [
18043
- "Turns played: ",
18044
- currentTurn
18045
- ] }),
18046
- /* @__PURE__ */ jsx(
18047
- "button",
18048
- {
18049
- className: "px-8 py-3 rounded-lg bg-[var(--color-primary)] text-white font-semibold hover:opacity-90",
18050
- onClick: handleReset,
18051
- children: "Play Again"
18052
- }
18053
- )
18054
- ] }) }))
18055
- ] });
18056
- }
18057
- BattleTemplate.displayName = "BattleTemplate";
18058
- function CastleTemplate({
18059
- tiles,
18060
- features = [],
18061
- units = [],
18919
+ )
18920
+ ]
18921
+ }
18922
+ );
18923
+ };
18924
+ GameShell.displayName = "GameShell";
18925
+ function BattleTemplate({
18926
+ entity,
18927
+ scale = 0.45,
18928
+ unitScale = 1,
18929
+ className
18930
+ }) {
18931
+ return /* @__PURE__ */ jsx(
18932
+ BattleBoard,
18933
+ {
18934
+ entity,
18935
+ scale,
18936
+ unitScale,
18937
+ tileClickEvent: "TILE_CLICK",
18938
+ unitClickEvent: "UNIT_CLICK",
18939
+ endTurnEvent: "END_TURN",
18940
+ cancelEvent: "CANCEL",
18941
+ gameEndEvent: "GAME_END",
18942
+ playAgainEvent: "PLAY_AGAIN",
18943
+ attackEvent: "ATTACK",
18944
+ className
18945
+ }
18946
+ );
18947
+ }
18948
+ BattleTemplate.displayName = "BattleTemplate";
18949
+ function CastleTemplate({
18950
+ entity,
18062
18951
  scale = 0.45,
18063
- assetManifest,
18064
- backgroundImage,
18065
- header,
18066
- sidePanel,
18067
- overlay,
18068
- footer,
18069
- onFeatureClick,
18070
- onUnitClick,
18071
- onTileClick,
18072
18952
  className
18073
18953
  }) {
18074
- const [hoveredTile, setHoveredTile] = useState(null);
18075
- const [selectedFeature, setSelectedFeature] = useState(null);
18076
- const hoveredFeature = useMemo(() => {
18077
- if (!hoveredTile) return null;
18078
- return features.find((f) => f.x === hoveredTile.x && f.y === hoveredTile.y) ?? null;
18079
- }, [hoveredTile, features]);
18080
- const hoveredUnit = useMemo(() => {
18081
- if (!hoveredTile) return null;
18082
- return units.find(
18083
- (u) => u.position.x === hoveredTile.x && u.position.y === hoveredTile.y
18084
- ) ?? null;
18085
- }, [hoveredTile, units]);
18086
- const maxY = Math.max(...tiles.map((t) => t.y), 0);
18087
- const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
18088
- const tileToScreen = useCallback(
18089
- (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
18090
- [scale, baseOffsetX]
18091
- );
18092
- const handleTileClick = useCallback((x, y) => {
18093
- const feature = features.find((f) => f.x === x && f.y === y);
18094
- if (feature) {
18095
- setSelectedFeature(feature);
18096
- onFeatureClick?.(feature);
18954
+ return /* @__PURE__ */ jsx(
18955
+ CastleBoard,
18956
+ {
18957
+ entity,
18958
+ scale,
18959
+ featureClickEvent: "FEATURE_CLICK",
18960
+ unitClickEvent: "UNIT_CLICK",
18961
+ tileClickEvent: "TILE_CLICK",
18962
+ className
18097
18963
  }
18098
- onTileClick?.(x, y);
18099
- }, [features, onFeatureClick, onTileClick]);
18100
- const handleUnitClick = useCallback((unitId) => {
18101
- const unit = units.find((u) => u.id === unitId);
18102
- if (unit) onUnitClick?.(unit);
18103
- }, [units, onUnitClick]);
18104
- const clearSelection = useCallback(() => setSelectedFeature(null), []);
18105
- const ctx = useMemo(
18106
- () => ({
18107
- hoveredTile,
18108
- hoveredFeature,
18109
- hoveredUnit,
18110
- selectedFeature,
18111
- clearSelection,
18112
- tileToScreen,
18113
- scale
18114
- }),
18115
- [hoveredTile, hoveredFeature, hoveredUnit, selectedFeature, clearSelection, tileToScreen, scale]
18116
18964
  );
18117
- return /* @__PURE__ */ jsxs("div", { className: cn("castle-template min-h-screen flex flex-col bg-[var(--color-background)]", className), children: [
18118
- header && header(ctx),
18119
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
18120
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto p-4 relative", children: [
18121
- /* @__PURE__ */ jsx(
18122
- IsometricCanvas_default,
18123
- {
18124
- tiles,
18125
- units,
18126
- features,
18127
- hoveredTile,
18128
- onTileClick: handleTileClick,
18129
- onUnitClick: handleUnitClick,
18130
- onTileHover: (x, y) => setHoveredTile({ x, y }),
18131
- onTileLeave: () => setHoveredTile(null),
18132
- scale,
18133
- assetBaseUrl: assetManifest?.baseUrl,
18134
- assetManifest,
18135
- backgroundImage
18136
- }
18137
- ),
18138
- overlay && overlay(ctx)
18139
- ] }),
18140
- sidePanel && /* @__PURE__ */ jsx("div", { className: "w-96 shrink-0 border-l border-[var(--color-border)] bg-[var(--color-surface)] overflow-y-auto", children: sidePanel(ctx) })
18141
- ] }),
18142
- footer && footer(ctx)
18143
- ] });
18144
18965
  }
18145
18966
  CastleTemplate.displayName = "CastleTemplate";
18146
- function defaultIsInRange(from, to, range) {
18147
- return Math.abs(from.x - to.x) + Math.abs(from.y - to.y) <= range;
18148
- }
18149
18967
  function WorldMapTemplate({
18150
- hexes,
18151
- heroes,
18152
- features = [],
18153
- selectedHeroId,
18968
+ entity,
18154
18969
  scale = 0.4,
18155
18970
  unitScale = 2.5,
18156
- assetManifest,
18157
- backgroundImage,
18158
18971
  allowMoveAllHeroes = false,
18159
- isInRange = defaultIsInRange,
18160
- header,
18161
- sidePanel,
18162
- overlay,
18163
- footer,
18164
- onHeroSelect,
18165
- onHeroMove,
18166
- onBattleEncounter,
18167
- onFeatureEnter,
18168
- effectSpriteUrls = [],
18169
- resolveUnitFrame,
18170
18972
  className
18171
18973
  }) {
18172
- const [hoveredTile, setHoveredTile] = useState(null);
18173
- const selectedHero = useMemo(
18174
- () => heroes.find((h) => h.id === selectedHeroId) ?? null,
18175
- [heroes, selectedHeroId]
18176
- );
18177
- const tiles = useMemo(
18178
- () => hexes.map((hex) => ({
18179
- x: hex.x,
18180
- y: hex.y,
18181
- terrain: hex.terrain,
18182
- terrainSprite: hex.terrainSprite
18183
- })),
18184
- [hexes]
18185
- );
18186
- const baseUnits = useMemo(
18187
- () => heroes.map((hero) => ({
18188
- id: hero.id,
18189
- position: hero.position,
18190
- name: hero.name,
18191
- team: hero.owner === "enemy" ? "enemy" : "player",
18192
- health: 100,
18193
- maxHealth: 100,
18194
- sprite: hero.sprite
18195
- })),
18196
- [heroes]
18197
- );
18198
- const MOVE_SPEED_MS_PER_TILE = 300;
18199
- const movementAnimRef = useRef(null);
18200
- const [movingPositions, setMovingPositions] = useState(/* @__PURE__ */ new Map());
18201
- const startMoveAnimation = useCallback((heroId, from, to, onComplete) => {
18202
- const dist = Math.max(Math.abs(to.x - from.x), Math.abs(to.y - from.y));
18203
- movementAnimRef.current = { heroId, from, to, elapsed: 0, duration: dist * MOVE_SPEED_MS_PER_TILE, onComplete };
18204
- }, []);
18205
- useEffect(() => {
18206
- const interval = setInterval(() => {
18207
- const anim2 = movementAnimRef.current;
18208
- if (!anim2) return;
18209
- anim2.elapsed += 16;
18210
- const t = Math.min(anim2.elapsed / anim2.duration, 1);
18211
- const eased = 1 - (1 - t) * (1 - t);
18212
- const cx = anim2.from.x + (anim2.to.x - anim2.from.x) * eased;
18213
- const cy = anim2.from.y + (anim2.to.y - anim2.from.y) * eased;
18214
- if (t >= 1) {
18215
- movementAnimRef.current = null;
18216
- setMovingPositions((prev) => {
18217
- const n = new Map(prev);
18218
- n.delete(anim2.heroId);
18219
- return n;
18220
- });
18221
- anim2.onComplete();
18222
- } else {
18223
- setMovingPositions((prev) => {
18224
- const n = new Map(prev);
18225
- n.set(anim2.heroId, { x: cx, y: cy });
18226
- return n;
18227
- });
18228
- }
18229
- }, 16);
18230
- return () => clearInterval(interval);
18231
- }, []);
18232
- const isoUnits = useMemo(() => {
18233
- if (movingPositions.size === 0) return baseUnits;
18234
- return baseUnits.map((u) => {
18235
- const pos = movingPositions.get(u.id);
18236
- return pos ? { ...u, position: pos } : u;
18237
- });
18238
- }, [baseUnits, movingPositions]);
18239
- const validMoves = useMemo(() => {
18240
- if (!selectedHero || selectedHero.movement <= 0) return [];
18241
- const moves = [];
18242
- hexes.forEach((hex) => {
18243
- if (hex.passable === false) return;
18244
- if (hex.x === selectedHero.position.x && hex.y === selectedHero.position.y) return;
18245
- if (!isInRange(selectedHero.position, { x: hex.x, y: hex.y }, selectedHero.movement)) return;
18246
- if (heroes.some((h) => h.position.x === hex.x && h.position.y === hex.y && h.owner === selectedHero.owner)) return;
18247
- moves.push({ x: hex.x, y: hex.y });
18248
- });
18249
- return moves;
18250
- }, [selectedHero, hexes, heroes, isInRange]);
18251
- const attackTargets = useMemo(() => {
18252
- if (!selectedHero || selectedHero.movement <= 0) return [];
18253
- return heroes.filter((h) => h.owner !== selectedHero.owner).filter((h) => isInRange(selectedHero.position, h.position, selectedHero.movement)).map((h) => h.position);
18254
- }, [selectedHero, heroes, isInRange]);
18255
- const maxY = Math.max(...hexes.map((h) => h.y), 0);
18256
- const baseOffsetX = (maxY + 1) * (TILE_WIDTH * scale / 2);
18257
- const tileToScreen = useCallback(
18258
- (tx, ty) => isoToScreen(tx, ty, scale, baseOffsetX),
18259
- [scale, baseOffsetX]
18260
- );
18261
- const hoveredHex = useMemo(
18262
- () => hoveredTile ? hexes.find((h) => h.x === hoveredTile.x && h.y === hoveredTile.y) ?? null : null,
18263
- [hoveredTile, hexes]
18264
- );
18265
- const hoveredHero = useMemo(
18266
- () => hoveredTile ? heroes.find((h) => h.position.x === hoveredTile.x && h.position.y === hoveredTile.y) ?? null : null,
18267
- [hoveredTile, heroes]
18268
- );
18269
- const handleTileClick = useCallback((x, y) => {
18270
- if (movementAnimRef.current) return;
18271
- const hex = hexes.find((h) => h.x === x && h.y === y);
18272
- if (!hex) return;
18273
- if (selectedHero && validMoves.some((m) => m.x === x && m.y === y)) {
18274
- startMoveAnimation(selectedHero.id, { ...selectedHero.position }, { x, y }, () => {
18275
- onHeroMove?.(selectedHero.id, x, y);
18276
- if (hex.feature && hex.feature !== "none") {
18277
- onFeatureEnter?.(selectedHero.id, hex);
18278
- }
18279
- });
18280
- return;
18281
- }
18282
- const enemy = heroes.find((h) => h.position.x === x && h.position.y === y && h.owner === "enemy");
18283
- if (selectedHero && enemy && attackTargets.some((t) => t.x === x && t.y === y)) {
18284
- onBattleEncounter?.(selectedHero.id, enemy.id);
18285
- }
18286
- }, [hexes, heroes, selectedHero, validMoves, attackTargets, startMoveAnimation, onHeroMove, onFeatureEnter, onBattleEncounter]);
18287
- const handleUnitClick = useCallback((unitId) => {
18288
- const hero = heroes.find((h) => h.id === unitId);
18289
- if (hero && (hero.owner === "player" || allowMoveAllHeroes)) {
18290
- onHeroSelect?.(unitId);
18974
+ return /* @__PURE__ */ jsx(
18975
+ WorldMapBoard,
18976
+ {
18977
+ entity,
18978
+ scale,
18979
+ unitScale,
18980
+ allowMoveAllHeroes,
18981
+ heroSelectEvent: "HERO_SELECT",
18982
+ heroMoveEvent: "HERO_MOVE",
18983
+ battleEncounterEvent: "BATTLE_ENCOUNTER",
18984
+ featureEnterEvent: "FEATURE_ENTER",
18985
+ className
18291
18986
  }
18292
- }, [heroes, onHeroSelect, allowMoveAllHeroes]);
18293
- const selectHero = useCallback((id) => onHeroSelect?.(id), [onHeroSelect]);
18294
- const ctx = useMemo(
18295
- () => ({
18296
- hoveredTile,
18297
- hoveredHex,
18298
- hoveredHero,
18299
- selectedHero,
18300
- validMoves,
18301
- selectHero,
18302
- tileToScreen,
18303
- scale
18304
- }),
18305
- [hoveredTile, hoveredHex, hoveredHero, selectedHero, validMoves, selectHero, tileToScreen, scale]
18306
18987
  );
18307
- return /* @__PURE__ */ jsxs("div", { className: cn("world-map-template min-h-screen flex flex-col bg-[var(--color-background)]", className), children: [
18308
- header && header(ctx),
18309
- /* @__PURE__ */ jsxs("div", { className: "flex flex-1 overflow-hidden", children: [
18310
- /* @__PURE__ */ jsxs("div", { className: "flex-1 overflow-auto p-4 relative", children: [
18311
- /* @__PURE__ */ jsx(
18312
- IsometricCanvas_default,
18313
- {
18314
- tiles,
18315
- units: isoUnits,
18316
- features,
18317
- selectedUnitId: selectedHeroId,
18318
- validMoves,
18319
- attackTargets,
18320
- hoveredTile,
18321
- onTileClick: handleTileClick,
18322
- onUnitClick: handleUnitClick,
18323
- onTileHover: (x, y) => setHoveredTile({ x, y }),
18324
- onTileLeave: () => setHoveredTile(null),
18325
- scale,
18326
- assetBaseUrl: assetManifest?.baseUrl,
18327
- assetManifest,
18328
- backgroundImage,
18329
- effectSpriteUrls,
18330
- resolveUnitFrame,
18331
- unitScale
18332
- }
18333
- ),
18334
- overlay && overlay(ctx)
18335
- ] }),
18336
- sidePanel && /* @__PURE__ */ jsx("div", { className: "w-80 shrink-0 border-l border-[var(--color-border)] bg-[var(--color-surface)] overflow-y-auto p-4", children: sidePanel(ctx) })
18337
- ] }),
18338
- footer && footer(ctx)
18339
- ] });
18340
18988
  }
18341
18989
  WorldMapTemplate.displayName = "WorldMapTemplate";
18342
18990
 
18343
- export { Accordion, Card2 as ActionCard, Alert, AuthLayout, Avatar, Badge, BattleTemplate, Box, Breadcrumb, Button, ButtonGroup, CanvasEffect, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, CastleTemplate, Center, Chart, Checkbox, CodeViewer, ConditionalWrapper, ConfirmDialog, Container, ControlButton, CounterTemplate, CrudTemplate, DashboardGrid, DashboardLayout, DataTable, DetailPanel, DialogueBox, Divider, DocumentViewer, Drawer, DrawerSlot, EmptyState, ErrorState, FEATURE_COLORS, FLOOR_HEIGHT, FilterGroup, Flex, FloatingActionButton, Form, FormActions, FormField, FormLayout, FormSection, FormSectionHeader, FormTemplate, GameHud, GameMenu, GameOverScreen, GameShell, GameTemplate, GenericAppTemplate, GraphCanvas, Grid2 as Grid, HStack, Header, Heading, HealthBar, Icon, Input, InputGroup, InventoryPanel, IsometricCanvas, Label, LawReferenceTooltip, List2 as List, ListTemplate, LoadingState, MasterDetail, MediaGallery, Menu2 as Menu, Meter, Modal, ModalSlot, Navigation, OrbitalVisualization, Overlay, PageHeader, Pagination, Popover, ProgressBar, Radio, RelationSelect, RepeatableFormSection, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, ScoreDisplay, SearchInput, Section, Select, SettingsTemplate, SidePanel, Sidebar, SignaturePad, SimpleGrid, SlotContentRenderer, Spacer, Spinner, Split, SplitPane, Sprite, Stack, StatCard, Switch, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, Table, Tabs, Text, TextHighlight, Textarea, ThemeSelector, ThemeToggle, Timeline, Toast, ToastSlot, Tooltip, Typography, UISlotComponent, UISlotRenderer, VStack, ViolationAlert, WizardContainer, WizardNavigation, WizardProgress, WorldMapTemplate, createUnitAnimationState, drawSprite, getCurrentFrame, inferDirection, isoToScreen, resolveFrame, resolveSheetDirection, screenToIso, tickAnimationState, transitionAnimation, useCamera, useImageCache, useSpriteAnimations };
18991
+ export { Accordion, Card2 as ActionCard, Alert, AuthLayout, Avatar, Badge, BattleBoard, BattleTemplate, Box, Breadcrumb, Button, ButtonGroup, CanvasEffect, Card, CardBody, CardContent, CardFooter, CardGrid, CardHeader, CardTitle, CastleBoard, CastleTemplate, Center, Chart, Checkbox, CodeViewer, CollapsibleSection, ConditionalWrapper, ConfirmDialog, Container, ControlButton, CounterTemplate, CrudTemplate, DIAMOND_TOP_Y, DashboardGrid, DashboardLayout, DataTable, DetailPanel, DialogueBox, Divider, DocumentViewer, Drawer, DrawerSlot, EditorCheckbox, EditorSelect, EditorSlider, EditorTextInput, EditorToolbar, EmptyState, ErrorState, FEATURE_COLORS, FEATURE_TYPES, FLOOR_HEIGHT, FilterGroup, Flex, FloatingActionButton, Form, FormActions, FormField, FormLayout, FormSection, FormSectionHeader, FormTemplate, GameHud, GameMenu, GameOverScreen, GameShell, GameTemplate, GenericAppTemplate, GraphCanvas, Grid2 as Grid, HStack, Header, Heading, HealthBar, Icon, Input, InputGroup, InventoryPanel, IsometricCanvas, Label, LawReferenceTooltip, List2 as List, ListTemplate, LoadingState, MasterDetail, MediaGallery, Menu2 as Menu, Meter, Modal, ModalSlot, Navigation, OrbitalVisualization, Overlay, PageHeader, Pagination, PhysicsManager, Popover, ProgressBar, Radio, RelationSelect, RepeatableFormSection, SHEET_COLUMNS, SPRITE_SHEET_LAYOUT, ScoreDisplay, SearchInput, Section, Select, SettingsTemplate, SidePanel, Sidebar, SignaturePad, SimpleGrid, SlotContentRenderer, Spacer, Spinner, Split, SplitPane, Sprite, Stack, StatCard, StatusBar, Switch, TERRAIN_COLORS, TILE_HEIGHT, TILE_WIDTH, TabbedContainer, Table, Tabs, TerrainPalette, Text, TextHighlight, Textarea, ThemeSelector, ThemeToggle, Timeline, Toast, ToastSlot, Tooltip, Typography, UISlotComponent, UISlotRenderer, VStack, ViolationAlert, WizardContainer, WizardNavigation, WizardProgress, WorldMapBoard, WorldMapTemplate, createUnitAnimationState, drawSprite, getCurrentFrame, inferDirection, isoToScreen, resolveFrame, resolveSheetDirection, screenToIso, tickAnimationState, transitionAnimation, useCamera, useImageCache, usePhysics2D, useSpriteAnimations };