@almadar/ui 5.64.0 → 5.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14723,16 +14723,26 @@ var init_useUnitSpriteAtlas = __esm({
14723
14723
  });
14724
14724
 
14725
14725
  // components/game/organisms/utils/isometric.ts
14726
- function isoToScreen(tileX, tileY, scale, baseOffsetX) {
14726
+ function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
14727
14727
  const scaledTileWidth = TILE_WIDTH * scale;
14728
14728
  const scaledFloorHeight = FLOOR_HEIGHT * scale;
14729
+ if (layout === "hex") {
14730
+ const screenX2 = tileX * scaledTileWidth + (tileY & 1) * (scaledTileWidth / 2) + baseOffsetX;
14731
+ const screenY2 = tileY * (scaledFloorHeight * 0.75);
14732
+ return { x: screenX2, y: screenY2 };
14733
+ }
14729
14734
  const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
14730
14735
  const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
14731
14736
  return { x: screenX, y: screenY };
14732
14737
  }
14733
- function screenToIso(screenX, screenY, scale, baseOffsetX) {
14738
+ function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
14734
14739
  const scaledTileWidth = TILE_WIDTH * scale;
14735
14740
  const scaledFloorHeight = FLOOR_HEIGHT * scale;
14741
+ if (layout === "hex") {
14742
+ const row = Math.round(screenY / (scaledFloorHeight * 0.75));
14743
+ const col = Math.round((screenX - (row & 1) * (scaledTileWidth / 2) - baseOffsetX) / scaledTileWidth);
14744
+ return { x: col, y: row };
14745
+ }
14736
14746
  const adjustedX = screenX - baseOffsetX;
14737
14747
  const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
14738
14748
  const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
@@ -14782,6 +14792,7 @@ function IsometricCanvas({
14782
14792
  tileHoverEvent,
14783
14793
  tileLeaveEvent,
14784
14794
  // Rendering options
14795
+ tileLayout = "isometric",
14785
14796
  scale = 0.4,
14786
14797
  debug: debug2 = false,
14787
14798
  backgroundImage = "",
@@ -14851,13 +14862,17 @@ function IsometricCanvas({
14851
14862
  );
14852
14863
  const sortedTiles = React93.useMemo(() => {
14853
14864
  const tiles = [...tilesProp];
14854
- tiles.sort((a, b) => {
14855
- const depthA = a.x + a.y;
14856
- const depthB = b.x + b.y;
14857
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
14858
- });
14865
+ if (tileLayout === "hex") {
14866
+ tiles.sort((a, b) => a.y !== b.y ? a.y - b.y : a.x - b.x);
14867
+ } else {
14868
+ tiles.sort((a, b) => {
14869
+ const depthA = a.x + a.y;
14870
+ const depthB = b.x + b.y;
14871
+ return depthA !== depthB ? depthA - depthB : a.y - b.y;
14872
+ });
14873
+ }
14859
14874
  return tiles;
14860
- }, [tilesProp]);
14875
+ }, [tilesProp, tileLayout]);
14861
14876
  const gridWidth = React93.useMemo(() => {
14862
14877
  if (sortedTiles.length === 0) return 0;
14863
14878
  return Math.max(...sortedTiles.map((t2) => t2.x)) + 1;
@@ -14971,7 +14986,7 @@ function IsometricCanvas({
14971
14986
  miniCanvas.width = mW;
14972
14987
  miniCanvas.height = mH;
14973
14988
  mCtx.clearRect(0, 0, mW, mH);
14974
- const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX));
14989
+ const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX, tileLayout));
14975
14990
  const minX = Math.min(...allScreenPos.map((p2) => p2.x));
14976
14991
  const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
14977
14992
  const minY = Math.min(...allScreenPos.map((p2) => p2.y));
@@ -14982,7 +14997,7 @@ function IsometricCanvas({
14982
14997
  const offsetMx = (mW - worldW * scaleM) / 2;
14983
14998
  const offsetMy = (mH - worldH * scaleM) / 2;
14984
14999
  for (const tile of sortedTiles) {
14985
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
15000
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
14986
15001
  const mx = (pos.x - minX) * scaleM + offsetMx;
14987
15002
  const my = (pos.y - minY) * scaleM + offsetMy;
14988
15003
  const mTileW = scaledTileWidth * scaleM;
@@ -14998,7 +15013,7 @@ function IsometricCanvas({
14998
15013
  }
14999
15014
  for (const unit of units) {
15000
15015
  if (!unit.position) continue;
15001
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15016
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15002
15017
  const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
15003
15018
  const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
15004
15019
  mCtx.fillStyle = unit.team === "player" ? "#60a5fa" : unit.team === "enemy" ? "#f87171" : "#9ca3af";
@@ -15014,7 +15029,7 @@ function IsometricCanvas({
15014
15029
  mCtx.strokeStyle = "rgba(255, 255, 255, 0.8)";
15015
15030
  mCtx.lineWidth = 1;
15016
15031
  mCtx.strokeRect(vLeft, vTop, vW, vH);
15017
- }, [showMinimap, sortedTiles, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
15032
+ }, [showMinimap, sortedTiles, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
15018
15033
  const draw = React93.useCallback((animTime) => {
15019
15034
  const canvas = canvasRef.current;
15020
15035
  if (!canvas) return;
@@ -15054,7 +15069,7 @@ function IsometricCanvas({
15054
15069
  const visTop = cam.y - viewportSize.height / cam.zoom;
15055
15070
  const visBottom = cam.y + viewportSize.height * 2 / cam.zoom;
15056
15071
  for (const tile of sortedTiles) {
15057
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
15072
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
15058
15073
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15059
15074
  continue;
15060
15075
  }
@@ -15134,12 +15149,13 @@ function IsometricCanvas({
15134
15149
  }
15135
15150
  }
15136
15151
  const sortedFeatures = [...features].sort((a, b) => {
15152
+ if (tileLayout === "hex") return a.y !== b.y ? a.y - b.y : a.x - b.x;
15137
15153
  const depthA = a.x + a.y;
15138
15154
  const depthB = b.x + b.y;
15139
15155
  return depthA !== depthB ? depthA - depthB : a.y - b.y;
15140
15156
  });
15141
15157
  for (const feature of sortedFeatures) {
15142
- const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX);
15158
+ const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX, tileLayout);
15143
15159
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15144
15160
  continue;
15145
15161
  }
@@ -15174,12 +15190,13 @@ function IsometricCanvas({
15174
15190
  }
15175
15191
  const unitsWithPosition = units.filter((u) => !!u.position);
15176
15192
  const sortedUnits = [...unitsWithPosition].sort((a, b) => {
15193
+ if (tileLayout === "hex") return a.position.y !== b.position.y ? a.position.y - b.position.y : a.position.x - b.position.x;
15177
15194
  const depthA = a.position.x + a.position.y;
15178
15195
  const depthB = b.position.x + b.position.y;
15179
15196
  return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
15180
15197
  });
15181
15198
  for (const unit of sortedUnits) {
15182
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15199
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15183
15200
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15184
15201
  continue;
15185
15202
  }
@@ -15199,7 +15216,7 @@ function IsometricCanvas({
15199
15216
  drawH = maxUnitW / ar;
15200
15217
  }
15201
15218
  if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
15202
- const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
15219
+ const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX, tileLayout);
15203
15220
  const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
15204
15221
  const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
15205
15222
  ctx.save();
@@ -15328,6 +15345,7 @@ function IsometricCanvas({
15328
15345
  units,
15329
15346
  features,
15330
15347
  selectedUnitId,
15348
+ tileLayout,
15331
15349
  scale,
15332
15350
  debug2,
15333
15351
  resolveTerrainSpriteUrl,
@@ -15356,14 +15374,14 @@ function IsometricCanvas({
15356
15374
  if (!selectedUnitId) return;
15357
15375
  const unit = units.find((u) => u.id === selectedUnitId);
15358
15376
  if (!unit?.position) return;
15359
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15377
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15360
15378
  const centerX = pos.x + scaledTileWidth / 2;
15361
15379
  const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
15362
15380
  targetCameraRef.current = {
15363
15381
  x: centerX - viewportSize.width / 2,
15364
15382
  y: centerY - viewportSize.height / 2
15365
15383
  };
15366
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15384
+ }, [selectedUnitId, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15367
15385
  React93.useEffect(() => {
15368
15386
  const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
15369
15387
  draw(animTimeRef.current);
@@ -15396,13 +15414,13 @@ function IsometricCanvas({
15396
15414
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15397
15415
  const adjustedX = world.x - scaledTileWidth / 2;
15398
15416
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15399
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15417
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15400
15418
  const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
15401
15419
  if (tileExists) {
15402
15420
  if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
15403
15421
  onTileHover?.(isoPos.x, isoPos.y);
15404
15422
  }
15405
- }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15423
+ }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15406
15424
  const handleCanvasPointerUp = React93.useCallback((e) => {
15407
15425
  singlePointerActiveRef.current = false;
15408
15426
  if (enableCamera) handlePointerUp();
@@ -15411,7 +15429,7 @@ function IsometricCanvas({
15411
15429
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15412
15430
  const adjustedX = world.x - scaledTileWidth / 2;
15413
15431
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15414
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15432
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15415
15433
  const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
15416
15434
  if (clickedUnit && (onUnitClick || unitClickEvent)) {
15417
15435
  if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
@@ -15423,7 +15441,7 @@ function IsometricCanvas({
15423
15441
  onTileClick?.(isoPos.x, isoPos.y);
15424
15442
  }
15425
15443
  }
15426
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
15444
+ }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
15427
15445
  const handleCanvasPointerLeave = React93.useCallback(() => {
15428
15446
  handleMouseLeave();
15429
15447
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -46760,6 +46778,48 @@ var init_HeroOrganism = __esm({
46760
46778
  _HeroClickInterceptor.displayName = "_HeroClickInterceptor";
46761
46779
  }
46762
46780
  });
46781
+ function HexStrategyBoard({
46782
+ tiles,
46783
+ units,
46784
+ features,
46785
+ assetManifest,
46786
+ assetBaseUrl,
46787
+ scale = 0.45,
46788
+ showMinimap = true,
46789
+ enableCamera = true,
46790
+ tileClickEvent,
46791
+ unitClickEvent,
46792
+ isLoading,
46793
+ error,
46794
+ className
46795
+ }) {
46796
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("hex-strategy-board relative w-full h-full", className), children: /* @__PURE__ */ jsxRuntime.jsx(
46797
+ IsometricCanvas_default,
46798
+ {
46799
+ tileLayout: "hex",
46800
+ tiles,
46801
+ units,
46802
+ features,
46803
+ assetManifest,
46804
+ assetBaseUrl,
46805
+ scale,
46806
+ showMinimap,
46807
+ enableCamera,
46808
+ tileClickEvent,
46809
+ unitClickEvent,
46810
+ isLoading,
46811
+ error
46812
+ }
46813
+ ) });
46814
+ }
46815
+ var init_HexStrategyBoard = __esm({
46816
+ "components/game/organisms/HexStrategyBoard.tsx"() {
46817
+ "use client";
46818
+ init_cn();
46819
+ init_IsometricCanvas();
46820
+ HexStrategyBoard.displayName = "HexStrategyBoard";
46821
+ }
46822
+ });
46763
46823
  var LandingPageTemplate;
46764
46824
  var init_LandingPageTemplate = __esm({
46765
46825
  "components/marketing/templates/LandingPageTemplate.tsx"() {
@@ -54588,6 +54648,7 @@ var init_component_registry_generated = __esm({
54588
54648
  init_HealthPanel();
54589
54649
  init_HeroOrganism();
54590
54650
  init_HeroSection();
54651
+ init_HexStrategyBoard();
54591
54652
  init_Icon();
54592
54653
  init_InfiniteScrollSentinel();
54593
54654
  init_Input();
@@ -54924,6 +54985,7 @@ var init_component_registry_generated = __esm({
54924
54985
  "HealthPanel": HealthPanel,
54925
54986
  "HeroOrganism": HeroOrganism,
54926
54987
  "HeroSection": HeroSection,
54988
+ "HexStrategyBoard": HexStrategyBoard,
54927
54989
  "Icon": Icon,
54928
54990
  "InfiniteScrollSentinel": InfiniteScrollSentinel,
54929
54991
  "Input": Input,
package/dist/avl/index.js CHANGED
@@ -14676,16 +14676,26 @@ var init_useUnitSpriteAtlas = __esm({
14676
14676
  });
14677
14677
 
14678
14678
  // components/game/organisms/utils/isometric.ts
14679
- function isoToScreen(tileX, tileY, scale, baseOffsetX) {
14679
+ function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
14680
14680
  const scaledTileWidth = TILE_WIDTH * scale;
14681
14681
  const scaledFloorHeight = FLOOR_HEIGHT * scale;
14682
+ if (layout === "hex") {
14683
+ const screenX2 = tileX * scaledTileWidth + (tileY & 1) * (scaledTileWidth / 2) + baseOffsetX;
14684
+ const screenY2 = tileY * (scaledFloorHeight * 0.75);
14685
+ return { x: screenX2, y: screenY2 };
14686
+ }
14682
14687
  const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
14683
14688
  const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
14684
14689
  return { x: screenX, y: screenY };
14685
14690
  }
14686
- function screenToIso(screenX, screenY, scale, baseOffsetX) {
14691
+ function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
14687
14692
  const scaledTileWidth = TILE_WIDTH * scale;
14688
14693
  const scaledFloorHeight = FLOOR_HEIGHT * scale;
14694
+ if (layout === "hex") {
14695
+ const row = Math.round(screenY / (scaledFloorHeight * 0.75));
14696
+ const col = Math.round((screenX - (row & 1) * (scaledTileWidth / 2) - baseOffsetX) / scaledTileWidth);
14697
+ return { x: col, y: row };
14698
+ }
14689
14699
  const adjustedX = screenX - baseOffsetX;
14690
14700
  const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
14691
14701
  const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
@@ -14735,6 +14745,7 @@ function IsometricCanvas({
14735
14745
  tileHoverEvent,
14736
14746
  tileLeaveEvent,
14737
14747
  // Rendering options
14748
+ tileLayout = "isometric",
14738
14749
  scale = 0.4,
14739
14750
  debug: debug2 = false,
14740
14751
  backgroundImage = "",
@@ -14804,13 +14815,17 @@ function IsometricCanvas({
14804
14815
  );
14805
14816
  const sortedTiles = useMemo(() => {
14806
14817
  const tiles = [...tilesProp];
14807
- tiles.sort((a, b) => {
14808
- const depthA = a.x + a.y;
14809
- const depthB = b.x + b.y;
14810
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
14811
- });
14818
+ if (tileLayout === "hex") {
14819
+ tiles.sort((a, b) => a.y !== b.y ? a.y - b.y : a.x - b.x);
14820
+ } else {
14821
+ tiles.sort((a, b) => {
14822
+ const depthA = a.x + a.y;
14823
+ const depthB = b.x + b.y;
14824
+ return depthA !== depthB ? depthA - depthB : a.y - b.y;
14825
+ });
14826
+ }
14812
14827
  return tiles;
14813
- }, [tilesProp]);
14828
+ }, [tilesProp, tileLayout]);
14814
14829
  const gridWidth = useMemo(() => {
14815
14830
  if (sortedTiles.length === 0) return 0;
14816
14831
  return Math.max(...sortedTiles.map((t2) => t2.x)) + 1;
@@ -14924,7 +14939,7 @@ function IsometricCanvas({
14924
14939
  miniCanvas.width = mW;
14925
14940
  miniCanvas.height = mH;
14926
14941
  mCtx.clearRect(0, 0, mW, mH);
14927
- const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX));
14942
+ const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX, tileLayout));
14928
14943
  const minX = Math.min(...allScreenPos.map((p2) => p2.x));
14929
14944
  const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
14930
14945
  const minY = Math.min(...allScreenPos.map((p2) => p2.y));
@@ -14935,7 +14950,7 @@ function IsometricCanvas({
14935
14950
  const offsetMx = (mW - worldW * scaleM) / 2;
14936
14951
  const offsetMy = (mH - worldH * scaleM) / 2;
14937
14952
  for (const tile of sortedTiles) {
14938
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
14953
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
14939
14954
  const mx = (pos.x - minX) * scaleM + offsetMx;
14940
14955
  const my = (pos.y - minY) * scaleM + offsetMy;
14941
14956
  const mTileW = scaledTileWidth * scaleM;
@@ -14951,7 +14966,7 @@ function IsometricCanvas({
14951
14966
  }
14952
14967
  for (const unit of units) {
14953
14968
  if (!unit.position) continue;
14954
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
14969
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
14955
14970
  const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
14956
14971
  const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
14957
14972
  mCtx.fillStyle = unit.team === "player" ? "#60a5fa" : unit.team === "enemy" ? "#f87171" : "#9ca3af";
@@ -14967,7 +14982,7 @@ function IsometricCanvas({
14967
14982
  mCtx.strokeStyle = "rgba(255, 255, 255, 0.8)";
14968
14983
  mCtx.lineWidth = 1;
14969
14984
  mCtx.strokeRect(vLeft, vTop, vW, vH);
14970
- }, [showMinimap, sortedTiles, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
14985
+ }, [showMinimap, sortedTiles, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
14971
14986
  const draw = useCallback((animTime) => {
14972
14987
  const canvas = canvasRef.current;
14973
14988
  if (!canvas) return;
@@ -15007,7 +15022,7 @@ function IsometricCanvas({
15007
15022
  const visTop = cam.y - viewportSize.height / cam.zoom;
15008
15023
  const visBottom = cam.y + viewportSize.height * 2 / cam.zoom;
15009
15024
  for (const tile of sortedTiles) {
15010
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
15025
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
15011
15026
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15012
15027
  continue;
15013
15028
  }
@@ -15087,12 +15102,13 @@ function IsometricCanvas({
15087
15102
  }
15088
15103
  }
15089
15104
  const sortedFeatures = [...features].sort((a, b) => {
15105
+ if (tileLayout === "hex") return a.y !== b.y ? a.y - b.y : a.x - b.x;
15090
15106
  const depthA = a.x + a.y;
15091
15107
  const depthB = b.x + b.y;
15092
15108
  return depthA !== depthB ? depthA - depthB : a.y - b.y;
15093
15109
  });
15094
15110
  for (const feature of sortedFeatures) {
15095
- const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX);
15111
+ const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX, tileLayout);
15096
15112
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15097
15113
  continue;
15098
15114
  }
@@ -15127,12 +15143,13 @@ function IsometricCanvas({
15127
15143
  }
15128
15144
  const unitsWithPosition = units.filter((u) => !!u.position);
15129
15145
  const sortedUnits = [...unitsWithPosition].sort((a, b) => {
15146
+ if (tileLayout === "hex") return a.position.y !== b.position.y ? a.position.y - b.position.y : a.position.x - b.position.x;
15130
15147
  const depthA = a.position.x + a.position.y;
15131
15148
  const depthB = b.position.x + b.position.y;
15132
15149
  return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
15133
15150
  });
15134
15151
  for (const unit of sortedUnits) {
15135
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15152
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15136
15153
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15137
15154
  continue;
15138
15155
  }
@@ -15152,7 +15169,7 @@ function IsometricCanvas({
15152
15169
  drawH = maxUnitW / ar;
15153
15170
  }
15154
15171
  if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
15155
- const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
15172
+ const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX, tileLayout);
15156
15173
  const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
15157
15174
  const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
15158
15175
  ctx.save();
@@ -15281,6 +15298,7 @@ function IsometricCanvas({
15281
15298
  units,
15282
15299
  features,
15283
15300
  selectedUnitId,
15301
+ tileLayout,
15284
15302
  scale,
15285
15303
  debug2,
15286
15304
  resolveTerrainSpriteUrl,
@@ -15309,14 +15327,14 @@ function IsometricCanvas({
15309
15327
  if (!selectedUnitId) return;
15310
15328
  const unit = units.find((u) => u.id === selectedUnitId);
15311
15329
  if (!unit?.position) return;
15312
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15330
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15313
15331
  const centerX = pos.x + scaledTileWidth / 2;
15314
15332
  const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
15315
15333
  targetCameraRef.current = {
15316
15334
  x: centerX - viewportSize.width / 2,
15317
15335
  y: centerY - viewportSize.height / 2
15318
15336
  };
15319
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15337
+ }, [selectedUnitId, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15320
15338
  useEffect(() => {
15321
15339
  const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
15322
15340
  draw(animTimeRef.current);
@@ -15349,13 +15367,13 @@ function IsometricCanvas({
15349
15367
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15350
15368
  const adjustedX = world.x - scaledTileWidth / 2;
15351
15369
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15352
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15370
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15353
15371
  const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
15354
15372
  if (tileExists) {
15355
15373
  if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
15356
15374
  onTileHover?.(isoPos.x, isoPos.y);
15357
15375
  }
15358
- }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15376
+ }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15359
15377
  const handleCanvasPointerUp = useCallback((e) => {
15360
15378
  singlePointerActiveRef.current = false;
15361
15379
  if (enableCamera) handlePointerUp();
@@ -15364,7 +15382,7 @@ function IsometricCanvas({
15364
15382
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15365
15383
  const adjustedX = world.x - scaledTileWidth / 2;
15366
15384
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15367
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15385
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15368
15386
  const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
15369
15387
  if (clickedUnit && (onUnitClick || unitClickEvent)) {
15370
15388
  if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
@@ -15376,7 +15394,7 @@ function IsometricCanvas({
15376
15394
  onTileClick?.(isoPos.x, isoPos.y);
15377
15395
  }
15378
15396
  }
15379
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
15397
+ }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
15380
15398
  const handleCanvasPointerLeave = useCallback(() => {
15381
15399
  handleMouseLeave();
15382
15400
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -46713,6 +46731,48 @@ var init_HeroOrganism = __esm({
46713
46731
  _HeroClickInterceptor.displayName = "_HeroClickInterceptor";
46714
46732
  }
46715
46733
  });
46734
+ function HexStrategyBoard({
46735
+ tiles,
46736
+ units,
46737
+ features,
46738
+ assetManifest,
46739
+ assetBaseUrl,
46740
+ scale = 0.45,
46741
+ showMinimap = true,
46742
+ enableCamera = true,
46743
+ tileClickEvent,
46744
+ unitClickEvent,
46745
+ isLoading,
46746
+ error,
46747
+ className
46748
+ }) {
46749
+ return /* @__PURE__ */ jsx("div", { className: cn("hex-strategy-board relative w-full h-full", className), children: /* @__PURE__ */ jsx(
46750
+ IsometricCanvas_default,
46751
+ {
46752
+ tileLayout: "hex",
46753
+ tiles,
46754
+ units,
46755
+ features,
46756
+ assetManifest,
46757
+ assetBaseUrl,
46758
+ scale,
46759
+ showMinimap,
46760
+ enableCamera,
46761
+ tileClickEvent,
46762
+ unitClickEvent,
46763
+ isLoading,
46764
+ error
46765
+ }
46766
+ ) });
46767
+ }
46768
+ var init_HexStrategyBoard = __esm({
46769
+ "components/game/organisms/HexStrategyBoard.tsx"() {
46770
+ "use client";
46771
+ init_cn();
46772
+ init_IsometricCanvas();
46773
+ HexStrategyBoard.displayName = "HexStrategyBoard";
46774
+ }
46775
+ });
46716
46776
  var LandingPageTemplate;
46717
46777
  var init_LandingPageTemplate = __esm({
46718
46778
  "components/marketing/templates/LandingPageTemplate.tsx"() {
@@ -54541,6 +54601,7 @@ var init_component_registry_generated = __esm({
54541
54601
  init_HealthPanel();
54542
54602
  init_HeroOrganism();
54543
54603
  init_HeroSection();
54604
+ init_HexStrategyBoard();
54544
54605
  init_Icon();
54545
54606
  init_InfiniteScrollSentinel();
54546
54607
  init_Input();
@@ -54877,6 +54938,7 @@ var init_component_registry_generated = __esm({
54877
54938
  "HealthPanel": HealthPanel,
54878
54939
  "HeroOrganism": HeroOrganism,
54879
54940
  "HeroSection": HeroSection,
54941
+ "HexStrategyBoard": HexStrategyBoard,
54880
54942
  "Icon": Icon,
54881
54943
  "InfiniteScrollSentinel": InfiniteScrollSentinel,
54882
54944
  "Input": Input,
@@ -27,6 +27,7 @@ import * as React from 'react';
27
27
  import type { AssetUrl, EventEmit } from '@almadar/core';
28
28
  import type { IsometricTile, IsometricUnit, IsometricFeature } from '../organisms/types/isometric';
29
29
  import type { ResolvedFrame } from '../organisms/types/spriteAnimation';
30
+ import type { TileLayout } from '../organisms/utils/isometric';
30
31
  import type { UiError } from '../../core/atoms/types';
31
32
  /** Event Contract:
32
33
  * Emits: UI:TILE_CLICK
@@ -79,6 +80,8 @@ export interface IsometricCanvasProps {
79
80
  }>;
80
81
  /** Declarative event: emits UI:{tileLeaveEvent} with {} on tile leave */
81
82
  tileLeaveEvent?: EventEmit<Record<string, never>>;
83
+ /** Tile layout projection: 'isometric' (default 2:1 diamond) or 'hex' (pointy-top offset rows) */
84
+ tileLayout?: TileLayout;
82
85
  /** Render scale (0.4 = 40% zoom) */
83
86
  scale?: number;
84
87
  /** Show debug grid lines and coordinates */
@@ -129,7 +132,7 @@ export interface IsometricCanvasProps {
129
132
  effects?: Record<string, string>;
130
133
  };
131
134
  }
132
- export declare function IsometricCanvas({ className, isLoading, error, tiles: _tilesPropRaw, units: _unitsPropRaw, features: _featuresPropRaw, selectedUnitId, validMoves, attackTargets, hoveredTile, onTileClick, onUnitClick, onTileHover, onTileLeave, tileClickEvent, unitClickEvent, tileHoverEvent, tileLeaveEvent, scale, debug, backgroundImage, showMinimap, enableCamera, unitScale, spriteHeightRatio, spriteMaxWidthRatio, getTerrainSprite, getFeatureSprite, getUnitSprite, resolveUnitFrame, effectSpriteUrls, onDrawEffects, hasActiveEffects, diamondTopY: diamondTopYProp, assetBaseUrl, assetManifest, }: IsometricCanvasProps): React.JSX.Element;
135
+ export declare function IsometricCanvas({ className, isLoading, error, tiles: _tilesPropRaw, units: _unitsPropRaw, features: _featuresPropRaw, selectedUnitId, validMoves, attackTargets, hoveredTile, onTileClick, onUnitClick, onTileHover, onTileLeave, tileClickEvent, unitClickEvent, tileHoverEvent, tileLeaveEvent, tileLayout, scale, debug, backgroundImage, showMinimap, enableCamera, unitScale, spriteHeightRatio, spriteMaxWidthRatio, getTerrainSprite, getFeatureSprite, getUnitSprite, resolveUnitFrame, effectSpriteUrls, onDrawEffects, hasActiveEffects, diamondTopY: diamondTopYProp, assetBaseUrl, assetManifest, }: IsometricCanvasProps): React.JSX.Element;
133
136
  export declare namespace IsometricCanvas {
134
137
  var displayName: string;
135
138
  }
@@ -28,4 +28,5 @@ export { GameMenu, type GameMenuProps, type MenuOption } from './GameMenu';
28
28
  export { GameOverScreen, type GameOverScreenProps, type GameOverStat, type GameOverAction } from './GameOverScreen';
29
29
  export { PlatformerCanvas, type PlatformerCanvasProps, type PlatformerPlatform, type PlatformerPlayer } from './PlatformerCanvas';
30
30
  export { IsometricCanvas, type IsometricCanvasProps } from './IsometricCanvas';
31
+ export type { TileLayout } from '../organisms/utils/isometric';
31
32
  export type { IsometricTile, IsometricUnit, IsometricFeature } from '../organisms/types/isometric';
@@ -0,0 +1,37 @@
1
+ import React from 'react';
2
+ import type { AssetUrl, EventEmit } from '@almadar/core';
3
+ import type { IsometricTile, IsometricUnit, IsometricFeature } from './types/isometric';
4
+ import type { DisplayStateProps } from '../../core/organisms/types';
5
+ import type { IsometricCanvasProps } from '../molecules/IsometricCanvas';
6
+ export interface HexStrategyBoardProps extends DisplayStateProps {
7
+ /** Hex grid tiles */
8
+ tiles?: IsometricTile[];
9
+ /** Units on the board */
10
+ units?: IsometricUnit[];
11
+ /** Features (resources, structures, etc.) on the board */
12
+ features?: IsometricFeature[];
13
+ /** Asset sprite manifest (same shape as IsometricCanvas.assetManifest) */
14
+ assetManifest?: IsometricCanvasProps['assetManifest'];
15
+ /** Base URL prepended to manifest sprite paths */
16
+ assetBaseUrl?: AssetUrl;
17
+ /** Render scale */
18
+ scale?: number;
19
+ /** Show minimap overlay */
20
+ showMinimap?: boolean;
21
+ /** Enable camera pan/zoom controls */
22
+ enableCamera?: boolean;
23
+ /** Declarative event: emits UI:{tileClickEvent} with { x, y } on tile click */
24
+ tileClickEvent?: EventEmit<{
25
+ x: number;
26
+ y: number;
27
+ }>;
28
+ /** Declarative event: emits UI:{unitClickEvent} with { unitId } on unit click */
29
+ unitClickEvent?: EventEmit<{
30
+ unitId: string;
31
+ }>;
32
+ }
33
+ export declare function HexStrategyBoard({ tiles, units, features, assetManifest, assetBaseUrl, scale, showMinimap, enableCamera, tileClickEvent, unitClickEvent, isLoading, error, className, }: HexStrategyBoardProps): React.ReactElement;
34
+ export declare namespace HexStrategyBoard {
35
+ var displayName: string;
36
+ }
37
+ export default HexStrategyBoard;
@@ -18,6 +18,7 @@ export { useSpriteAnimations, type UseSpriteAnimationsResult, type UseSpriteAnim
18
18
  export { usePhysics2D, type UsePhysics2DOptions, type UsePhysics2DReturn, } from './hooks/usePhysics2D';
19
19
  export { PhysicsManager, type Physics2DState, type PhysicsBounds, type PhysicsConfig, } from './managers/PhysicsManager';
20
20
  export { isoToScreen, screenToIso, TILE_WIDTH, TILE_HEIGHT, FLOOR_HEIGHT, DIAMOND_TOP_Y, FEATURE_COLORS, } from './utils/isometric';
21
+ export type { TileLayout } from './utils/isometric';
21
22
  export { inferDirection, resolveSheetDirection, getCurrentFrame, resolveFrame, createUnitAnimationState, transitionAnimation, tickAnimationState, } from './utils/spriteAnimation';
22
23
  export { SPRITE_SHEET_LAYOUT, SHEET_COLUMNS } from './utils/spriteSheetConstants';
23
24
  export { BattleBoard, type BattleBoardProps, type BattlePhase, type BattleSlotContext, } from './BattleBoard';
@@ -33,6 +34,7 @@ export { CityBuilderBoard, type CityBuilderBoardProps, } from './CityBuilderBoar
33
34
  export { GameBoard3D, type GameBoard3DProps, } from './GameBoard3D';
34
35
  export { VisualNovelBoard, type VisualNovelBoardProps, type VisualNovelNode, type VisualNovelChoice, } from './VisualNovelBoard';
35
36
  export { CardBattlerBoard, type CardBattlerBoardProps, type CardBattlerCard, } from './CardBattlerBoard';
37
+ export { HexStrategyBoard, type HexStrategyBoardProps, } from './HexStrategyBoard';
36
38
  export { TraitStateViewer, type TraitStateViewerProps, type TraitStateMachineDefinition, type TraitTransition, } from './TraitStateViewer';
37
39
  export { TraitSlot, type TraitSlotProps, type SlotItemData, } from './TraitSlot';
38
40
  export { CollapsibleSection, EditorSlider, EditorSelect, EditorCheckbox, EditorTextInput, StatusBar, TerrainPalette, EditorToolbar, TERRAIN_COLORS, FEATURE_TYPES, type EditorMode, type CollapsibleSectionProps, type EditorSliderProps, type EditorSelectProps, type EditorCheckboxProps, type EditorTextInputProps, type StatusBarProps, type TerrainPaletteProps, type EditorToolbarProps, } from './editor';