@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.
@@ -6,6 +6,7 @@ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
6
6
  import { OrbitControls, Billboard, Grid, Stars, Sparkles, Html, RoundedBox } from '@react-three/drei';
7
7
  import { createLogger } from '@almadar/logger';
8
8
  import { GLTFLoader as GLTFLoader$1 } from 'three/examples/jsm/loaders/GLTFLoader';
9
+ import { clone } from 'three/examples/jsm/utils/SkeletonUtils';
9
10
  import { OrbitControls as OrbitControls$1 } from 'three/examples/jsm/controls/OrbitControls.js';
10
11
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
11
12
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
@@ -368,7 +369,8 @@ function ModelLoader({
368
369
  const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
369
370
  const model = useMemo(() => {
370
371
  if (!loadedModel) return null;
371
- const cloned = loadedModel.clone();
372
+ const cloned = clone(loadedModel);
373
+ cloned.updateMatrixWorld(true);
372
374
  cloned.traverse((child) => {
373
375
  if (child instanceof THREE6.Mesh) {
374
376
  child.castShadow = castShadow;
@@ -383,7 +385,8 @@ function ModelLoader({
383
385
  const size = new THREE6.Vector3();
384
386
  box.getSize(size);
385
387
  const maxDim = Math.max(size.x, size.y, size.z);
386
- return maxDim > 0 && Number.isFinite(maxDim) ? 1 / maxDim : 1;
388
+ if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
389
+ return 1 / maxDim;
387
390
  }, [model]);
388
391
  const scaleArray = useMemo(() => {
389
392
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;
@@ -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';
@@ -27,20 +27,32 @@ export declare const DIAMOND_TOP_Y = 374;
27
27
  * Feature type → fallback color mapping (when sprites not loaded).
28
28
  */
29
29
  export declare const FEATURE_COLORS: Record<string, string>;
30
+ /**
31
+ * Tile layout mode. 'isometric' = 2:1 diamond projection (default).
32
+ * 'hex' = pointy-top offset-row hex grid.
33
+ *
34
+ * Hex constants (at scale=1):
35
+ * w = TILE_WIDTH (256) — horizontal cell width
36
+ * h = FLOOR_HEIGHT (128) — vertical row stride (= w/2; hex row-to-row = h*0.75 = 96)
37
+ * Forward: screenX = col*w + (row&1)*(w/2) + baseOffsetX
38
+ * screenY = row * (h * 0.75)
39
+ * Inverse: row = round(screenY / (h*0.75)); col = round((screenX - (row&1)*(w/2) - baseOffsetX) / w)
40
+ */
41
+ export type TileLayout = 'isometric' | 'hex';
30
42
  /**
31
43
  * Convert tile grid coordinates to screen pixel coordinates.
32
44
  *
33
- * Uses 2:1 diamond isometric projection:
34
- * - X increases to the lower-right
35
- * - Y increases to the lower-left
45
+ * For 'isometric' (default): 2:1 diamond projection.
46
+ * For 'hex': pointy-top offset-row projection (odd rows shifted right by w/2).
36
47
  *
37
- * @param tileX - Grid X coordinate
38
- * @param tileY - Grid Y coordinate
48
+ * @param tileX - Grid X (col) coordinate
49
+ * @param tileY - Grid Y (row) coordinate
39
50
  * @param scale - Render scale factor
40
51
  * @param baseOffsetX - Horizontal offset to center the grid
52
+ * @param layout - Tile layout mode (default: 'isometric')
41
53
  * @returns Screen position { x, y } of the tile's top-left corner
42
54
  */
43
- export declare function isoToScreen(tileX: number, tileY: number, scale: number, baseOffsetX: number): {
55
+ export declare function isoToScreen(tileX: number, tileY: number, scale: number, baseOffsetX: number, layout?: TileLayout): {
44
56
  x: number;
45
57
  y: number;
46
58
  };
@@ -53,9 +65,10 @@ export declare function isoToScreen(tileX: number, tileY: number, scale: number,
53
65
  * @param screenY - Screen Y in pixels
54
66
  * @param scale - Render scale factor
55
67
  * @param baseOffsetX - Horizontal offset used in isoToScreen
68
+ * @param layout - Tile layout mode (default: 'isometric')
56
69
  * @returns Snapped grid position { x, y }
57
70
  */
58
- export declare function screenToIso(screenX: number, screenY: number, scale: number, baseOffsetX: number): {
71
+ export declare function screenToIso(screenX: number, screenY: number, scale: number, baseOffsetX: number, layout?: TileLayout): {
59
72
  x: number;
60
73
  y: number;
61
74
  };
@@ -45,6 +45,7 @@ var THREE3 = require('three');
45
45
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
46
46
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
47
47
  var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
48
+ var SkeletonUtils = require('three/examples/jsm/utils/SkeletonUtils');
48
49
  var fiber = require('@react-three/fiber');
49
50
  var drei = require('@react-three/drei');
50
51
  var patterns = require('@almadar/patterns');
@@ -9922,16 +9923,26 @@ var init_useUnitSpriteAtlas = __esm({
9922
9923
  });
9923
9924
 
9924
9925
  // components/game/organisms/utils/isometric.ts
9925
- function isoToScreen(tileX, tileY, scale, baseOffsetX) {
9926
+ function isoToScreen(tileX, tileY, scale, baseOffsetX, layout = "isometric") {
9926
9927
  const scaledTileWidth = exports.TILE_WIDTH * scale;
9927
9928
  const scaledFloorHeight = exports.FLOOR_HEIGHT * scale;
9929
+ if (layout === "hex") {
9930
+ const screenX2 = tileX * scaledTileWidth + (tileY & 1) * (scaledTileWidth / 2) + baseOffsetX;
9931
+ const screenY2 = tileY * (scaledFloorHeight * 0.75);
9932
+ return { x: screenX2, y: screenY2 };
9933
+ }
9928
9934
  const screenX = (tileX - tileY) * (scaledTileWidth / 2) + baseOffsetX;
9929
9935
  const screenY = (tileX + tileY) * (scaledFloorHeight / 2);
9930
9936
  return { x: screenX, y: screenY };
9931
9937
  }
9932
- function screenToIso(screenX, screenY, scale, baseOffsetX) {
9938
+ function screenToIso(screenX, screenY, scale, baseOffsetX, layout = "isometric") {
9933
9939
  const scaledTileWidth = exports.TILE_WIDTH * scale;
9934
9940
  const scaledFloorHeight = exports.FLOOR_HEIGHT * scale;
9941
+ if (layout === "hex") {
9942
+ const row = Math.round(screenY / (scaledFloorHeight * 0.75));
9943
+ const col = Math.round((screenX - (row & 1) * (scaledTileWidth / 2) - baseOffsetX) / scaledTileWidth);
9944
+ return { x: col, y: row };
9945
+ }
9935
9946
  const adjustedX = screenX - baseOffsetX;
9936
9947
  const tileX = (adjustedX / (scaledTileWidth / 2) + screenY / (scaledFloorHeight / 2)) / 2;
9937
9948
  const tileY = (screenY / (scaledFloorHeight / 2) - adjustedX / (scaledTileWidth / 2)) / 2;
@@ -9981,6 +9992,7 @@ function IsometricCanvas({
9981
9992
  tileHoverEvent,
9982
9993
  tileLeaveEvent,
9983
9994
  // Rendering options
9995
+ tileLayout = "isometric",
9984
9996
  scale = 0.4,
9985
9997
  debug: debug2 = false,
9986
9998
  backgroundImage = "",
@@ -10050,13 +10062,17 @@ function IsometricCanvas({
10050
10062
  );
10051
10063
  const sortedTiles = React79.useMemo(() => {
10052
10064
  const tiles = [...tilesProp];
10053
- tiles.sort((a, b) => {
10054
- const depthA = a.x + a.y;
10055
- const depthB = b.x + b.y;
10056
- return depthA !== depthB ? depthA - depthB : a.y - b.y;
10057
- });
10065
+ if (tileLayout === "hex") {
10066
+ tiles.sort((a, b) => a.y !== b.y ? a.y - b.y : a.x - b.x);
10067
+ } else {
10068
+ tiles.sort((a, b) => {
10069
+ const depthA = a.x + a.y;
10070
+ const depthB = b.x + b.y;
10071
+ return depthA !== depthB ? depthA - depthB : a.y - b.y;
10072
+ });
10073
+ }
10058
10074
  return tiles;
10059
- }, [tilesProp]);
10075
+ }, [tilesProp, tileLayout]);
10060
10076
  const gridWidth = React79.useMemo(() => {
10061
10077
  if (sortedTiles.length === 0) return 0;
10062
10078
  return Math.max(...sortedTiles.map((t2) => t2.x)) + 1;
@@ -10170,7 +10186,7 @@ function IsometricCanvas({
10170
10186
  miniCanvas.width = mW;
10171
10187
  miniCanvas.height = mH;
10172
10188
  mCtx.clearRect(0, 0, mW, mH);
10173
- const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX));
10189
+ const allScreenPos = sortedTiles.map((t2) => isoToScreen(t2.x, t2.y, scale, baseOffsetX, tileLayout));
10174
10190
  const minX = Math.min(...allScreenPos.map((p2) => p2.x));
10175
10191
  const maxX = Math.max(...allScreenPos.map((p2) => p2.x + scaledTileWidth));
10176
10192
  const minY = Math.min(...allScreenPos.map((p2) => p2.y));
@@ -10181,7 +10197,7 @@ function IsometricCanvas({
10181
10197
  const offsetMx = (mW - worldW * scaleM) / 2;
10182
10198
  const offsetMy = (mH - worldH * scaleM) / 2;
10183
10199
  for (const tile of sortedTiles) {
10184
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
10200
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
10185
10201
  const mx = (pos.x - minX) * scaleM + offsetMx;
10186
10202
  const my = (pos.y - minY) * scaleM + offsetMy;
10187
10203
  const mTileW = scaledTileWidth * scaleM;
@@ -10197,7 +10213,7 @@ function IsometricCanvas({
10197
10213
  }
10198
10214
  for (const unit of units) {
10199
10215
  if (!unit.position) continue;
10200
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
10216
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
10201
10217
  const mx = (pos.x + scaledTileWidth / 2 - minX) * scaleM + offsetMx;
10202
10218
  const my = (pos.y + scaledTileHeight / 2 - minY) * scaleM + offsetMy;
10203
10219
  mCtx.fillStyle = unit.team === "player" ? "#60a5fa" : unit.team === "enemy" ? "#f87171" : "#9ca3af";
@@ -10213,7 +10229,7 @@ function IsometricCanvas({
10213
10229
  mCtx.strokeStyle = "rgba(255, 255, 255, 0.8)";
10214
10230
  mCtx.lineWidth = 1;
10215
10231
  mCtx.strokeRect(vLeft, vTop, vW, vH);
10216
- }, [showMinimap, sortedTiles, units, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
10232
+ }, [showMinimap, sortedTiles, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledTileHeight, scaledFloorHeight, viewportSize, cameraRef]);
10217
10233
  const draw = React79.useCallback((animTime) => {
10218
10234
  const canvas = canvasRef.current;
10219
10235
  if (!canvas) return;
@@ -10253,7 +10269,7 @@ function IsometricCanvas({
10253
10269
  const visTop = cam.y - viewportSize.height / cam.zoom;
10254
10270
  const visBottom = cam.y + viewportSize.height * 2 / cam.zoom;
10255
10271
  for (const tile of sortedTiles) {
10256
- const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX);
10272
+ const pos = isoToScreen(tile.x, tile.y, scale, baseOffsetX, tileLayout);
10257
10273
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
10258
10274
  continue;
10259
10275
  }
@@ -10333,12 +10349,13 @@ function IsometricCanvas({
10333
10349
  }
10334
10350
  }
10335
10351
  const sortedFeatures = [...features].sort((a, b) => {
10352
+ if (tileLayout === "hex") return a.y !== b.y ? a.y - b.y : a.x - b.x;
10336
10353
  const depthA = a.x + a.y;
10337
10354
  const depthB = b.x + b.y;
10338
10355
  return depthA !== depthB ? depthA - depthB : a.y - b.y;
10339
10356
  });
10340
10357
  for (const feature of sortedFeatures) {
10341
- const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX);
10358
+ const pos = isoToScreen(feature.x, feature.y, scale, baseOffsetX, tileLayout);
10342
10359
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
10343
10360
  continue;
10344
10361
  }
@@ -10373,12 +10390,13 @@ function IsometricCanvas({
10373
10390
  }
10374
10391
  const unitsWithPosition = units.filter((u) => !!u.position);
10375
10392
  const sortedUnits = [...unitsWithPosition].sort((a, b) => {
10393
+ if (tileLayout === "hex") return a.position.y !== b.position.y ? a.position.y - b.position.y : a.position.x - b.position.x;
10376
10394
  const depthA = a.position.x + a.position.y;
10377
10395
  const depthB = b.position.x + b.position.y;
10378
10396
  return depthA !== depthB ? depthA - depthB : a.position.y - b.position.y;
10379
10397
  });
10380
10398
  for (const unit of sortedUnits) {
10381
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
10399
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
10382
10400
  if (pos.x + scaledTileWidth < visLeft || pos.x > visRight || pos.y + scaledTileHeight < visTop || pos.y > visBottom) {
10383
10401
  continue;
10384
10402
  }
@@ -10398,7 +10416,7 @@ function IsometricCanvas({
10398
10416
  drawH = maxUnitW / ar;
10399
10417
  }
10400
10418
  if (unit.previousPosition && (unit.previousPosition.x !== unit.position.x || unit.previousPosition.y !== unit.position.y)) {
10401
- const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX);
10419
+ const ghostPos = isoToScreen(unit.previousPosition.x, unit.previousPosition.y, scale, baseOffsetX, tileLayout);
10402
10420
  const ghostCenterX = ghostPos.x + scaledTileWidth / 2;
10403
10421
  const ghostGroundY = ghostPos.y + scaledDiamondTopY + scaledFloorHeight * 0.5;
10404
10422
  ctx.save();
@@ -10527,6 +10545,7 @@ function IsometricCanvas({
10527
10545
  units,
10528
10546
  features,
10529
10547
  selectedUnitId,
10548
+ tileLayout,
10530
10549
  scale,
10531
10550
  debug2,
10532
10551
  resolveTerrainSpriteUrl,
@@ -10555,14 +10574,14 @@ function IsometricCanvas({
10555
10574
  if (!selectedUnitId) return;
10556
10575
  const unit = units.find((u) => u.id === selectedUnitId);
10557
10576
  if (!unit?.position) return;
10558
- const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX);
10577
+ const pos = isoToScreen(unit.position.x, unit.position.y, scale, baseOffsetX, tileLayout);
10559
10578
  const centerX = pos.x + scaledTileWidth / 2;
10560
10579
  const centerY = pos.y + scaledDiamondTopY + scaledFloorHeight / 2;
10561
10580
  targetCameraRef.current = {
10562
10581
  x: centerX - viewportSize.width / 2,
10563
10582
  y: centerY - viewportSize.height / 2
10564
10583
  };
10565
- }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
10584
+ }, [selectedUnitId, units, tileLayout, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
10566
10585
  React79.useEffect(() => {
10567
10586
  const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
10568
10587
  draw(animTimeRef.current);
@@ -10595,13 +10614,13 @@ function IsometricCanvas({
10595
10614
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10596
10615
  const adjustedX = world.x - scaledTileWidth / 2;
10597
10616
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10598
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
10617
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
10599
10618
  const tileExists = tilesProp.some((t2) => t2.x === isoPos.x && t2.y === isoPos.y);
10600
10619
  if (tileExists) {
10601
10620
  if (tileHoverEvent) eventBus.emit(`UI:${tileHoverEvent}`, { x: isoPos.x, y: isoPos.y });
10602
10621
  onTileHover?.(isoPos.x, isoPos.y);
10603
10622
  }
10604
- }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
10623
+ }, [onTileHover, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, tilesProp, tileHoverEvent, eventBus]);
10605
10624
  const handleCanvasPointerUp = React79.useCallback((e) => {
10606
10625
  singlePointerActiveRef.current = false;
10607
10626
  if (enableCamera) handlePointerUp();
@@ -10610,7 +10629,7 @@ function IsometricCanvas({
10610
10629
  const world = screenToWorld(e.clientX, e.clientY, canvasRef.current, viewportSize);
10611
10630
  const adjustedX = world.x - scaledTileWidth / 2;
10612
10631
  const adjustedY = world.y - scaledDiamondTopY - scaledFloorHeight / 2;
10613
- const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX);
10632
+ const isoPos = screenToIso(adjustedX, adjustedY, scale, baseOffsetX, tileLayout);
10614
10633
  const clickedUnit = units.find((u) => u.position?.x === isoPos.x && u.position?.y === isoPos.y);
10615
10634
  if (clickedUnit && (onUnitClick || unitClickEvent)) {
10616
10635
  if (unitClickEvent) eventBus.emit(`UI:${unitClickEvent}`, { unitId: clickedUnit.id });
@@ -10622,7 +10641,7 @@ function IsometricCanvas({
10622
10641
  onTileClick?.(isoPos.x, isoPos.y);
10623
10642
  }
10624
10643
  }
10625
- }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
10644
+ }, [enableCamera, handlePointerUp, dragDistance, screenToWorld, viewportSize, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, tileLayout, scale, baseOffsetX, units, tilesProp, onUnitClick, onTileClick, unitClickEvent, tileClickEvent, eventBus]);
10626
10645
  const handleCanvasPointerLeave = React79.useCallback(() => {
10627
10646
  handleMouseLeave();
10628
10647
  if (tileLeaveEvent) eventBus.emit(`UI:${tileLeaveEvent}`, {});
@@ -44014,7 +44033,8 @@ function ModelLoader({
44014
44033
  const { model: loadedModel, isLoading, error } = useGLTFModel(url, resourceBasePath);
44015
44034
  const model = React79.useMemo(() => {
44016
44035
  if (!loadedModel) return null;
44017
- const cloned = loadedModel.clone();
44036
+ const cloned = SkeletonUtils.clone(loadedModel);
44037
+ cloned.updateMatrixWorld(true);
44018
44038
  cloned.traverse((child) => {
44019
44039
  if (child instanceof THREE3__namespace.Mesh) {
44020
44040
  child.castShadow = castShadow;
@@ -44029,7 +44049,8 @@ function ModelLoader({
44029
44049
  const size = new THREE3__namespace.Vector3();
44030
44050
  box.getSize(size);
44031
44051
  const maxDim = Math.max(size.x, size.y, size.z);
44032
- return maxDim > 0 && Number.isFinite(maxDim) ? 1 / maxDim : 1;
44052
+ if (!Number.isFinite(maxDim) || maxDim < 0.05) return 1;
44053
+ return 1 / maxDim;
44033
44054
  }, [model]);
44034
44055
  const scaleArray = React79.useMemo(() => {
44035
44056
  const base = typeof scale === "number" ? [scale, scale, scale] : scale;
@@ -45311,6 +45332,48 @@ var init_HeroOrganism = __esm({
45311
45332
  _HeroClickInterceptor.displayName = "_HeroClickInterceptor";
45312
45333
  }
45313
45334
  });
45335
+ function HexStrategyBoard({
45336
+ tiles,
45337
+ units,
45338
+ features,
45339
+ assetManifest,
45340
+ assetBaseUrl,
45341
+ scale = 0.45,
45342
+ showMinimap = true,
45343
+ enableCamera = true,
45344
+ tileClickEvent,
45345
+ unitClickEvent,
45346
+ isLoading,
45347
+ error,
45348
+ className
45349
+ }) {
45350
+ return /* @__PURE__ */ jsxRuntime.jsx("div", { className: cn("hex-strategy-board relative w-full h-full", className), children: /* @__PURE__ */ jsxRuntime.jsx(
45351
+ IsometricCanvas_default,
45352
+ {
45353
+ tileLayout: "hex",
45354
+ tiles,
45355
+ units,
45356
+ features,
45357
+ assetManifest,
45358
+ assetBaseUrl,
45359
+ scale,
45360
+ showMinimap,
45361
+ enableCamera,
45362
+ tileClickEvent,
45363
+ unitClickEvent,
45364
+ isLoading,
45365
+ error
45366
+ }
45367
+ ) });
45368
+ }
45369
+ var init_HexStrategyBoard = __esm({
45370
+ "components/game/organisms/HexStrategyBoard.tsx"() {
45371
+ "use client";
45372
+ init_cn();
45373
+ init_IsometricCanvas();
45374
+ HexStrategyBoard.displayName = "HexStrategyBoard";
45375
+ }
45376
+ });
45314
45377
  exports.LandingPageTemplate = void 0;
45315
45378
  var init_LandingPageTemplate = __esm({
45316
45379
  "components/marketing/templates/LandingPageTemplate.tsx"() {
@@ -53619,6 +53682,7 @@ var init_component_registry_generated = __esm({
53619
53682
  init_HealthPanel();
53620
53683
  init_HeroOrganism();
53621
53684
  init_HeroSection();
53685
+ init_HexStrategyBoard();
53622
53686
  init_Icon();
53623
53687
  init_InfiniteScrollSentinel();
53624
53688
  init_Input();
@@ -53955,6 +54019,7 @@ var init_component_registry_generated = __esm({
53955
54019
  "HealthPanel": HealthPanel,
53956
54020
  "HeroOrganism": exports.HeroOrganism,
53957
54021
  "HeroSection": exports.HeroSection,
54022
+ "HexStrategyBoard": HexStrategyBoard,
53958
54023
  "Icon": exports.Icon,
53959
54024
  "InfiniteScrollSentinel": exports.InfiniteScrollSentinel,
53960
54025
  "Input": exports.Input,
@@ -55600,6 +55665,7 @@ init_CityBuilderBoard();
55600
55665
  init_GameBoard3D();
55601
55666
  init_VisualNovelBoard();
55602
55667
  init_CardBattlerBoard();
55668
+ init_HexStrategyBoard();
55603
55669
  init_TraitStateViewer();
55604
55670
  init_TraitSlot();
55605
55671
 
@@ -56241,6 +56307,7 @@ exports.GameMenu = GameMenu;
56241
56307
  exports.GameOverScreen = GameOverScreen;
56242
56308
  exports.HealthBar = HealthBar;
56243
56309
  exports.HealthPanel = HealthPanel;
56310
+ exports.HexStrategyBoard = HexStrategyBoard;
56244
56311
  exports.InventoryGrid = InventoryGrid;
56245
56312
  exports.InventoryPanel = InventoryPanel;
56246
56313
  exports.IsometricCanvas = IsometricCanvas;