@dreamboard-games/sdk 0.2.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.
- package/LICENSE.md +96 -0
- package/README.md +12 -0
- package/dist/HandView-ncJIVLhN.d.ts +193 -0
- package/dist/ResourceCounter-CTREyF73.d.ts +102 -0
- package/dist/ThemeProvider-fy0_QzgO.d.ts +99 -0
- package/dist/bundle-TIZcw8LB.d.ts +281 -0
- package/dist/cards-Sl3b40Mv.d.ts +13 -0
- package/dist/chunk-7YAHLYBR.js +481 -0
- package/dist/chunk-7YAHLYBR.js.map +1 -0
- package/dist/chunk-FDNZTDD6.js +8085 -0
- package/dist/chunk-FDNZTDD6.js.map +1 -0
- package/dist/chunk-GKKBPPSW.js +598 -0
- package/dist/chunk-GKKBPPSW.js.map +1 -0
- package/dist/chunk-I46YJSOD.js +1 -0
- package/dist/chunk-I46YJSOD.js.map +1 -0
- package/dist/chunk-KAELH4KC.js +104 -0
- package/dist/chunk-KAELH4KC.js.map +1 -0
- package/dist/chunk-PZ5AY32C.js +10 -0
- package/dist/chunk-PZ5AY32C.js.map +1 -0
- package/dist/chunk-T3ZKNUZ7.js +1 -0
- package/dist/chunk-T3ZKNUZ7.js.map +1 -0
- package/dist/chunk-T52J5RMF.js +1 -0
- package/dist/chunk-T52J5RMF.js.map +1 -0
- package/dist/chunk-TDSWKVZ4.js +5401 -0
- package/dist/chunk-TDSWKVZ4.js.map +1 -0
- package/dist/chunk-U5C6BONG.js +34 -0
- package/dist/chunk-U5C6BONG.js.map +1 -0
- package/dist/chunk-VDXOF4FW.js +69 -0
- package/dist/chunk-VDXOF4FW.js.map +1 -0
- package/dist/chunk-VFTAA4WO.js +115 -0
- package/dist/chunk-VFTAA4WO.js.map +1 -0
- package/dist/chunk-WN74KVNY.js +17 -0
- package/dist/chunk-WN74KVNY.js.map +1 -0
- package/dist/chunk-WYPQ3GG5.js +10990 -0
- package/dist/chunk-WYPQ3GG5.js.map +1 -0
- package/dist/components-D5ZRE2Hl.d.ts +1451 -0
- package/dist/generated/runtime/primitives.d.ts +12 -0
- package/dist/generated/runtime/primitives.js +180 -0
- package/dist/generated/runtime/primitives.js.map +1 -0
- package/dist/generated/runtime-api.d.ts +3 -0
- package/dist/generated/runtime-api.js +2 -0
- package/dist/generated/runtime-api.js.map +1 -0
- package/dist/generated/runtime.d.ts +14 -0
- package/dist/generated/runtime.js +18 -0
- package/dist/generated/runtime.js.map +1 -0
- package/dist/generated/workspace-contract.d.ts +14 -0
- package/dist/generated/workspace-contract.js +14 -0
- package/dist/generated/workspace-contract.js.map +1 -0
- package/dist/hex-board-view-D_07hO6O.d.ts +933 -0
- package/dist/hex-color-MhOyuY-o.d.ts +8 -0
- package/dist/index-BwqPQtBu.d.ts +1433 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/infrastructure/reducer-bundle-abi.d.ts +1083 -0
- package/dist/infrastructure/reducer-bundle-abi.js +14 -0
- package/dist/infrastructure/reducer-bundle-abi.js.map +1 -0
- package/dist/infrastructure/workspace-codegen.d.ts +53 -0
- package/dist/infrastructure/workspace-codegen.js +44 -0
- package/dist/infrastructure/workspace-codegen.js.map +1 -0
- package/dist/manifest-contract-BNHVGFtU.d.ts +9 -0
- package/dist/package-set.d.ts +13 -0
- package/dist/package-set.js +12 -0
- package/dist/package-set.js.map +1 -0
- package/dist/primitive-props-DpKs-GCr.d.ts +11 -0
- package/dist/reducer.d.ts +3786 -0
- package/dist/reducer.js +8131 -0
- package/dist/reducer.js.map +1 -0
- package/dist/runtime/primitives.d.ts +226 -0
- package/dist/runtime/primitives.js +180 -0
- package/dist/runtime/primitives.js.map +1 -0
- package/dist/runtime/types/runtime-api.d.ts +1 -0
- package/dist/runtime/types/runtime-api.js +2 -0
- package/dist/runtime/types/runtime-api.js.map +1 -0
- package/dist/runtime/workspace-contract.d.ts +172 -0
- package/dist/runtime/workspace-contract.js +14 -0
- package/dist/runtime/workspace-contract.js.map +1 -0
- package/dist/runtime-api-3dshj6kK.d.ts +101 -0
- package/dist/runtime-api-DWxvTr-O.d.ts +379 -0
- package/dist/runtime.d.ts +58 -0
- package/dist/runtime.js +13 -0
- package/dist/runtime.js.map +1 -0
- package/dist/slots-1GPGihk8.d.ts +8 -0
- package/dist/testing.d.ts +149 -0
- package/dist/testing.js +513 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +496 -0
- package/dist/types.js +28 -0
- package/dist/types.js.map +1 -0
- package/dist/ui/components.d.ts +16 -0
- package/dist/ui/components.js +192 -0
- package/dist/ui/components.js.map +1 -0
- package/dist/ui/defaults.d.ts +19 -0
- package/dist/ui/defaults.js +104 -0
- package/dist/ui/defaults.js.map +1 -0
- package/dist/ui/plugin-styles.css +250 -0
- package/dist/ui/types/player-state.d.ts +365 -0
- package/dist/ui/types/player-state.js +1 -0
- package/dist/ui/types/player-state.js.map +1 -0
- package/dist/ui-contract-iQfTtUSL.d.ts +1161 -0
- package/dist/ui.d.ts +320 -0
- package/dist/ui.js +253 -0
- package/dist/ui.js.map +1 -0
- package/package.json +199 -0
- package/src/generated/reducer-contract/builders.ts +90 -0
- package/src/generated/reducer-contract/version.ts +9 -0
- package/src/generated/reducer-contract/wire.ts +100 -0
- package/src/generated/reducer-contract/zod.ts +101 -0
- package/src/generated/runtime/primitives.ts +2 -0
- package/src/generated/runtime-api.ts +5 -0
- package/src/generated/runtime.ts +35 -0
- package/src/generated/workspace-contract.ts +2 -0
- package/src/index.ts +7 -0
- package/src/infrastructure/reducer-bundle-abi.ts +8 -0
- package/src/infrastructure/reducer-contract/bundle.ts +37 -0
- package/src/infrastructure/workspace-codegen/hex-geometry.ts +69 -0
- package/src/infrastructure/workspace-codegen/index.ts +64 -0
- package/src/infrastructure/workspace-codegen/manifest-contract.ts +6632 -0
- package/src/infrastructure/workspace-codegen/manifest-validation.ts +795 -0
- package/src/infrastructure/workspace-codegen/ownership.ts +131 -0
- package/src/infrastructure/workspace-codegen/preset-card-sets.ts +169 -0
- package/src/infrastructure/workspace-codegen/seeds.ts +1705 -0
- package/src/infrastructure/workspace-codegen.ts +1 -0
- package/src/package-set.ts +19 -0
- package/src/reducer/authoring/contract.ts +157 -0
- package/src/reducer/authoring/effect.ts +224 -0
- package/src/reducer/authoring/game.ts +23 -0
- package/src/reducer/authoring/interaction.ts +98 -0
- package/src/reducer/authoring/phase.ts +300 -0
- package/src/reducer/authoring/types.ts +70 -0
- package/src/reducer/authoring/validation.ts +382 -0
- package/src/reducer/authoring/view-stage.ts +68 -0
- package/src/reducer/authoring.ts +29 -0
- package/src/reducer/bundle/ingress-bundle.ts +491 -0
- package/src/reducer/bundle/trusted/engine-instruction-resolver.ts +254 -0
- package/src/reducer/bundle/trusted/flow-instruction-resolver.ts +73 -0
- package/src/reducer/bundle/trusted/instruction-runner.ts +414 -0
- package/src/reducer/bundle/trusted/interaction-authorization.ts +137 -0
- package/src/reducer/bundle/trusted/interaction-collectors.ts +859 -0
- package/src/reducer/bundle/trusted/interaction-decision.ts +747 -0
- package/src/reducer/bundle/trusted/interaction-resolver.ts +95 -0
- package/src/reducer/bundle/trusted/interaction-types.ts +171 -0
- package/src/reducer/bundle/trusted/lifecycle-runner.ts +427 -0
- package/src/reducer/bundle/trusted/projection-builder.ts +356 -0
- package/src/reducer/bundle/trusted/projection-context.ts +39 -0
- package/src/reducer/bundle/trusted/rng-sampler.ts +150 -0
- package/src/reducer/bundle/trusted/runtime-registry.ts +120 -0
- package/src/reducer/bundle/trusted/runtime-scope.ts +336 -0
- package/src/reducer/bundle/trusted/simultaneous-player.ts +97 -0
- package/src/reducer/bundle/trusted/stage-resolver.ts +87 -0
- package/src/reducer/bundle/trusted/static-projection.ts +116 -0
- package/src/reducer/bundle/trusted/trusted-runtime-args.ts +97 -0
- package/src/reducer/bundle/trusted/trusted-runtime-result.ts +39 -0
- package/src/reducer/bundle/trusted/trusted-setup-profiles.ts +43 -0
- package/src/reducer/bundle/trusted/trusted-state-codec.ts +48 -0
- package/src/reducer/bundle/trusted-bundle.ts +97 -0
- package/src/reducer/bundle/types.ts +171 -0
- package/src/reducer/bundle.ts +2 -0
- package/src/reducer/client-param-schemas.ts +57 -0
- package/src/reducer/compose.ts +34 -0
- package/src/reducer/core/runtime-input.ts +30 -0
- package/src/reducer/core/runtime-instruction.ts +59 -0
- package/src/reducer/core/types.ts +62 -0
- package/src/reducer/definition-index.ts +277 -0
- package/src/reducer/derived.ts +106 -0
- package/src/reducer/effects.ts +92 -0
- package/src/reducer/engine/runtime-instruction-engine.ts +155 -0
- package/src/reducer/ingress/decode-runtime-input.ts +7 -0
- package/src/reducer/ingress/decode-session-state.ts +9 -0
- package/src/reducer/ingress/encode-session-state.ts +6 -0
- package/src/reducer/ingress/input-codec.ts +18 -0
- package/src/reducer/ingress/phase-schemas.ts +62 -0
- package/src/reducer/ingress/raw-types.ts +107 -0
- package/src/reducer/ingress/runtime-codec.ts +14 -0
- package/src/reducer/ingress/runtime-payload.ts +13 -0
- package/src/reducer/ingress/session-codec.ts +392 -0
- package/src/reducer/ingress/types.ts +6 -0
- package/src/reducer/inputs/boardInput.ts +217 -0
- package/src/reducer/inputs/boardTarget.ts +190 -0
- package/src/reducer/inputs/cardInput.ts +86 -0
- package/src/reducer/inputs/cardTarget.ts +101 -0
- package/src/reducer/inputs/choiceTarget.ts +104 -0
- package/src/reducer/inputs/defineInputs.ts +71 -0
- package/src/reducer/inputs/formInput.ts +809 -0
- package/src/reducer/inputs/many.ts +120 -0
- package/src/reducer/inputs/promptInput.ts +87 -0
- package/src/reducer/inputs/rngInput.ts +58 -0
- package/src/reducer/inputs/targetRule.ts +123 -0
- package/src/reducer/inputs.ts +41 -0
- package/src/reducer/model/definition.ts +1072 -0
- package/src/reducer/model/extract.ts +745 -0
- package/src/reducer/model/manifest.ts +570 -0
- package/src/reducer/model/queries.ts +641 -0
- package/src/reducer/model/runtime.ts +264 -0
- package/src/reducer/model/spec.ts +1386 -0
- package/src/reducer/model/table.ts +260 -0
- package/src/reducer/model.ts +7 -0
- package/src/reducer/ops.ts +1034 -0
- package/src/reducer/parse-utils.ts +28 -0
- package/src/reducer/per-player.ts +422 -0
- package/src/reducer/rng.ts +69 -0
- package/src/reducer/schema-helpers.ts +185 -0
- package/src/reducer/setup-bootstrap-helpers.ts +171 -0
- package/src/reducer/setup-bootstrap.ts +481 -0
- package/src/reducer/table-ops.ts +2671 -0
- package/src/reducer/table-queries.ts +372 -0
- package/src/reducer/transaction.ts +120 -0
- package/src/reducer.ts +314 -0
- package/src/runtime/primitives.ts +1 -0
- package/src/runtime/types/runtime-api.ts +1 -0
- package/src/runtime/workspace-contract.ts +32 -0
- package/src/runtime-internal/components/InteractionForm.tsx +1309 -0
- package/src/runtime-internal/components/PluginRuntime.tsx +103 -0
- package/src/runtime-internal/components/board/target-layer.ts +70 -0
- package/src/runtime-internal/context/ClientParamSchemaContext.tsx +44 -0
- package/src/runtime-internal/context/InteractionDraftContext.tsx +279 -0
- package/src/runtime-internal/context/PluginSessionContext.tsx +47 -0
- package/src/runtime-internal/context/PluginStateContext.tsx +262 -0
- package/src/runtime-internal/context/RuntimeContext.tsx +96 -0
- package/src/runtime-internal/defaults/components.tsx +409 -0
- package/src/runtime-internal/defaults/index.ts +11 -0
- package/src/runtime-internal/errors/ValidationError.ts +29 -0
- package/src/runtime-internal/hooks/useActivePlayers.ts +33 -0
- package/src/runtime-internal/hooks/useBoardInteractions.ts +665 -0
- package/src/runtime-internal/hooks/useGameSelector.ts +105 -0
- package/src/runtime-internal/hooks/useGameView.ts +9 -0
- package/src/runtime-internal/hooks/useInteractionByKey.ts +354 -0
- package/src/runtime-internal/hooks/useInteractionHandle.ts +438 -0
- package/src/runtime-internal/hooks/useIsMyTurn.ts +20 -0
- package/src/runtime-internal/hooks/useLobby.ts +76 -0
- package/src/runtime-internal/hooks/useMe.ts +48 -0
- package/src/runtime-internal/hooks/usePlayerInfo.ts +28 -0
- package/src/runtime-internal/hooks/usePlayerTurnOrder.ts +23 -0
- package/src/runtime-internal/hooks/usePluginRuntime.ts +147 -0
- package/src/runtime-internal/hooks/useSeatInbox.ts +61 -0
- package/src/runtime-internal/hooks/useSimultaneousPhase.ts +10 -0
- package/src/runtime-internal/index.ts +42 -0
- package/src/runtime-internal/internal.ts +43 -0
- package/src/runtime-internal/plugin-styles.css +250 -0
- package/src/runtime-internal/primitives/board.tsx +459 -0
- package/src/runtime-internal/primitives/dialog-lifecycle.ts +58 -0
- package/src/runtime-internal/primitives/dice.tsx +79 -0
- package/src/runtime-internal/primitives/game-ui-provider.tsx +35 -0
- package/src/runtime-internal/primitives/game.tsx +387 -0
- package/src/runtime-internal/primitives/hand-intent-adapter.ts +147 -0
- package/src/runtime-internal/primitives/hand-surface.tsx +594 -0
- package/src/runtime-internal/primitives/index.ts +196 -0
- package/src/runtime-internal/primitives/interaction-form-binding.tsx +56 -0
- package/src/runtime-internal/primitives/interaction-submit.ts +90 -0
- package/src/runtime-internal/primitives/interaction.tsx +987 -0
- package/src/runtime-internal/primitives/phase.tsx +43 -0
- package/src/runtime-internal/primitives/player-roster.tsx +302 -0
- package/src/runtime-internal/primitives/primitive-props.tsx +101 -0
- package/src/runtime-internal/primitives/prompt.tsx +255 -0
- package/src/runtime-internal/primitives/ui.tsx +60 -0
- package/src/runtime-internal/primitives/zone.tsx +791 -0
- package/src/runtime-internal/reducer.ts +30 -0
- package/src/runtime-internal/runtime/createPluginRuntimeAPI.ts +605 -0
- package/src/runtime-internal/types/plugin-state.ts +508 -0
- package/src/runtime-internal/types/reducer-state.ts +24 -0
- package/src/runtime-internal/types/runtime-api.ts +114 -0
- package/src/runtime-internal/ui-contract.ts +519 -0
- package/src/runtime-internal/utils/card-intent-adapter.ts +546 -0
- package/src/runtime-internal/utils/interaction-inputs.ts +492 -0
- package/src/runtime-internal/utils/interaction-labels.ts +23 -0
- package/src/runtime-internal/utils/interaction-router.ts +273 -0
- package/src/runtime-internal/utils/interaction-status.ts +74 -0
- package/src/runtime-internal/workspace-contract.ts +1170 -0
- package/src/runtime.ts +34 -0
- package/src/testing/create-expect-api.ts +352 -0
- package/src/testing/create-test-runtime.ts +381 -0
- package/src/testing/definitions.ts +127 -0
- package/src/testing/index.ts +3 -0
- package/src/testing.ts +1 -0
- package/src/type-stubs/manifest-contract.d.ts +42 -0
- package/src/type-stubs/manifest-contract.js +72 -0
- package/src/type-stubs/ui-contract.d.ts +5 -0
- package/src/type-stubs/ui-contract.js +1 -0
- package/src/types/authoring-card-properties.type-test.ts +266 -0
- package/src/types/authoring.ts +1282 -0
- package/src/types/cards.ts +19 -0
- package/src/types/contracts.ts +1550 -0
- package/src/types/generated-helpers.ts +35 -0
- package/src/types/index.ts +147 -0
- package/src/types/slots.ts +11 -0
- package/src/types.ts +1 -0
- package/src/ui/components/ActionButton.tsx +97 -0
- package/src/ui/components/ActionPanel.tsx +315 -0
- package/src/ui/components/Card.tsx +378 -0
- package/src/ui/components/CardDragSurface.tsx +1076 -0
- package/src/ui/components/ChromeSuppressionContext.tsx +70 -0
- package/src/ui/components/CostDisplay.tsx +145 -0
- package/src/ui/components/DiceRoller.tsx +581 -0
- package/src/ui/components/Drawer.tsx +180 -0
- package/src/ui/components/ErrorBoundary.tsx +275 -0
- package/src/ui/components/GameEndDisplay.tsx +398 -0
- package/src/ui/components/GameSkeleton.tsx +260 -0
- package/src/ui/components/Hand.tsx +468 -0
- package/src/ui/components/HandDock.tsx +299 -0
- package/src/ui/components/HandView.tsx +441 -0
- package/src/ui/components/MobileHandTray.tsx +381 -0
- package/src/ui/components/MoreActions.tsx +143 -0
- package/src/ui/components/PhaseIndicator.tsx +341 -0
- package/src/ui/components/PlayArea.tsx +146 -0
- package/src/ui/components/PrimaryActionButton.tsx +336 -0
- package/src/ui/components/PrimaryButton.tsx +45 -0
- package/src/ui/components/ResourceCounter.tsx +270 -0
- package/src/ui/components/StagingZone.tsx +134 -0
- package/src/ui/components/ThemedButton.tsx +113 -0
- package/src/ui/components/Toast.tsx +264 -0
- package/src/ui/components/board/HexGrid.tsx +1294 -0
- package/src/ui/components/board/NetworkGraph.tsx +476 -0
- package/src/ui/components/board/SlotSystem.tsx +388 -0
- package/src/ui/components/board/SquareGrid.tsx +1165 -0
- package/src/ui/components/board/TrackBoard.tsx +496 -0
- package/src/ui/components/board/ZoneMap.tsx +448 -0
- package/src/ui/components/board/hex-board-view.ts +123 -0
- package/src/ui/components/board/index.ts +142 -0
- package/src/ui/components/board/interaction-accessibility.ts +21 -0
- package/src/ui/components/board/target-layer.ts +66 -0
- package/src/ui/components/card-render-content.type-test.ts +27 -0
- package/src/ui/components/hand-layout-math.ts +163 -0
- package/src/ui/components/hand-pointer-engine.ts +413 -0
- package/src/ui/components/index.ts +245 -0
- package/src/ui/components.ts +1 -0
- package/src/ui/defaults/components.tsx +106 -0
- package/src/ui/defaults/index.ts +8 -0
- package/src/ui/defaults.ts +1 -0
- package/src/ui/errors/ValidationError.ts +29 -0
- package/src/ui/helpers/cards.ts +19 -0
- package/src/ui/helpers/track-board.ts +211 -0
- package/src/ui/hooks/useBoardTopology.ts +316 -0
- package/src/ui/hooks/useCards.ts +10 -0
- package/src/ui/hooks/useHandCardPointer.ts +381 -0
- package/src/ui/hooks/useHandLayout.ts +378 -0
- package/src/ui/hooks/useHandPresentation.ts +121 -0
- package/src/ui/hooks/useHexBoard.ts +74 -0
- package/src/ui/hooks/useHexGrid.ts +185 -0
- package/src/ui/hooks/useIsMobile.ts +35 -0
- package/src/ui/hooks/usePanZoom.ts +278 -0
- package/src/ui/hooks/useSquareBoard.ts +124 -0
- package/src/ui/hooks/useSquareGrid.ts +328 -0
- package/src/ui/index.ts +98 -0
- package/src/ui/internal/ui/alert.tsx +51 -0
- package/src/ui/internal/ui/button.tsx +58 -0
- package/src/ui/internal/ui/dialog.tsx +134 -0
- package/src/ui/internal/ui/input.tsx +21 -0
- package/src/ui/internal/ui/label.tsx +21 -0
- package/src/ui/internal/ui/select.tsx +129 -0
- package/src/ui/internal/ui/tooltip.tsx +54 -0
- package/src/ui/internal/ui/utils.ts +5 -0
- package/src/ui/plugin-styles.css +250 -0
- package/src/ui/primitives/dialog-lifecycle.ts +58 -0
- package/src/ui/primitives/dice.tsx +79 -0
- package/src/ui/primitives/primitive-props.tsx +101 -0
- package/src/ui/theme/ThemeProvider.tsx +252 -0
- package/src/ui/theme/board.ts +61 -0
- package/src/ui/theme/css-vars.ts +105 -0
- package/src/ui/theme/derive.ts +240 -0
- package/src/ui/theme/index.ts +61 -0
- package/src/ui/theme/presets/arcade.ts +261 -0
- package/src/ui/theme/presets/studio.ts +261 -0
- package/src/ui/theme/presets/tabletop.ts +266 -0
- package/src/ui/theme/tokens.ts +392 -0
- package/src/ui/types/hex-color.ts +20 -0
- package/src/ui/types/player-state.ts +463 -0
- package/src/ui/types/tiled-board.ts +785 -0
- package/src/ui/types/visual-state.ts +137 -0
- package/src/ui.ts +1 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useHexGrid hook - Headless logic for hex grid games
|
|
3
|
+
*
|
|
4
|
+
* Provides utilities for:
|
|
5
|
+
* - Coordinate conversion and neighbor finding
|
|
6
|
+
* - Distance calculations
|
|
7
|
+
* - Tile lookups
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```tsx
|
|
11
|
+
* const { getNeighbors, getDistance, getTile } = useHexGrid(tiles);
|
|
12
|
+
*
|
|
13
|
+
* // Find all adjacent tiles
|
|
14
|
+
* const neighbors = getNeighbors('center');
|
|
15
|
+
*
|
|
16
|
+
* // Check if two tiles are adjacent
|
|
17
|
+
* const isAdjacent = getDistance('tile1', 'tile2') === 1;
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { useMemo, useCallback } from "react";
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Types
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export interface HexTileData {
|
|
28
|
+
/** Unique tile identifier */
|
|
29
|
+
id: string;
|
|
30
|
+
/** Axial coordinate Q */
|
|
31
|
+
q: number;
|
|
32
|
+
/** Axial coordinate R */
|
|
33
|
+
r: number;
|
|
34
|
+
/** Tile type */
|
|
35
|
+
type?: string;
|
|
36
|
+
/** Additional data */
|
|
37
|
+
data?: Record<string, unknown>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface UseHexGridReturn {
|
|
41
|
+
/** Get a tile by ID */
|
|
42
|
+
getTile: (tileId: string) => HexTileData | undefined;
|
|
43
|
+
/** Get a tile by coordinates */
|
|
44
|
+
getTileAt: (q: number, r: number) => HexTileData | undefined;
|
|
45
|
+
/** Get neighboring tiles */
|
|
46
|
+
getNeighbors: (tileId: string) => HexTileData[];
|
|
47
|
+
/** Get distance between two tiles */
|
|
48
|
+
getDistance: (fromId: string, toId: string) => number;
|
|
49
|
+
/** Get all tiles within a range */
|
|
50
|
+
getHexesInRange: (centerId: string, range: number) => HexTileData[];
|
|
51
|
+
/** Convert axial to cube coordinates */
|
|
52
|
+
axialToCube: (q: number, r: number) => { x: number; y: number; z: number };
|
|
53
|
+
/** Convert cube to axial coordinates */
|
|
54
|
+
cubeToAxial: (x: number, y: number, z: number) => { q: number; r: number };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ============================================================================
|
|
58
|
+
// Axial direction vectors
|
|
59
|
+
// ============================================================================
|
|
60
|
+
|
|
61
|
+
const AXIAL_DIRECTIONS = [
|
|
62
|
+
{ q: 1, r: 0 },
|
|
63
|
+
{ q: 1, r: -1 },
|
|
64
|
+
{ q: 0, r: -1 },
|
|
65
|
+
{ q: -1, r: 0 },
|
|
66
|
+
{ q: -1, r: 1 },
|
|
67
|
+
{ q: 0, r: 1 },
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
// ============================================================================
|
|
71
|
+
// Hook Implementation
|
|
72
|
+
// ============================================================================
|
|
73
|
+
|
|
74
|
+
export function useHexGrid(tiles: HexTileData[]): UseHexGridReturn {
|
|
75
|
+
// Create lookup maps
|
|
76
|
+
const tileById = useMemo(() => {
|
|
77
|
+
return new Map(tiles.map((t) => [t.id, t]));
|
|
78
|
+
}, [tiles]);
|
|
79
|
+
|
|
80
|
+
const tileByCoord = useMemo(() => {
|
|
81
|
+
return new Map(tiles.map((t) => [`${t.q},${t.r}`, t]));
|
|
82
|
+
}, [tiles]);
|
|
83
|
+
|
|
84
|
+
// Get tile by ID
|
|
85
|
+
const getTile = useCallback(
|
|
86
|
+
(tileId: string): HexTileData | undefined => {
|
|
87
|
+
return tileById.get(tileId);
|
|
88
|
+
},
|
|
89
|
+
[tileById],
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
// Get tile by coordinates
|
|
93
|
+
const getTileAt = useCallback(
|
|
94
|
+
(q: number, r: number): HexTileData | undefined => {
|
|
95
|
+
return tileByCoord.get(`${q},${r}`);
|
|
96
|
+
},
|
|
97
|
+
[tileByCoord],
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
// Get neighboring tiles
|
|
101
|
+
const getNeighbors = useCallback(
|
|
102
|
+
(tileId: string): HexTileData[] => {
|
|
103
|
+
const tile = tileById.get(tileId);
|
|
104
|
+
if (!tile) return [];
|
|
105
|
+
|
|
106
|
+
const neighbors: HexTileData[] = [];
|
|
107
|
+
for (const dir of AXIAL_DIRECTIONS) {
|
|
108
|
+
const neighbor = tileByCoord.get(`${tile.q + dir.q},${tile.r + dir.r}`);
|
|
109
|
+
if (neighbor) {
|
|
110
|
+
neighbors.push(neighbor);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return neighbors;
|
|
114
|
+
},
|
|
115
|
+
[tileById, tileByCoord],
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Calculate distance between two tiles
|
|
119
|
+
const getDistance = useCallback(
|
|
120
|
+
(fromId: string, toId: string): number => {
|
|
121
|
+
const from = tileById.get(fromId);
|
|
122
|
+
const to = tileById.get(toId);
|
|
123
|
+
if (!from || !to) return Infinity;
|
|
124
|
+
|
|
125
|
+
// Hex distance formula using axial coordinates
|
|
126
|
+
return (
|
|
127
|
+
(Math.abs(from.q - to.q) +
|
|
128
|
+
Math.abs(from.q + from.r - to.q - to.r) +
|
|
129
|
+
Math.abs(from.r - to.r)) /
|
|
130
|
+
2
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
[tileById],
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
// Get all tiles within range
|
|
137
|
+
const getHexesInRange = useCallback(
|
|
138
|
+
(centerId: string, range: number): HexTileData[] => {
|
|
139
|
+
const center = tileById.get(centerId);
|
|
140
|
+
if (!center) return [];
|
|
141
|
+
|
|
142
|
+
const results: HexTileData[] = [];
|
|
143
|
+
|
|
144
|
+
for (let dq = -range; dq <= range; dq++) {
|
|
145
|
+
const minR = Math.max(-range, -dq - range);
|
|
146
|
+
const maxR = Math.min(range, -dq + range);
|
|
147
|
+
for (let dr = minR; dr <= maxR; dr++) {
|
|
148
|
+
const tile = tileByCoord.get(`${center.q + dq},${center.r + dr}`);
|
|
149
|
+
if (tile) {
|
|
150
|
+
results.push(tile);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return results;
|
|
156
|
+
},
|
|
157
|
+
[tileById, tileByCoord],
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
// Convert axial to cube coordinates
|
|
161
|
+
const axialToCube = useCallback(
|
|
162
|
+
(q: number, r: number): { x: number; y: number; z: number } => {
|
|
163
|
+
return { x: q, z: r, y: -q - r };
|
|
164
|
+
},
|
|
165
|
+
[],
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
// Convert cube to axial coordinates
|
|
169
|
+
const cubeToAxial = useCallback(
|
|
170
|
+
(x: number, _y: number, z: number): { q: number; r: number } => {
|
|
171
|
+
return { q: x, r: z };
|
|
172
|
+
},
|
|
173
|
+
[],
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
getTile,
|
|
178
|
+
getTileAt,
|
|
179
|
+
getNeighbors,
|
|
180
|
+
getDistance,
|
|
181
|
+
getHexesInRange,
|
|
182
|
+
axialToCube,
|
|
183
|
+
cubeToAxial,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
|
|
3
|
+
const MOBILE_BREAKPOINT = 768;
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Hook to detect if the user is on a mobile/small screen device
|
|
7
|
+
* @param breakpoint - The width threshold in pixels (default: 768)
|
|
8
|
+
* @returns boolean indicating if the screen is mobile-sized
|
|
9
|
+
*/
|
|
10
|
+
export function useIsMobile(breakpoint: number = MOBILE_BREAKPOINT): boolean {
|
|
11
|
+
const [isMobile, setIsMobile] = useState(() => {
|
|
12
|
+
// SSR-safe: default to false if window is not available
|
|
13
|
+
if (typeof window === "undefined") return false;
|
|
14
|
+
return window.innerWidth < breakpoint;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (typeof window === "undefined") return;
|
|
19
|
+
|
|
20
|
+
const mediaQuery = window.matchMedia(`(max-width: ${breakpoint - 1}px)`);
|
|
21
|
+
|
|
22
|
+
const handleChange = (e: MediaQueryListEvent | MediaQueryList) => {
|
|
23
|
+
setIsMobile(e.matches);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
// Set initial value
|
|
27
|
+
handleChange(mediaQuery);
|
|
28
|
+
|
|
29
|
+
// Listen for changes
|
|
30
|
+
mediaQuery.addEventListener("change", handleChange);
|
|
31
|
+
return () => mediaQuery.removeEventListener("change", handleChange);
|
|
32
|
+
}, [breakpoint]);
|
|
33
|
+
|
|
34
|
+
return isMobile;
|
|
35
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* usePanZoom hook - Unified pan and zoom gestures using @use-gesture/react
|
|
3
|
+
*
|
|
4
|
+
* Provides a declarative API for pan and zoom interactions on board components.
|
|
5
|
+
* Works with both SVG (via viewBox) and HTML (via CSS transforms) elements.
|
|
6
|
+
*
|
|
7
|
+
* Features:
|
|
8
|
+
* - Single finger/mouse drag for panning
|
|
9
|
+
* - Pinch-to-zoom on touch devices
|
|
10
|
+
* - Mouse wheel zoom on desktop
|
|
11
|
+
* - Configurable zoom limits
|
|
12
|
+
* - Optional momentum/inertia
|
|
13
|
+
*
|
|
14
|
+
* @example SVG usage (NetworkGraph, HexGrid, etc.)
|
|
15
|
+
* ```tsx
|
|
16
|
+
* const { transform, bind, resetTransform } = usePanZoom({
|
|
17
|
+
* enabled: enablePanZoom,
|
|
18
|
+
* minZoom: 0.5,
|
|
19
|
+
* maxZoom: 3,
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Apply to viewBox calculation
|
|
23
|
+
* const viewBoxWidth = contentWidth / transform.zoom;
|
|
24
|
+
* const viewBoxX = baseX - transform.pan.x;
|
|
25
|
+
*
|
|
26
|
+
* <svg {...bind()} style={{ touchAction: 'none' }}>
|
|
27
|
+
* ...
|
|
28
|
+
* </svg>
|
|
29
|
+
* ```
|
|
30
|
+
*
|
|
31
|
+
* @example HTML usage (SlotSystem, etc.)
|
|
32
|
+
* ```tsx
|
|
33
|
+
* const { transform, bind, style } = usePanZoom({
|
|
34
|
+
* enabled: enablePanZoom,
|
|
35
|
+
* mode: 'css',
|
|
36
|
+
* });
|
|
37
|
+
*
|
|
38
|
+
* <div {...bind()} style={{ ...style, touchAction: 'none' }}>
|
|
39
|
+
* ...
|
|
40
|
+
* </div>
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { useState, useCallback, useEffect, useMemo } from "react";
|
|
45
|
+
import { useGesture, type Handler } from "@use-gesture/react";
|
|
46
|
+
|
|
47
|
+
export interface PanZoomTransform {
|
|
48
|
+
/** Current zoom level (1 = 100%) */
|
|
49
|
+
zoom: number;
|
|
50
|
+
/** Current pan offset */
|
|
51
|
+
pan: { x: number; y: number };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UsePanZoomOptions {
|
|
55
|
+
/** Whether pan/zoom is enabled */
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
/** Initial zoom level */
|
|
58
|
+
initialZoom?: number;
|
|
59
|
+
/** Minimum zoom level */
|
|
60
|
+
minZoom?: number;
|
|
61
|
+
/** Maximum zoom level */
|
|
62
|
+
maxZoom?: number;
|
|
63
|
+
/** Initial pan offset */
|
|
64
|
+
initialPan?: { x: number; y: number };
|
|
65
|
+
/** Transform mode: 'viewbox' for SVG, 'css' for HTML elements */
|
|
66
|
+
mode?: "viewbox" | "css";
|
|
67
|
+
/** Zoom sensitivity for wheel events (default: 0.002) */
|
|
68
|
+
wheelSensitivity?: number;
|
|
69
|
+
/** Called when transform changes */
|
|
70
|
+
onTransformChange?: (transform: PanZoomTransform) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Type for gesture bind function that returns props to spread on element
|
|
74
|
+
* Note: We omit ref from the return type to avoid conflicts with explicit refs on elements.
|
|
75
|
+
* The gesture library handles its own internal ref binding.
|
|
76
|
+
*/
|
|
77
|
+
type GestureBindFunction = () => Omit<React.HTMLAttributes<Element>, "ref">;
|
|
78
|
+
|
|
79
|
+
export interface UsePanZoomReturn {
|
|
80
|
+
/** Current transform state */
|
|
81
|
+
transform: PanZoomTransform;
|
|
82
|
+
/** Gesture handlers to spread on the target element - always returns spreadable props */
|
|
83
|
+
bind: GestureBindFunction;
|
|
84
|
+
/** Reset transform to initial values */
|
|
85
|
+
resetTransform: () => void;
|
|
86
|
+
/** Set zoom programmatically */
|
|
87
|
+
setZoom: (zoom: number) => void;
|
|
88
|
+
/** Set pan programmatically */
|
|
89
|
+
setPan: (pan: { x: number; y: number }) => void;
|
|
90
|
+
/** CSS transform style (for mode: 'css') */
|
|
91
|
+
style: React.CSSProperties;
|
|
92
|
+
/** Whether currently dragging/panning */
|
|
93
|
+
isDragging: boolean;
|
|
94
|
+
/** Whether currently pinching */
|
|
95
|
+
isPinching: boolean;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Hook for pan and zoom gestures on board components
|
|
100
|
+
*/
|
|
101
|
+
export function usePanZoom(options: UsePanZoomOptions = {}): UsePanZoomReturn {
|
|
102
|
+
const {
|
|
103
|
+
enabled = true,
|
|
104
|
+
initialZoom = 1,
|
|
105
|
+
minZoom = 0.5,
|
|
106
|
+
maxZoom = 3,
|
|
107
|
+
initialPan = { x: 0, y: 0 },
|
|
108
|
+
mode = "viewbox",
|
|
109
|
+
wheelSensitivity = 0.002,
|
|
110
|
+
onTransformChange,
|
|
111
|
+
} = options;
|
|
112
|
+
|
|
113
|
+
const [zoom, setZoomState] = useState(initialZoom);
|
|
114
|
+
const [pan, setPanState] = useState(initialPan);
|
|
115
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
116
|
+
const [isPinching, setIsPinching] = useState(false);
|
|
117
|
+
|
|
118
|
+
// Clamp zoom to bounds
|
|
119
|
+
const clampZoom = useCallback(
|
|
120
|
+
(z: number) => Math.min(maxZoom, Math.max(minZoom, z)),
|
|
121
|
+
[minZoom, maxZoom],
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
// Update transform and notify
|
|
125
|
+
const updateTransform = useCallback(
|
|
126
|
+
(newZoom: number, newPan: { x: number; y: number }) => {
|
|
127
|
+
const clampedZoom = clampZoom(newZoom);
|
|
128
|
+
setZoomState(clampedZoom);
|
|
129
|
+
setPanState(newPan);
|
|
130
|
+
onTransformChange?.({ zoom: clampedZoom, pan: newPan });
|
|
131
|
+
},
|
|
132
|
+
[clampZoom, onTransformChange],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
// Gesture bindings
|
|
136
|
+
const bind = useGesture(
|
|
137
|
+
{
|
|
138
|
+
onDrag: (({
|
|
139
|
+
delta: [dx, dy],
|
|
140
|
+
active,
|
|
141
|
+
pinching,
|
|
142
|
+
}: Parameters<Handler<"drag">>[0]) => {
|
|
143
|
+
if (!enabled || pinching) return;
|
|
144
|
+
|
|
145
|
+
setIsDragging(active);
|
|
146
|
+
|
|
147
|
+
if (active) {
|
|
148
|
+
// For viewbox mode, we invert and scale the delta
|
|
149
|
+
// For CSS mode, we apply directly
|
|
150
|
+
const scaleFactor = mode === "viewbox" ? 1 / zoom : 1;
|
|
151
|
+
setPanState((prev) => ({
|
|
152
|
+
x: prev.x + dx * scaleFactor,
|
|
153
|
+
y: prev.y + dy * scaleFactor,
|
|
154
|
+
}));
|
|
155
|
+
}
|
|
156
|
+
}) as Handler<"drag">,
|
|
157
|
+
onPinch: (({
|
|
158
|
+
offset: [scale],
|
|
159
|
+
active,
|
|
160
|
+
}: Parameters<Handler<"pinch">>[0]) => {
|
|
161
|
+
if (!enabled) return;
|
|
162
|
+
|
|
163
|
+
setIsPinching(active);
|
|
164
|
+
|
|
165
|
+
if (active) {
|
|
166
|
+
const newZoom = clampZoom(scale);
|
|
167
|
+
setZoomState(newZoom);
|
|
168
|
+
onTransformChange?.({ zoom: newZoom, pan });
|
|
169
|
+
}
|
|
170
|
+
}) as Handler<"pinch">,
|
|
171
|
+
onWheel: (({ delta: [, dy], event }: Parameters<Handler<"wheel">>[0]) => {
|
|
172
|
+
if (!enabled) return;
|
|
173
|
+
|
|
174
|
+
event.preventDefault();
|
|
175
|
+
const newZoom = clampZoom(zoom - dy * wheelSensitivity);
|
|
176
|
+
setZoomState(newZoom);
|
|
177
|
+
onTransformChange?.({ zoom: newZoom, pan });
|
|
178
|
+
}) as Handler<"wheel">,
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
drag: {
|
|
182
|
+
enabled,
|
|
183
|
+
filterTaps: true,
|
|
184
|
+
},
|
|
185
|
+
pinch: {
|
|
186
|
+
enabled,
|
|
187
|
+
scaleBounds: { min: minZoom, max: maxZoom },
|
|
188
|
+
from: () => [zoom, 0],
|
|
189
|
+
},
|
|
190
|
+
wheel: {
|
|
191
|
+
enabled,
|
|
192
|
+
eventOptions: { passive: false },
|
|
193
|
+
},
|
|
194
|
+
},
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
// Reset to initial values
|
|
198
|
+
const resetTransform = useCallback(() => {
|
|
199
|
+
updateTransform(initialZoom, initialPan);
|
|
200
|
+
}, [initialZoom, initialPan, updateTransform]);
|
|
201
|
+
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
updateTransform(initialZoom, initialPan);
|
|
204
|
+
}, [initialZoom, initialPan.x, initialPan.y, updateTransform]);
|
|
205
|
+
|
|
206
|
+
// Programmatic setters
|
|
207
|
+
const setZoom = useCallback(
|
|
208
|
+
(newZoom: number) => {
|
|
209
|
+
updateTransform(clampZoom(newZoom), pan);
|
|
210
|
+
},
|
|
211
|
+
[pan, clampZoom, updateTransform],
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const setPan = useCallback(
|
|
215
|
+
(newPan: { x: number; y: number }) => {
|
|
216
|
+
updateTransform(zoom, newPan);
|
|
217
|
+
},
|
|
218
|
+
[zoom, updateTransform],
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// CSS transform style for HTML mode
|
|
222
|
+
const style = useMemo<React.CSSProperties>(
|
|
223
|
+
() =>
|
|
224
|
+
mode === "css"
|
|
225
|
+
? {
|
|
226
|
+
transform: `translate(${pan.x}px, ${pan.y}px) scale(${zoom})`,
|
|
227
|
+
transformOrigin: "center center",
|
|
228
|
+
}
|
|
229
|
+
: {},
|
|
230
|
+
[mode, pan.x, pan.y, zoom],
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
// Current transform state
|
|
234
|
+
const transform = useMemo<PanZoomTransform>(
|
|
235
|
+
() => ({ zoom, pan }),
|
|
236
|
+
[zoom, pan],
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// Wrap bind to ensure it always returns spreadable props (never void)
|
|
240
|
+
// We exclude 'ref' from the result to avoid type conflicts with explicit element refs
|
|
241
|
+
const safeBind: GestureBindFunction = useCallback(() => {
|
|
242
|
+
const result = bind();
|
|
243
|
+
// If bind returns void (shouldn't happen with our config), return empty object
|
|
244
|
+
// Destructure to omit ref, avoiding type conflicts with SVG/HTML element refs
|
|
245
|
+
|
|
246
|
+
const { ref: _ref, ...propsWithoutRef } = (result ??
|
|
247
|
+
{}) as React.HTMLAttributes<Element> & { ref?: unknown };
|
|
248
|
+
return propsWithoutRef;
|
|
249
|
+
}, [bind]);
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
transform,
|
|
253
|
+
bind: safeBind,
|
|
254
|
+
resetTransform,
|
|
255
|
+
setZoom,
|
|
256
|
+
setPan,
|
|
257
|
+
style,
|
|
258
|
+
isDragging,
|
|
259
|
+
isPinching,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Helper to calculate SVG viewBox with pan/zoom applied
|
|
265
|
+
*/
|
|
266
|
+
export function calculateViewBox(
|
|
267
|
+
bounds: { minX: number; minY: number; width: number; height: number },
|
|
268
|
+
transform: PanZoomTransform,
|
|
269
|
+
): string {
|
|
270
|
+
const viewBoxWidth = bounds.width / transform.zoom;
|
|
271
|
+
const viewBoxHeight = bounds.height / transform.zoom;
|
|
272
|
+
const viewBoxX =
|
|
273
|
+
bounds.minX + (bounds.width - viewBoxWidth) / 2 - transform.pan.x;
|
|
274
|
+
const viewBoxY =
|
|
275
|
+
bounds.minY + (bounds.height - viewBoxHeight) / 2 - transform.pan.y;
|
|
276
|
+
|
|
277
|
+
return `${viewBoxX} ${viewBoxY} ${viewBoxWidth} ${viewBoxHeight}`;
|
|
278
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { useCallback, useMemo } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
AnySquareBoardInput,
|
|
4
|
+
BoardSpaceIdOf,
|
|
5
|
+
NormalizedSquareBoard,
|
|
6
|
+
NormalizedSquareCellOf,
|
|
7
|
+
} from "../types/tiled-board.js";
|
|
8
|
+
import { normalizeSquareBoardInput } from "../types/tiled-board.js";
|
|
9
|
+
import { useBoardTopology } from "./useBoardTopology.js";
|
|
10
|
+
|
|
11
|
+
type NeighborMode = "orthogonal" | "diagonal" | "all";
|
|
12
|
+
type DistanceMetric = "manhattan" | "chebyshev";
|
|
13
|
+
|
|
14
|
+
export function useSquareBoard<const TBoard extends AnySquareBoardInput>(
|
|
15
|
+
board: TBoard,
|
|
16
|
+
) {
|
|
17
|
+
const normalizedBoard = useMemo<NormalizedSquareBoard<TBoard>>(
|
|
18
|
+
() => normalizeSquareBoardInput(board),
|
|
19
|
+
[board],
|
|
20
|
+
);
|
|
21
|
+
const topology = useBoardTopology(board);
|
|
22
|
+
|
|
23
|
+
const cellByCoordinate = useMemo(
|
|
24
|
+
() =>
|
|
25
|
+
new Map(
|
|
26
|
+
normalizedBoard.cells.map(
|
|
27
|
+
(cell) => [`${cell.row},${cell.col}`, cell] as const,
|
|
28
|
+
),
|
|
29
|
+
),
|
|
30
|
+
[normalizedBoard.cells],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const getCell = useCallback(
|
|
34
|
+
(cellId: BoardSpaceIdOf<TBoard>) => {
|
|
35
|
+
return topology.getSpace(cellId) as
|
|
36
|
+
| NormalizedSquareCellOf<TBoard>
|
|
37
|
+
| undefined;
|
|
38
|
+
},
|
|
39
|
+
[topology],
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const getCellAt = useCallback(
|
|
43
|
+
(row: number, col: number) => {
|
|
44
|
+
return cellByCoordinate.get(`${row},${col}`) as
|
|
45
|
+
| NormalizedSquareCellOf<TBoard>
|
|
46
|
+
| undefined;
|
|
47
|
+
},
|
|
48
|
+
[cellByCoordinate],
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
const getNeighbors = useCallback(
|
|
52
|
+
(cellId: BoardSpaceIdOf<TBoard>, mode: NeighborMode = "orthogonal") => {
|
|
53
|
+
const cell = getCell(cellId);
|
|
54
|
+
if (!cell) {
|
|
55
|
+
return [] as Array<NormalizedSquareCellOf<TBoard>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const offsets: ReadonlyArray<readonly [number, number]> =
|
|
59
|
+
mode === "diagonal"
|
|
60
|
+
? [
|
|
61
|
+
[-1, -1],
|
|
62
|
+
[-1, 1],
|
|
63
|
+
[1, -1],
|
|
64
|
+
[1, 1],
|
|
65
|
+
]
|
|
66
|
+
: mode === "all"
|
|
67
|
+
? [
|
|
68
|
+
[-1, 0],
|
|
69
|
+
[0, 1],
|
|
70
|
+
[1, 0],
|
|
71
|
+
[0, -1],
|
|
72
|
+
[-1, -1],
|
|
73
|
+
[-1, 1],
|
|
74
|
+
[1, -1],
|
|
75
|
+
[1, 1],
|
|
76
|
+
]
|
|
77
|
+
: [
|
|
78
|
+
[-1, 0],
|
|
79
|
+
[0, 1],
|
|
80
|
+
[1, 0],
|
|
81
|
+
[0, -1],
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
return offsets
|
|
85
|
+
.map(([rowOffset, colOffset]) =>
|
|
86
|
+
getCellAt(cell.row + rowOffset, cell.col + colOffset),
|
|
87
|
+
)
|
|
88
|
+
.filter(
|
|
89
|
+
(candidate): candidate is NormalizedSquareCellOf<TBoard> =>
|
|
90
|
+
candidate !== undefined,
|
|
91
|
+
);
|
|
92
|
+
},
|
|
93
|
+
[getCell, getCellAt],
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
const getDistance = useCallback(
|
|
97
|
+
(
|
|
98
|
+
fromCellId: BoardSpaceIdOf<TBoard>,
|
|
99
|
+
toCellId: BoardSpaceIdOf<TBoard>,
|
|
100
|
+
metric: DistanceMetric = "manhattan",
|
|
101
|
+
) => {
|
|
102
|
+
const fromCell = getCell(fromCellId);
|
|
103
|
+
const toCell = getCell(toCellId);
|
|
104
|
+
if (!fromCell || !toCell) {
|
|
105
|
+
return Number.POSITIVE_INFINITY;
|
|
106
|
+
}
|
|
107
|
+
const rowDistance = Math.abs(fromCell.row - toCell.row);
|
|
108
|
+
const colDistance = Math.abs(fromCell.col - toCell.col);
|
|
109
|
+
return metric === "chebyshev"
|
|
110
|
+
? Math.max(rowDistance, colDistance)
|
|
111
|
+
: rowDistance + colDistance;
|
|
112
|
+
},
|
|
113
|
+
[getCell],
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
...topology,
|
|
118
|
+
board: normalizedBoard,
|
|
119
|
+
getCell,
|
|
120
|
+
getCellAt,
|
|
121
|
+
getNeighbors,
|
|
122
|
+
getDistance,
|
|
123
|
+
};
|
|
124
|
+
}
|