@almadar/ui 5.63.2 → 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.
@@ -43,6 +43,7 @@ var THREE3 = require('three');
43
43
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
44
44
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
45
45
  var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
46
+ var SkeletonUtils = require('three/examples/jsm/utils/SkeletonUtils');
46
47
  var fiber = require('@react-three/fiber');
47
48
  var drei = require('@react-three/drei');
48
49
  var patterns = require('@almadar/patterns');
@@ -14722,16 +14723,26 @@ var init_useUnitSpriteAtlas = __esm({
14722
14723
  });
14723
14724
 
14724
14725
  // components/game/organisms/utils/isometric.ts
14725
- function isoToScreen(tileX, tileY, scale, baseOffsetX) {
14726
+ function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
14726
14727
  const scaledTileWidth = TILE_WIDTH * scale;
14727
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
+ }
14728
14734
  const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
14729
14735
  const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
14730
14736
  return { x: screenX, y: screenY };
14731
14737
  }
14732
- function screenToIso(screenX, screenY, scale, baseOffsetX) {
14738
+ function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
14733
14739
  const scaledTileWidth = TILE_WIDTH * scale;
14734
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
+ }
14735
14746
  const adjustedX = screenX - baseOffsetX;
14736
14747
  const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
14737
14748
  const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
@@ -14781,6 +14792,7 @@ function IsometricCanvas({
14781
14792
  tileHoverEvent,
14782
14793
  tileLeaveEvent,
14783
14794
  // Rendering options
14795
+ tileLayout = "isometric",
14784
14796
  scale = 0.4,
14785
14797
  debug: debug2 = false,
14786
14798
  backgroundImage = "",
@@ -14850,13 +14862,17 @@ function IsometricCanvas({
14850
14862
  );
14851
14863
  const sortedTiles = React93.useMemo(() => {
14852
14864
  const tiles = [...tilesProp];
14853
- tiles.sort((a, b) => {
14854
- const depthA = a.x + a.y;
14855
- const depthB = b.x + b.y;
14856
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
14857
- });
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
+ }
14858
14874
  return tiles;
14859
- }, [tilesProp]);
14875
+ }, [tilesProp, tileLayout]);
14860
14876
  const gridWidth = React93.useMemo(() => {
14861
14877
  if (sortedTiles.length === 0) return 0;
14862
14878
  return Math.max(...sortedTiles.map((t2) => t2.x)) + 1;
@@ -14970,7 +14986,7 @@ function IsometricCanvas({
14970
14986
  miniCanvas.width = mW;
14971
14987
  miniCanvas.height = mH;
14972
14988
  mCtx.clearRect(0, 0, mW, mH);
14973
- 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));
14974
14990
  const minX = Math.min(...allScreenPos.map((p2) => p2.x));
14975
14991
  const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
14976
14992
  const minY = Math.min(...allScreenPos.map((p2) => p2.y));
@@ -14981,7 +14997,7 @@ function IsometricCanvas({
14981
14997
  const offsetMx = (mW - worldW * scaleM) / 2;
14982
14998
  const offsetMy = (mH - worldH * scaleM) / 2;
14983
14999
  for (const tile of sortedTiles) {
14984
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
15000
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
14985
15001
  const mx = (pos.x - minX) * scaleM + offsetMx;
14986
15002
  const my = (pos.y - minY) * scaleM + offsetMy;
14987
15003
  const mTileW = scaledTileWidth * scaleM;
@@ -14997,7 +15013,7 @@ function IsometricCanvas({
14997
15013
  }
14998
15014
  for (const unit of units) {
14999
15015
  if (!unit.position) continue;
15000
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15016
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15001
15017
  const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
15002
15018
  const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
15003
15019
  mCtx.fillStyle = unit.team === "player" ? "#60a5fa" : unit.team === "enemy" ? "#f87171" : "#9ca3af";
@@ -15013,7 +15029,7 @@ function IsometricCanvas({
15013
15029
  mCtx.strokeStyle = "rgba(255, 255, 255, 0.8)";
15014
15030
  mCtx.lineWidth = 1;
15015
15031
  mCtx.strokeRect(vLeft, vTop, vW, vH);
15016
- }, [showMinimap, sortedTiles, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
15032
+ }, [showMinimap, sortedTiles, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
15017
15033
  const draw = React93.useCallback((animTime) => {
15018
15034
  const canvas = canvasRef.current;
15019
15035
  if (!canvas) return;
@@ -15053,7 +15069,7 @@ function IsometricCanvas({
15053
15069
  const visTop = cam.y - viewportSize.height / cam.zoom;
15054
15070
  const visBottom = cam.y + viewportSize.height * 2 / cam.zoom;
15055
15071
  for (const tile of sortedTiles) {
15056
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
15072
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
15057
15073
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15058
15074
  continue;
15059
15075
  }
@@ -15133,12 +15149,13 @@ function IsometricCanvas({
15133
15149
  }
15134
15150
  }
15135
15151
  const sortedFeatures = [...features].sort((a, b) => {
15152
+ if (tileLayout === "hex") return a.y !== b.y ? a.y - b.y : a.x - b.x;
15136
15153
  const depthA = a.x + a.y;
15137
15154
  const depthB = b.x + b.y;
15138
15155
  return depthA !== depthB ? depthA - depthB : a.y - b.y;
15139
15156
  });
15140
15157
  for (const feature of sortedFeatures) {
15141
- const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX);
15158
+ const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX, tileLayout);
15142
15159
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15143
15160
  continue;
15144
15161
  }
@@ -15173,12 +15190,13 @@ function IsometricCanvas({
15173
15190
  }
15174
15191
  const unitsWithPosition = units.filter((u) => !!u.position);
15175
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;
15176
15194
  const depthA = a.position.x + a.position.y;
15177
15195
  const depthB = b.position.x + b.position.y;
15178
15196
  return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
15179
15197
  });
15180
15198
  for (const unit of sortedUnits) {
15181
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15199
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15182
15200
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15183
15201
  continue;
15184
15202
  }
@@ -15198,7 +15216,7 @@ function IsometricCanvas({
15198
15216
  drawH = maxUnitW / ar;
15199
15217
  }
15200
15218
  if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
15201
- const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
15219
+ const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX, tileLayout);
15202
15220
  const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
15203
15221
  const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
15204
15222
  ctx.save();
@@ -15327,6 +15345,7 @@ function IsometricCanvas({
15327
15345
  units,
15328
15346
  features,
15329
15347
  selectedUnitId,
15348
+ tileLayout,
15330
15349
  scale,
15331
15350
  debug2,
15332
15351
  resolveTerrainSpriteUrl,
@@ -15355,14 +15374,14 @@ function IsometricCanvas({
15355
15374
  if (!selectedUnitId) return;
15356
15375
  const unit = units.find((u) => u.id === selectedUnitId);
15357
15376
  if (!unit?.position) return;
15358
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15377
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15359
15378
  const centerX = pos.x + scaledTileWidth / 2;
15360
15379
  const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
15361
15380
  targetCameraRef.current = {
15362
15381
  x: centerX - viewportSize.width / 2,
15363
15382
  y: centerY - viewportSize.height / 2
15364
15383
  };
15365
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15384
+ }, [selectedUnitId, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15366
15385
  React93.useEffect(() => {
15367
15386
  const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
15368
15387
  draw(animTimeRef.current);
@@ -15395,13 +15414,13 @@ function IsometricCanvas({
15395
15414
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15396
15415
  const adjustedX = world.x - scaledTileWidth / 2;
15397
15416
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15398
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15417
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15399
15418
  const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
15400
15419
  if (tileExists) {
15401
15420
  if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
15402
15421
  onTileHover?.(isoPos.x, isoPos.y);
15403
15422
  }
15404
- }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15423
+ }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15405
15424
  const handleCanvasPointerUp = React93.useCallback((e) => {
15406
15425
  singlePointerActiveRef.current = false;
15407
15426
  if (enableCamera) handlePointerUp();
@@ -15410,7 +15429,7 @@ function IsometricCanvas({
15410
15429
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15411
15430
  const adjustedX = world.x - scaledTileWidth / 2;
15412
15431
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15413
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15432
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15414
15433
  const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
15415
15434
  if (clickedUnit && (onUnitClick || unitClickEvent)) {
15416
15435
  if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
@@ -15422,7 +15441,7 @@ function IsometricCanvas({
15422
15441
  onTileClick?.(isoPos.x, isoPos.y);
15423
15442
  }
15424
15443
  }
15425
- }, [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]);
15426
15445
  const handleCanvasPointerLeave = React93.useCallback(() => {
15427
15446
  handleMouseLeave();
15428
15447
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -45460,7 +45479,8 @@ function ModelLoader({
45460
45479
  const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
45461
45480
  const model = React93.useMemo(() => {
45462
45481
  if (!loadedModel) return null;
45463
- const cloned = loadedModel.clone();
45482
+ const cloned = SkeletonUtils.clone(loadedModel);
45483
+ cloned.updateMatrixWorld(true);
45464
45484
  cloned.traverse((child) => {
45465
45485
  if (child instanceof THREE3__namespace.Mesh) {
45466
45486
  child.castShadow = castShadow;
@@ -45475,7 +45495,8 @@ function ModelLoader({
45475
45495
  const size = new THREE3__namespace.Vector3();
45476
45496
  box.getSize(size);
45477
45497
  const maxDim = Math.max(size.x, size.y, size.z);
45478
- return maxDim > 0 && Number.isFinite(maxDim) ? 1 / maxDim : 1;
45498
+ if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
45499
+ return 1 / maxDim;
45479
45500
  }, [model]);
45480
45501
  const scaleArray = React93.useMemo(() => {
45481
45502
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;
@@ -46757,6 +46778,48 @@ var init_HeroOrganism = __esm({
46757
46778
  _HeroClickInterceptor.displayName = "_HeroClickInterceptor";
46758
46779
  }
46759
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
+ });
46760
46823
  var LandingPageTemplate;
46761
46824
  var init_LandingPageTemplate = __esm({
46762
46825
  "components/marketing/templates/LandingPageTemplate.tsx"() {
@@ -54585,6 +54648,7 @@ var init_component_registry_generated = __esm({
54585
54648
  init_HealthPanel();
54586
54649
  init_HeroOrganism();
54587
54650
  init_HeroSection();
54651
+ init_HexStrategyBoard();
54588
54652
  init_Icon();
54589
54653
  init_InfiniteScrollSentinel();
54590
54654
  init_Input();
@@ -54921,6 +54985,7 @@ var init_component_registry_generated = __esm({
54921
54985
  "HealthPanel": HealthPanel,
54922
54986
  "HeroOrganism": HeroOrganism,
54923
54987
  "HeroSection": HeroSection,
54988
+ "HexStrategyBoard": HexStrategyBoard,
54924
54989
  "Icon": Icon,
54925
54990
  "InfiniteScrollSentinel": InfiniteScrollSentinel,
54926
54991
  "Input": Input,
package/dist/avl/index.js CHANGED
@@ -43,6 +43,7 @@ import * as THREE3 from 'three';
43
43
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
44
44
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
45
45
  import { GLTFLoader as GLTFLoader$1 } from 'three/examples/jsm/loaders/GLTFLoader';
46
+ import { clone } from 'three/examples/jsm/utils/SkeletonUtils';
46
47
  import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber';
47
48
  import { Billboard, Grid as Grid$1, OrbitControls } from '@react-three/drei';
48
49
  import { getPatternDefinition, getComponentForPattern as getComponentForPattern$1, isEntityAwarePattern } from '@almadar/patterns';
@@ -14675,16 +14676,26 @@ var init_useUnitSpriteAtlas = __esm({
14675
14676
  });
14676
14677
 
14677
14678
  // components/game/organisms/utils/isometric.ts
14678
- function isoToScreen(tileX, tileY, scale, baseOffsetX) {
14679
+ function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
14679
14680
  const scaledTileWidth = TILE_WIDTH * scale;
14680
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
+ }
14681
14687
  const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
14682
14688
  const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
14683
14689
  return { x: screenX, y: screenY };
14684
14690
  }
14685
- function screenToIso(screenX, screenY, scale, baseOffsetX) {
14691
+ function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
14686
14692
  const scaledTileWidth = TILE_WIDTH * scale;
14687
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
+ }
14688
14699
  const adjustedX = screenX - baseOffsetX;
14689
14700
  const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
14690
14701
  const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
@@ -14734,6 +14745,7 @@ function IsometricCanvas({
14734
14745
  tileHoverEvent,
14735
14746
  tileLeaveEvent,
14736
14747
  // Rendering options
14748
+ tileLayout = "isometric",
14737
14749
  scale = 0.4,
14738
14750
  debug: debug2 = false,
14739
14751
  backgroundImage = "",
@@ -14803,13 +14815,17 @@ function IsometricCanvas({
14803
14815
  );
14804
14816
  const sortedTiles = useMemo(() => {
14805
14817
  const tiles = [...tilesProp];
14806
- tiles.sort((a, b) => {
14807
- const depthA = a.x + a.y;
14808
- const depthB = b.x + b.y;
14809
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
14810
- });
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
+ }
14811
14827
  return tiles;
14812
- }, [tilesProp]);
14828
+ }, [tilesProp, tileLayout]);
14813
14829
  const gridWidth = useMemo(() => {
14814
14830
  if (sortedTiles.length === 0) return 0;
14815
14831
  return Math.max(...sortedTiles.map((t2) => t2.x)) + 1;
@@ -14923,7 +14939,7 @@ function IsometricCanvas({
14923
14939
  miniCanvas.width = mW;
14924
14940
  miniCanvas.height = mH;
14925
14941
  mCtx.clearRect(0, 0, mW, mH);
14926
- 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));
14927
14943
  const minX = Math.min(...allScreenPos.map((p2) => p2.x));
14928
14944
  const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
14929
14945
  const minY = Math.min(...allScreenPos.map((p2) => p2.y));
@@ -14934,7 +14950,7 @@ function IsometricCanvas({
14934
14950
  const offsetMx = (mW - worldW * scaleM) / 2;
14935
14951
  const offsetMy = (mH - worldH * scaleM) / 2;
14936
14952
  for (const tile of sortedTiles) {
14937
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
14953
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
14938
14954
  const mx = (pos.x - minX) * scaleM + offsetMx;
14939
14955
  const my = (pos.y - minY) * scaleM + offsetMy;
14940
14956
  const mTileW = scaledTileWidth * scaleM;
@@ -14950,7 +14966,7 @@ function IsometricCanvas({
14950
14966
  }
14951
14967
  for (const unit of units) {
14952
14968
  if (!unit.position) continue;
14953
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
14969
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
14954
14970
  const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
14955
14971
  const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
14956
14972
  mCtx.fillStyle = unit.team === "player" ? "#60a5fa" : unit.team === "enemy" ? "#f87171" : "#9ca3af";
@@ -14966,7 +14982,7 @@ function IsometricCanvas({
14966
14982
  mCtx.strokeStyle = "rgba(255, 255, 255, 0.8)";
14967
14983
  mCtx.lineWidth = 1;
14968
14984
  mCtx.strokeRect(vLeft, vTop, vW, vH);
14969
- }, [showMinimap, sortedTiles, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
14985
+ }, [showMinimap, sortedTiles, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
14970
14986
  const draw = useCallback((animTime) => {
14971
14987
  const canvas = canvasRef.current;
14972
14988
  if (!canvas) return;
@@ -15006,7 +15022,7 @@ function IsometricCanvas({
15006
15022
  const visTop = cam.y - viewportSize.height / cam.zoom;
15007
15023
  const visBottom = cam.y + viewportSize.height * 2 / cam.zoom;
15008
15024
  for (const tile of sortedTiles) {
15009
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
15025
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
15010
15026
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15011
15027
  continue;
15012
15028
  }
@@ -15086,12 +15102,13 @@ function IsometricCanvas({
15086
15102
  }
15087
15103
  }
15088
15104
  const sortedFeatures = [...features].sort((a, b) => {
15105
+ if (tileLayout === "hex") return a.y !== b.y ? a.y - b.y : a.x - b.x;
15089
15106
  const depthA = a.x + a.y;
15090
15107
  const depthB = b.x + b.y;
15091
15108
  return depthA !== depthB ? depthA - depthB : a.y - b.y;
15092
15109
  });
15093
15110
  for (const feature of sortedFeatures) {
15094
- const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX);
15111
+ const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX, tileLayout);
15095
15112
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15096
15113
  continue;
15097
15114
  }
@@ -15126,12 +15143,13 @@ function IsometricCanvas({
15126
15143
  }
15127
15144
  const unitsWithPosition = units.filter((u) => !!u.position);
15128
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;
15129
15147
  const depthA = a.position.x + a.position.y;
15130
15148
  const depthB = b.position.x + b.position.y;
15131
15149
  return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
15132
15150
  });
15133
15151
  for (const unit of sortedUnits) {
15134
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15152
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15135
15153
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
15136
15154
  continue;
15137
15155
  }
@@ -15151,7 +15169,7 @@ function IsometricCanvas({
15151
15169
  drawH = maxUnitW / ar;
15152
15170
  }
15153
15171
  if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
15154
- const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
15172
+ const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX, tileLayout);
15155
15173
  const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
15156
15174
  const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
15157
15175
  ctx.save();
@@ -15280,6 +15298,7 @@ function IsometricCanvas({
15280
15298
  units,
15281
15299
  features,
15282
15300
  selectedUnitId,
15301
+ tileLayout,
15283
15302
  scale,
15284
15303
  debug2,
15285
15304
  resolveTerrainSpriteUrl,
@@ -15308,14 +15327,14 @@ function IsometricCanvas({
15308
15327
  if (!selectedUnitId) return;
15309
15328
  const unit = units.find((u) => u.id === selectedUnitId);
15310
15329
  if (!unit?.position) return;
15311
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
15330
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
15312
15331
  const centerX = pos.x + scaledTileWidth / 2;
15313
15332
  const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
15314
15333
  targetCameraRef.current = {
15315
15334
  x: centerX - viewportSize.width / 2,
15316
15335
  y: centerY - viewportSize.height / 2
15317
15336
  };
15318
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15337
+ }, [selectedUnitId, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
15319
15338
  useEffect(() => {
15320
15339
  const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
15321
15340
  draw(animTimeRef.current);
@@ -15348,13 +15367,13 @@ function IsometricCanvas({
15348
15367
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15349
15368
  const adjustedX = world.x - scaledTileWidth / 2;
15350
15369
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15351
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15370
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15352
15371
  const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
15353
15372
  if (tileExists) {
15354
15373
  if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
15355
15374
  onTileHover?.(isoPos.x, isoPos.y);
15356
15375
  }
15357
- }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15376
+ }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
15358
15377
  const handleCanvasPointerUp = useCallback((e) => {
15359
15378
  singlePointerActiveRef.current = false;
15360
15379
  if (enableCamera) handlePointerUp();
@@ -15363,7 +15382,7 @@ function IsometricCanvas({
15363
15382
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
15364
15383
  const adjustedX = world.x - scaledTileWidth / 2;
15365
15384
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
15366
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
15385
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
15367
15386
  const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
15368
15387
  if (clickedUnit && (onUnitClick || unitClickEvent)) {
15369
15388
  if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
@@ -15375,7 +15394,7 @@ function IsometricCanvas({
15375
15394
  onTileClick?.(isoPos.x, isoPos.y);
15376
15395
  }
15377
15396
  }
15378
- }, [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]);
15379
15398
  const handleCanvasPointerLeave = useCallback(() => {
15380
15399
  handleMouseLeave();
15381
15400
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -45413,7 +45432,8 @@ function ModelLoader({
45413
45432
  const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
45414
45433
  const model = useMemo(() => {
45415
45434
  if (!loadedModel) return null;
45416
- const cloned = loadedModel.clone();
45435
+ const cloned = clone(loadedModel);
45436
+ cloned.updateMatrixWorld(true);
45417
45437
  cloned.traverse((child) => {
45418
45438
  if (child instanceof THREE3.Mesh) {
45419
45439
  child.castShadow = castShadow;
@@ -45428,7 +45448,8 @@ function ModelLoader({
45428
45448
  const size = new THREE3.Vector3();
45429
45449
  box.getSize(size);
45430
45450
  const maxDim = Math.max(size.x, size.y, size.z);
45431
- return maxDim > 0 && Number.isFinite(maxDim) ? 1 / maxDim : 1;
45451
+ if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
45452
+ return 1 / maxDim;
45432
45453
  }, [model]);
45433
45454
  const scaleArray = useMemo(() => {
45434
45455
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;
@@ -46710,6 +46731,48 @@ var init_HeroOrganism = __esm({
46710
46731
  _HeroClickInterceptor.displayName = "_HeroClickInterceptor";
46711
46732
  }
46712
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
+ });
46713
46776
  var LandingPageTemplate;
46714
46777
  var init_LandingPageTemplate = __esm({
46715
46778
  "components/marketing/templates/LandingPageTemplate.tsx"() {
@@ -54538,6 +54601,7 @@ var init_component_registry_generated = __esm({
54538
54601
  init_HealthPanel();
54539
54602
  init_HeroOrganism();
54540
54603
  init_HeroSection();
54604
+ init_HexStrategyBoard();
54541
54605
  init_Icon();
54542
54606
  init_InfiniteScrollSentinel();
54543
54607
  init_Input();
@@ -54874,6 +54938,7 @@ var init_component_registry_generated = __esm({
54874
54938
  "HealthPanel": HealthPanel,
54875
54939
  "HeroOrganism": HeroOrganism,
54876
54940
  "HeroSection": HeroSection,
54941
+ "HexStrategyBoard": HexStrategyBoard,
54877
54942
  "Icon": Icon,
54878
54943
  "InfiniteScrollSentinel": InfiniteScrollSentinel,
54879
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';
@@ -7,6 +7,7 @@ var jsxRuntime = require('react/jsx-runtime');
7
7
  var drei = require('@react-three/drei');
8
8
  var logger = require('@almadar/logger');
9
9
  var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
10
+ var SkeletonUtils = require('three/examples/jsm/utils/SkeletonUtils');
10
11
  var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
11
12
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
12
13
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
@@ -392,7 +393,8 @@ function ModelLoader({
392
393
  const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
393
394
  const model = React11.useMemo(() => {
394
395
  if (!loadedModel) return null;
395
- const cloned = loadedModel.clone();
396
+ const cloned = SkeletonUtils.clone(loadedModel);
397
+ cloned.updateMatrixWorld(true);
396
398
  cloned.traverse((child) => {
397
399
  if (child instanceof THREE6__namespace.Mesh) {
398
400
  child.castShadow = castShadow;
@@ -407,7 +409,8 @@ function ModelLoader({
407
409
  const size = new THREE6__namespace.Vector3();
408
410
  box.getSize(size);
409
411
  const maxDim = Math.max(size.x, size.y, size.z);
410
- return maxDim > 0 && Number.isFinite(maxDim) ? 1 / maxDim : 1;
412
+ if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
413
+ return 1 / maxDim;
411
414
  }, [model]);
412
415
  const scaleArray = React11.useMemo(() => {
413
416
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;