@buley/hexgrid-3d 3.6.0 → 3.6.2

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.
@@ -10,6 +10,7 @@ import { setCustomAccentColor, clearCustomAccentColor, getCurrentAccentHex, getA
10
10
  import { decodeHTMLEntities } from '../lib/html-utils';
11
11
  import { getProxiedImageUrl } from '../utils/image-utils';
12
12
  import { gridItemToPhoto } from '../compat';
13
+ import { generateScreenSpaceSphericalHexGrid, getNormalizedScreenSpaceSphericalCoordinates, } from '../workers/hexgrid-math';
13
14
  // Fallback no-op logger at module scope (unused - component-level dlog is used instead)
14
15
  // Kept for backward compatibility but renamed to avoid shadowing confusion
15
16
  const noopLog = (..._args) => { };
@@ -1118,7 +1119,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
1118
1119
  if (hasSignificantCurvature) {
1119
1120
  // Use spherical hexagonal grid for compact 3D packing
1120
1121
  const densityMultiplier = workerDebug.sphericalDensity ?? 1.4;
1121
- const result = generateSphericalHexGrid(totalHexagons, screenWidth, screenHeight, curveUDeg, curveVDeg, densityMultiplier, effectiveHexRadius // Pass the actual hex radius so spacing matches drawing
1122
+ const result = generateScreenSpaceSphericalHexGrid(totalHexagons, screenWidth, screenHeight, curveUDeg, curveVDeg, densityMultiplier, effectiveHexRadius // Pass the actual hex radius so spacing matches drawing
1122
1123
  );
1123
1124
  gridMetadataRef.current = result.metadata;
1124
1125
  return result.positions;
@@ -1304,8 +1305,15 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
1304
1305
  const minY = gridBounds.minY;
1305
1306
  const w = gridBounds.width;
1306
1307
  const h = gridBounds.height;
1307
- const u = (x - minX) / Math.max(1e-6, w);
1308
- const v = (y - minY) / Math.max(1e-6, h);
1308
+ const currentGridMetadata = gridMetadataRef.current;
1309
+ const sphericalGridMetadata = currentGridMetadata.isSpherical
1310
+ ? currentGridMetadata
1311
+ : null;
1312
+ const sphericalCoordinates = sphericalGridMetadata && typeof idx === 'number'
1313
+ ? getNormalizedScreenSpaceSphericalCoordinates(idx, sphericalGridMetadata, curveUDeg)
1314
+ : null;
1315
+ const u = sphericalCoordinates?.u ?? (x - minX) / Math.max(1e-6, w);
1316
+ const v = sphericalCoordinates?.v ?? (y - minY) / Math.max(1e-6, h);
1309
1317
  // Clear, explicit spherical mapping
1310
1318
  const deg2rad = Math.PI / 180;
1311
1319
  // longitude: center u=0.5 -> lon=0
@@ -1432,14 +1440,17 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
1432
1440
  try {
1433
1441
  if (typeof idx === 'number' && Array.isArray(hexPositions) && hexPositions.length > idx) {
1434
1442
  // Find neighbor indices using grid geometry (pass isSpherical flag)
1435
- const isSpherical = gridMetadataRef.current?.isSpherical ?? false;
1443
+ const isSpherical = sphericalGridMetadata !== null;
1436
1444
  const neighborIndices = getNeighbors(idx, hexPositions, drawnHexRadius, isSpherical);
1437
1445
  const projNeighbors = [];
1438
1446
  for (const ni of neighborIndices) {
1439
1447
  const np = hexPositions[ni];
1440
1448
  // Map neighbor pixel to lon/lat using same gridBounds mapping
1441
- const nu = (np[0] - minX) / Math.max(1e-6, w);
1442
- const nv = (np[1] - minY) / Math.max(1e-6, h);
1449
+ const neighborCoordinates = sphericalGridMetadata
1450
+ ? getNormalizedScreenSpaceSphericalCoordinates(ni, sphericalGridMetadata, curveUDeg)
1451
+ : null;
1452
+ const nu = neighborCoordinates?.u ?? (np[0] - minX) / Math.max(1e-6, w);
1453
+ const nv = neighborCoordinates?.v ?? (np[1] - minY) / Math.max(1e-6, h);
1443
1454
  const nlon = (nu - 0.5) * (curveUDeg * deg2rad);
1444
1455
  const nlat = (nv - 0.5) * (curveVDeg * deg2rad);
1445
1456
  const cosLatN = Math.cos(nlat);
@@ -2855,7 +2866,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
2855
2866
  // Use the same projection helper as hit-tests so angles/positions match exactly.
2856
2867
  if (dbg?.renderBothSides) {
2857
2868
  try {
2858
- const anti = mapAndProject(hexPositions[index], true);
2869
+ const anti = mapAndProject(hexPositions[index], true, index);
2859
2870
  const antiScale = anti.scale || 1;
2860
2871
  const antiAngle = anti.angle || 0;
2861
2872
  drawHexagon(ctx, [anti.x, anti.y, 0], drawnHexRadius * antiScale, infection, textures, index, blankCount, smoothed, sheenProgress, sheenIntensity, sheenEnabled, scratchEnabled, scratchCanvasRef.current, seamInset, pulseProgress, false, true, antiAngle);
@@ -3295,7 +3306,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
3295
3306
  // If configured, also check the antipodal (inside) projection
3296
3307
  if (workerDebug.renderBothSides) {
3297
3308
  try {
3298
- const anti = mapAndProject(hexPositions[i], true);
3309
+ const anti = mapAndProject(hexPositions[i], true, i);
3299
3310
  const antiScale = anti.scale || 1;
3300
3311
  const antiRadius = drawnHexRadius * antiScale;
3301
3312
  const antiAngle = anti.angle || 0;
@@ -3420,7 +3431,7 @@ export const HexGrid = ({ items, photos: photosProp, onItemClick, onHexClick, sp
3420
3431
  }
3421
3432
  if (workerDebug.renderBothSides) {
3422
3433
  try {
3423
- const anti = mapAndProject(hexPositions[i], true);
3434
+ const anti = mapAndProject(hexPositions[i], true, i);
3424
3435
  const antiRadius = drawnHexRadius * (anti.scale || 1);
3425
3436
  const antiAngle = anti.angle || 0;
3426
3437
  if (isPointInHexagon(x, y, [anti.x, anti.y, 0], antiRadius, antiAngle)) {
@@ -4359,62 +4370,6 @@ function generatePixelScreen(cols, rows, hexRadius) {
4359
4370
  // No centering needed for canvas - positions are already in canvas coordinates
4360
4371
  return positions;
4361
4372
  }
4362
- // Generate hexagons directly in spherical space for compact 3D packing
4363
- function generateSphericalHexGrid(targetCount, screenWidth, screenHeight, curveUDeg, curveVDeg, densityMultiplier = 1.4, hexRadius // The actual hex radius used for drawing
4364
- ) {
4365
- const positions = [];
4366
- const sqrt3 = Math.sqrt(3);
4367
- // Calculate grid dimensions based on hex radius and screen size
4368
- // Use the SAME spacing math as the flat grid for consistency
4369
- const baseHorizontalSpacing = sqrt3 * hexRadius;
4370
- const baseVerticalSpacing = 1.5 * hexRadius;
4371
- // Calculate how many hexes fit based on actual hex size
4372
- const baseCols = Math.ceil(screenWidth / baseHorizontalSpacing);
4373
- const baseRows = Math.ceil(screenHeight / baseVerticalSpacing);
4374
- // Apply density multiplier to get more/fewer hexes
4375
- const cols = Math.ceil(baseCols * Math.sqrt(densityMultiplier));
4376
- const rows = Math.ceil(baseRows * Math.sqrt(densityMultiplier));
4377
- const deg2rad = Math.PI / 180;
4378
- // Use the same spacing for generation (already calculated above)
4379
- const verticalSpacing = baseVerticalSpacing;
4380
- const horizontalSpacing = baseHorizontalSpacing;
4381
- // Generate positions directly in lat/lon space with proper hexagonal offsets
4382
- // Apply adaptive density based on latitude to naturally handle pole convergence
4383
- for (let row = 0; row < rows; row++) {
4384
- // Use vertical spacing for proper hex packing
4385
- const y = row * verticalSpacing;
4386
- // Calculate latitude for this row to determine if we're near a pole
4387
- const v = y / Math.max(1, screenHeight);
4388
- // Clamp v to [0, 1] range
4389
- if (v < 0 || v > 1)
4390
- continue;
4391
- const lat = (v - 0.5) * (curveVDeg * deg2rad);
4392
- // Natural pole density reduction: fewer hexes per row near poles
4393
- // This is more physically accurate for spherical surfaces
4394
- const latFactor = Math.max(0.3, Math.abs(Math.cos(lat)));
4395
- const effectiveColsForRow = Math.max(3, Math.round(cols * latFactor));
4396
- // CRITICAL: Calculate hex offset based on the base spacing (not per-row spacing)
4397
- // This ensures proper nesting at all latitudes
4398
- const hexOffsetX = horizontalSpacing * 0.5;
4399
- for (let col = 0; col < effectiveColsForRow; col++) {
4400
- // Start with evenly distributed positions in screen space
4401
- let x = col * horizontalSpacing;
4402
- // Apply hexagonal offset for odd rows in SCREEN SPACE
4403
- // This ensures hexagons nestle properly between rows below
4404
- if (row % 2 !== 0) {
4405
- x += hexOffsetX;
4406
- }
4407
- // Wrap/clamp x to screen bounds
4408
- // For full 360° wrap, hexes can exceed screen width and will wrap in projection
4409
- // For partial coverage, clamp to screen
4410
- if (Math.abs(curveUDeg) < 359) {
4411
- x = Math.max(0, Math.min(screenWidth, x));
4412
- }
4413
- positions.push([x, y, 0]);
4414
- }
4415
- }
4416
- return { positions, metadata: { cols, rows, isSpherical: true } };
4417
- }
4418
4373
  function initializeInfectionSystem(positions, photos, hexRadius, initialClusterMax = 3, loggerParam = logger, isSpherical = false) {
4419
4374
  const logger = loggerParam;
4420
4375
  // GUARD: Validate inputs to prevent creating invalid infections
@@ -1,4 +1,9 @@
1
1
  export { HexGrid, default } from './HexGrid';
2
2
  export type { Photo } from './HexGrid';
3
3
  export type { HexGridProps } from '../types';
4
+ export { GameSphere } from './GameSphere';
5
+ export { buildPieceMesh, placePieceOnSphere, animatePiece, buildCellMesh, buildCellBorder, buildHighlightRing, buildFogOverlay, buildAttackTrail, buildOrbitalStrike, disposePieceGroup, applyCellState, } from './GamePieceRenderer';
6
+ export type { AttackAnimationConfig, OrbitalStrikeConfig } from './GamePieceRenderer';
7
+ export type { GamePiece, PieceShape, PieceAnimation, PieceAnimationConfig, CellGameState, CellHighlight, CellBorder, FogLevel, GameSphereProps, GameSphereConfig, GameSphereEvents, } from '../types';
8
+ export { HexTerritoryGlobe } from '../territory/HexTerritoryGlobe';
4
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAC7C,YAAY,EAAE,KAAK,EAAE,MAAM,WAAW,CAAC;AACvC,YAAY,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAG7C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EACL,cAAc,EACd,kBAAkB,EAClB,YAAY,EACZ,aAAa,EACb,eAAe,EACf,kBAAkB,EAClB,eAAe,EACf,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,GACf,MAAM,qBAAqB,CAAC;AAC7B,YAAY,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAC;AAGtF,YAAY,EACV,SAAS,EACT,UAAU,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,aAAa,EACb,UAAU,EACV,QAAQ,EACR,eAAe,EACf,gBAAgB,EAChB,gBAAgB,GACjB,MAAM,UAAU,CAAC;AAElB,OAAO,EAAE,iBAAiB,EAAE,MAAM,gCAAgC,CAAC"}
@@ -1 +1,5 @@
1
1
  export { HexGrid, default } from './HexGrid';
2
+ // Game piece rendering system
3
+ export { GameSphere } from './GameSphere';
4
+ export { buildPieceMesh, placePieceOnSphere, animatePiece, buildCellMesh, buildCellBorder, buildHighlightRing, buildFogOverlay, buildAttackTrail, buildOrbitalStrike, disposePieceGroup, applyCellState, } from './GamePieceRenderer';
5
+ export { HexTerritoryGlobe } from '../territory/HexTerritoryGlobe';
package/dist/index.d.ts CHANGED
@@ -9,4 +9,6 @@ export * from './algorithms';
9
9
  export * from './HexGridEnhanced';
10
10
  export * from './wasm';
11
11
  export * from './Snapshot';
12
+ export * from './territory';
13
+ export type { NarrationMessage } from './lib/narration';
12
14
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAG3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AAGpC,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAG5D,cAAc,QAAQ,CAAC;AAGvB,cAAc,cAAc,CAAC;AAG7B,cAAc,mBAAmB,CAAC;AAGlC,cAAc,QAAQ,CAAC;AAGvB,cAAc,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAG3B,cAAc,wBAAwB,CAAC;AACvC,cAAc,qBAAqB,CAAC;AAGpC,YAAY,EAAE,WAAW,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAG5D,cAAc,QAAQ,CAAC;AAGvB,cAAc,cAAc,CAAC;AAG7B,cAAc,mBAAmB,CAAC;AAGlC,cAAc,QAAQ,CAAC;AAGvB,cAAc,YAAY,CAAC;AAG3B,cAAc,aAAa,CAAC;AAC5B,YAAY,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
package/dist/index.js CHANGED
@@ -15,3 +15,5 @@ export * from './HexGridEnhanced';
15
15
  export * from './wasm';
16
16
  // Unified Snapshot API
17
17
  export * from './Snapshot';
18
+ // Territory globe exports
19
+ export * from './territory';
@@ -0,0 +1,18 @@
1
+ import React from 'react';
2
+ import { type HexTerritoryAffiliation, type HexTerritoryAllianceBinding, type HexTerritoryCell, type HexTerritoryRallyMarker } from './globe';
3
+ export interface HexTerritoryGlobeProps {
4
+ cells: HexTerritoryCell[];
5
+ selectedCellId?: string | null;
6
+ hoverCellId?: string | null;
7
+ claimedCellIds?: Iterable<string>;
8
+ lockedCellIds?: Iterable<string>;
9
+ colorsByCellId?: Record<string, string>;
10
+ affiliationByCellId?: Record<string, HexTerritoryAffiliation>;
11
+ allianceBindings?: HexTerritoryAllianceBinding[];
12
+ rallyMarkers?: HexTerritoryRallyMarker[];
13
+ tileRadius?: number;
14
+ onSelectCell?: (cell: HexTerritoryCell) => void;
15
+ onHoverCell?: (cell: HexTerritoryCell | null) => void;
16
+ }
17
+ export declare function HexTerritoryGlobe({ cells, selectedCellId, hoverCellId, claimedCellIds, lockedCellIds, colorsByCellId, affiliationByCellId, allianceBindings, rallyMarkers, tileRadius, onSelectCell, onHoverCell, }: HexTerritoryGlobeProps): React.JSX.Element;
18
+ //# sourceMappingURL=HexTerritoryGlobe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"HexTerritoryGlobe.d.ts","sourceRoot":"","sources":["../../src/territory/HexTerritoryGlobe.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAqC,MAAM,OAAO,CAAC;AAG1D,OAAO,EAEL,KAAK,uBAAuB,EAC5B,KAAK,2BAA2B,EAChC,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,EAC7B,MAAM,SAAS,CAAC;AAEjB,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAC1B,cAAc,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,cAAc,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IAClC,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACxC,mBAAmB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,uBAAuB,CAAC,CAAC;IAC9D,gBAAgB,CAAC,EAAE,2BAA2B,EAAE,CAAC;IACjD,YAAY,CAAC,EAAE,uBAAuB,EAAE,CAAC;IACzC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,KAAK,IAAI,CAAC;IAChD,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,gBAAgB,GAAG,IAAI,KAAK,IAAI,CAAC;CACvD;AAMD,wBAAgB,iBAAiB,CAAC,EAChC,KAAK,EACL,cAAqB,EACrB,WAAkB,EAClB,cAAc,EACd,aAAa,EACb,cAAc,EACd,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,UAAU,EACV,YAAY,EACZ,WAAW,GACZ,EAAE,sBAAsB,GAAG,KAAK,CAAC,GAAG,CAAC,OAAO,CAyJ5C"}
@@ -0,0 +1,106 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef } from 'react';
3
+ import { Color, CircleGeometry, Matrix4, Object3D } from 'three';
4
+ import { calculateAutoTileRadiusByRow, } from './globe';
5
+ function asSet(values) {
6
+ return values ? new Set(values) : new Set();
7
+ }
8
+ export function HexTerritoryGlobe({ cells, selectedCellId = null, hoverCellId = null, claimedCellIds, lockedCellIds, colorsByCellId, affiliationByCellId, allianceBindings, rallyMarkers, tileRadius, onSelectCell, onHoverCell, }) {
9
+ const meshRef = useRef(null);
10
+ const geometry = useMemo(() => new CircleGeometry(1, 6), []);
11
+ const workingObject = useMemo(() => new Object3D(), []);
12
+ const claimed = useMemo(() => asSet(claimedCellIds), [claimedCellIds]);
13
+ const locked = useMemo(() => asSet(lockedCellIds), [lockedCellIds]);
14
+ const cellById = useMemo(() => new Map(cells.map((cell) => [cell.cellId, cell])), [cells]);
15
+ const autoTileRadiusByRow = useMemo(() => tileRadius === undefined ? calculateAutoTileRadiusByRow(cells) : undefined, [cells, tileRadius]);
16
+ useEffect(() => {
17
+ const mesh = meshRef.current;
18
+ if (!mesh) {
19
+ return;
20
+ }
21
+ const matrix = new Matrix4();
22
+ cells.forEach((cell, index) => {
23
+ const point = cell.surfacePoint;
24
+ const effectiveTileRadius = tileRadius ?? autoTileRadiusByRow?.get(cell.rowIndex) ?? 0;
25
+ workingObject.position.set(point.x, point.y, point.z);
26
+ workingObject.lookAt(point.x * 2, point.y * 2, point.z * 2);
27
+ workingObject.scale.setScalar(effectiveTileRadius);
28
+ workingObject.updateMatrix();
29
+ matrix.copy(workingObject.matrix);
30
+ mesh.setMatrixAt(index, matrix);
31
+ const affiliation = affiliationByCellId?.[cell.cellId] ?? 'neutral';
32
+ const baseColor = locked.has(cell.cellId)
33
+ ? '#23345c'
34
+ : colorsByCellId?.[cell.cellId] ??
35
+ (selectedCellId === cell.cellId
36
+ ? '#7ee7ff'
37
+ : hoverCellId === cell.cellId
38
+ ? '#59d0ff'
39
+ : affiliation === 'self'
40
+ ? '#7ee7ff'
41
+ : affiliation === 'ally'
42
+ ? '#63f2c6'
43
+ : affiliation === 'hostile'
44
+ ? '#ff9675'
45
+ : claimed.has(cell.cellId)
46
+ ? '#63f2c6'
47
+ : '#1f2a4a');
48
+ mesh.setColorAt(index, new Color(baseColor));
49
+ });
50
+ mesh.instanceMatrix.needsUpdate = true;
51
+ if (mesh.instanceColor) {
52
+ mesh.instanceColor.needsUpdate = true;
53
+ }
54
+ }, [
55
+ cells,
56
+ claimed,
57
+ colorsByCellId,
58
+ affiliationByCellId,
59
+ hoverCellId,
60
+ locked,
61
+ selectedCellId,
62
+ tileRadius,
63
+ autoTileRadiusByRow,
64
+ workingObject,
65
+ ]);
66
+ return (_jsxs(_Fragment, { children: [_jsx("instancedMesh", { ref: meshRef, args: [geometry, undefined, cells.length], onClick: (event) => {
67
+ if (typeof event.instanceId !== 'number') {
68
+ return;
69
+ }
70
+ const cell = cells[event.instanceId];
71
+ if (cell) {
72
+ onSelectCell?.(cell);
73
+ }
74
+ }, onPointerMove: (event) => {
75
+ if (typeof event.instanceId !== 'number') {
76
+ onHoverCell?.(null);
77
+ return;
78
+ }
79
+ const cell = cells[event.instanceId];
80
+ onHoverCell?.(cell ?? null);
81
+ }, onPointerOut: () => {
82
+ onHoverCell?.(null);
83
+ }, children: _jsx("meshStandardMaterial", { transparent: true, opacity: 0.92, metalness: 0.14, roughness: 0.42 }) }), (rallyMarkers ?? []).map((marker) => {
84
+ const cell = cellById.get(marker.cellId);
85
+ if (!cell) {
86
+ return null;
87
+ }
88
+ const intensity = Math.max(0.35, Math.min(marker.intensity ?? 1, 2));
89
+ const scale = 0.028 * intensity;
90
+ const point = cell.surfacePoint;
91
+ return (_jsxs("mesh", { position: [point.x * 1.015, point.y * 1.015, point.z * 1.015], children: [_jsx("sphereGeometry", { args: [scale, 10, 10] }), _jsx("meshBasicMaterial", { color: marker.directive === 'surge'
92
+ ? '#ffc857'
93
+ : marker.directive === 'fortify'
94
+ ? '#7ee7ff'
95
+ : marker.directive === 'support'
96
+ ? '#63f2c6'
97
+ : '#c9d4ff' })] }, `${marker.cellId}:${marker.directive}`));
98
+ }), (allianceBindings ?? []).flatMap((binding) => binding.rootCellIds.map((cellId) => {
99
+ const cell = cellById.get(cellId);
100
+ if (!cell) {
101
+ return null;
102
+ }
103
+ const point = cell.surfacePoint;
104
+ return (_jsxs("mesh", { position: [point.x * 1.005, point.y * 1.005, point.z * 1.005], children: [_jsx("sphereGeometry", { args: [0.018, 8, 8] }), _jsx("meshBasicMaterial", { color: "#7ee7ff", transparent: true, opacity: 0.75 })] }, `${binding.phyleId}:${cellId}:halo`));
105
+ }))] }));
106
+ }
@@ -0,0 +1,66 @@
1
+ export interface CanonicalHexGlobeConfig {
2
+ boardId: string;
3
+ curveUDeg: number;
4
+ curveVDeg: number;
5
+ rowCount: number;
6
+ equatorColumns: number;
7
+ minimumColumnsPerRow: number;
8
+ poleMinScale: number;
9
+ sphereRadius?: number;
10
+ }
11
+ export type HexSubdivisionSegment = 0 | 1 | 2 | 3 | 4 | 5 | 6;
12
+ export type HexNodePath = HexSubdivisionSegment[];
13
+ export type HexwarEmbedProvider = 'youtube' | 'x' | 'instagram' | 'threads' | 'tiktok';
14
+ export interface HexwarEmbedRef {
15
+ provider: HexwarEmbedProvider;
16
+ submittedUrl: string;
17
+ canonicalUrl: string;
18
+ kind: 'video' | 'post' | 'thread' | 'short' | 'reel';
19
+ title?: string;
20
+ authorName?: string;
21
+ thumbnailUrl?: string;
22
+ embedAllowed: boolean;
23
+ }
24
+ export interface HexTerritoryTickState {
25
+ stage: 'dormant' | 'active' | 'surging' | 'entrenched' | 'fading';
26
+ energy: number;
27
+ pressure: number;
28
+ cohesion: number;
29
+ lastResolvedAt: number;
30
+ }
31
+ export type HexTerritoryAffiliation = 'self' | 'ally' | 'neutral' | 'hostile';
32
+ export interface HexTerritoryAllianceBinding {
33
+ phyleId: string;
34
+ rootCellIds: string[];
35
+ displayName?: string;
36
+ }
37
+ export interface HexTerritoryRallyMarker {
38
+ cellId: string;
39
+ directive: 'fortify' | 'surge' | 'watch' | 'support';
40
+ phyleId?: string;
41
+ intensity?: number;
42
+ }
43
+ export interface HexTerritoryCellPoint {
44
+ x: number;
45
+ y: number;
46
+ z: number;
47
+ }
48
+ export interface HexTerritoryCell {
49
+ cellId: string;
50
+ rowIndex: number;
51
+ columnIndex: number;
52
+ columnCount: number;
53
+ lat: number;
54
+ lon: number;
55
+ surfacePoint: HexTerritoryCellPoint;
56
+ neighborCellIds: string[];
57
+ }
58
+ export interface HexTerritoryBoard {
59
+ boardId: string;
60
+ config: CanonicalHexGlobeConfig;
61
+ configHash: string;
62
+ cells: HexTerritoryCell[];
63
+ }
64
+ export declare function generateCanonicalHexGlobe(config: CanonicalHexGlobeConfig): HexTerritoryBoard;
65
+ export declare function calculateAutoTileRadiusByRow(cells: readonly HexTerritoryCell[]): Map<number, number>;
66
+ //# sourceMappingURL=globe.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"globe.d.ts","sourceRoot":"","sources":["../../src/territory/globe.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;IACvB,oBAAoB,EAAE,MAAM,CAAC;IAC7B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,qBAAqB,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC9D,MAAM,MAAM,WAAW,GAAG,qBAAqB,EAAE,CAAC;AAElD,MAAM,MAAM,mBAAmB,GAC3B,SAAS,GACT,GAAG,GACH,WAAW,GACX,SAAS,GACT,QAAQ,CAAC;AAEb,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,mBAAmB,CAAC;IAC9B,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CAAC;IACrD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,qBAAqB;IACpC,KAAK,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IAClE,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,cAAc,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,MAAM,uBAAuB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,uBAAuB;IACtC,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,SAAS,CAAC;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,qBAAqB;IACpC,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;IACV,CAAC,EAAE,MAAM,CAAC;CACX;AAED,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,EAAE,qBAAqB,CAAC;IACpC,eAAe,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,uBAAuB,CAAC;IAChC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC3B;AA2HD,wBAAgB,yBAAyB,CACvC,MAAM,EAAE,uBAAuB,GAC9B,iBAAiB,CAoDnB;AAED,wBAAgB,4BAA4B,CAC1C,KAAK,EAAE,SAAS,gBAAgB,EAAE,GACjC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAiErB"}
@@ -0,0 +1,180 @@
1
+ const boardCache = new Map();
2
+ const HEX_HORIZONTAL_RADIUS_RATIO = Math.sqrt(3);
3
+ const HEX_VERTICAL_RADIUS_RATIO = 1.5;
4
+ const TERRITORY_TILE_RADIUS_SAFETY_MARGIN = 0.96;
5
+ function toRadians(value) {
6
+ return (value * Math.PI) / 180;
7
+ }
8
+ function normalizeLongitude(value) {
9
+ let next = value;
10
+ while (next < -180) {
11
+ next += 360;
12
+ }
13
+ while (next >= 180) {
14
+ next -= 360;
15
+ }
16
+ return next;
17
+ }
18
+ function buildConfigHash(config) {
19
+ return [
20
+ config.boardId,
21
+ config.curveUDeg,
22
+ config.curveVDeg,
23
+ config.rowCount,
24
+ config.equatorColumns,
25
+ config.minimumColumnsPerRow,
26
+ config.poleMinScale,
27
+ config.sphereRadius ?? 1,
28
+ ].join(':');
29
+ }
30
+ function toSurfacePoint(lat, lon, sphereRadius) {
31
+ const latRadians = toRadians(lat);
32
+ const lonRadians = toRadians(lon);
33
+ const cosLat = Math.cos(latRadians);
34
+ return {
35
+ x: sphereRadius * cosLat * Math.cos(lonRadians),
36
+ y: sphereRadius * Math.sin(latRadians),
37
+ z: sphereRadius * cosLat * Math.sin(lonRadians),
38
+ };
39
+ }
40
+ function shortestWrappedDistance(a, b) {
41
+ const delta = Math.abs(a - b);
42
+ return Math.min(delta, 360 - delta);
43
+ }
44
+ function pointDistance(left, right) {
45
+ const dx = left.x - right.x;
46
+ const dy = left.y - right.y;
47
+ const dz = left.z - right.z;
48
+ return Math.sqrt(dx * dx + dy * dy + dz * dz);
49
+ }
50
+ function columnCountForLatitude(latitude, config) {
51
+ const cosScale = Math.abs(Math.cos(toRadians(latitude)));
52
+ const scaled = Math.max(config.poleMinScale, cosScale);
53
+ return Math.max(config.minimumColumnsPerRow, Math.round(config.equatorColumns * scaled));
54
+ }
55
+ function findClosestColumns(rowCells, lon) {
56
+ const ranked = rowCells
57
+ .map((cell) => ({
58
+ cell,
59
+ distance: shortestWrappedDistance(cell.lon, lon),
60
+ }))
61
+ .sort((left, right) => left.distance - right.distance);
62
+ return ranked.slice(0, Math.min(3, ranked.length)).map((item) => item.cell);
63
+ }
64
+ function buildNeighbors(cell, rows) {
65
+ const currentRow = rows[cell.rowIndex] ?? [];
66
+ const neighborIds = new Set();
67
+ const sameRowCount = currentRow.length;
68
+ if (sameRowCount > 1) {
69
+ const left = currentRow[(cell.columnIndex - 1 + sameRowCount) % sameRowCount] ?? null;
70
+ const right = currentRow[(cell.columnIndex + 1) % sameRowCount] ?? null;
71
+ if (left) {
72
+ neighborIds.add(left.cellId);
73
+ }
74
+ if (right) {
75
+ neighborIds.add(right.cellId);
76
+ }
77
+ }
78
+ const adjacentRows = [cell.rowIndex - 1, cell.rowIndex + 1];
79
+ for (const rowIndex of adjacentRows) {
80
+ const row = rows[rowIndex] ?? [];
81
+ for (const adjacent of findClosestColumns(row, cell.lon)) {
82
+ neighborIds.add(adjacent.cellId);
83
+ }
84
+ }
85
+ return Array.from(neighborIds);
86
+ }
87
+ export function generateCanonicalHexGlobe(config) {
88
+ const configHash = buildConfigHash(config);
89
+ const cached = boardCache.get(configHash);
90
+ if (cached) {
91
+ return cached;
92
+ }
93
+ const sphereRadius = config.sphereRadius ?? 1;
94
+ const rows = [];
95
+ for (let rowIndex = 0; rowIndex < config.rowCount; rowIndex += 1) {
96
+ const latitudeProgress = config.rowCount <= 1 ? 0.5 : (rowIndex + 0.5) / config.rowCount;
97
+ const lat = -90 + latitudeProgress * 180;
98
+ const columnCount = columnCountForLatitude(lat, config);
99
+ const lonStep = 360 / columnCount;
100
+ const lonOffset = rowIndex % 2 === 0 ? 0 : lonStep / 2;
101
+ const rowCells = [];
102
+ for (let columnIndex = 0; columnIndex < columnCount; columnIndex += 1) {
103
+ const lon = normalizeLongitude(-180 + lonOffset + columnIndex * lonStep + lonStep / 2);
104
+ rowCells.push({
105
+ cellId: `${config.boardId}:r${rowIndex}:c${columnIndex}`,
106
+ rowIndex,
107
+ columnIndex,
108
+ columnCount,
109
+ lat,
110
+ lon,
111
+ surfacePoint: toSurfacePoint(lat, lon, sphereRadius),
112
+ neighborCellIds: [],
113
+ });
114
+ }
115
+ rows.push(rowCells);
116
+ }
117
+ for (const row of rows) {
118
+ for (const cell of row) {
119
+ cell.neighborCellIds = buildNeighbors(cell, rows);
120
+ }
121
+ }
122
+ const board = {
123
+ boardId: config.boardId,
124
+ config,
125
+ configHash,
126
+ cells: rows.flat(),
127
+ };
128
+ boardCache.set(configHash, board);
129
+ return board;
130
+ }
131
+ export function calculateAutoTileRadiusByRow(cells) {
132
+ const rowMap = new Map();
133
+ const cellById = new Map();
134
+ for (const cell of cells) {
135
+ const row = rowMap.get(cell.rowIndex);
136
+ if (row) {
137
+ row.push(cell);
138
+ }
139
+ else {
140
+ rowMap.set(cell.rowIndex, [cell]);
141
+ }
142
+ cellById.set(cell.cellId, cell);
143
+ }
144
+ const radiusByRow = new Map();
145
+ for (const [rowIndex, unsortedRow] of rowMap.entries()) {
146
+ const row = [...unsortedRow].sort((left, right) => left.columnIndex - right.columnIndex);
147
+ let minimumSameRowDistance = Number.POSITIVE_INFINITY;
148
+ let minimumAdjacentRowDistance = Number.POSITIVE_INFINITY;
149
+ if (row.length > 1) {
150
+ for (let columnIndex = 0; columnIndex < row.length; columnIndex += 1) {
151
+ const current = row[columnIndex];
152
+ const next = row[(columnIndex + 1) % row.length];
153
+ if (current && next) {
154
+ minimumSameRowDistance = Math.min(minimumSameRowDistance, pointDistance(current.surfacePoint, next.surfacePoint));
155
+ }
156
+ }
157
+ }
158
+ for (const cell of row) {
159
+ for (const neighborId of cell.neighborCellIds) {
160
+ const neighbor = cellById.get(neighborId);
161
+ if (!neighbor || Math.abs(neighbor.rowIndex - rowIndex) !== 1) {
162
+ continue;
163
+ }
164
+ minimumAdjacentRowDistance = Math.min(minimumAdjacentRowDistance, pointDistance(cell.surfacePoint, neighbor.surfacePoint));
165
+ }
166
+ }
167
+ const horizontalRadius = Number.isFinite(minimumSameRowDistance)
168
+ ? minimumSameRowDistance / HEX_HORIZONTAL_RADIUS_RATIO
169
+ : Number.POSITIVE_INFINITY;
170
+ const verticalRadius = Number.isFinite(minimumAdjacentRowDistance)
171
+ ? minimumAdjacentRowDistance / HEX_VERTICAL_RADIUS_RATIO
172
+ : Number.POSITIVE_INFINITY;
173
+ const unconstrainedRadius = Math.min(horizontalRadius, verticalRadius);
174
+ const safeRadius = Number.isFinite(unconstrainedRadius) && unconstrainedRadius > 0
175
+ ? unconstrainedRadius * TERRITORY_TILE_RADIUS_SAFETY_MARGIN
176
+ : 0;
177
+ radiusByRow.set(rowIndex, safeRadius);
178
+ }
179
+ return radiusByRow;
180
+ }
@@ -0,0 +1,4 @@
1
+ export * from './globe';
2
+ export * from './narration';
3
+ export * from './HexTerritoryGlobe';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/territory/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,aAAa,CAAC;AAC5B,cAAc,qBAAqB,CAAC"}
@@ -0,0 +1,3 @@
1
+ export * from './globe';
2
+ export * from './narration';
3
+ export * from './HexTerritoryGlobe';
@@ -0,0 +1,12 @@
1
+ import type { NarrationMessage } from '../lib/narration';
2
+ export type HexwarNarrationEventType = 'claim' | 'territory_captured' | 'embed_set' | 'delegation_issued' | 'delegation_revoked' | 'nexus_created' | 'nexus_connected' | 'alliance_root_bound' | 'rally_called' | 'allied_border_held' | 'alliance_surged' | 'unlock' | 'tick_surged' | 'tick_entrenched' | 'leaderboard_flip';
3
+ export interface HexwarNarrationEvent {
4
+ id: string;
5
+ eventType: HexwarNarrationEventType;
6
+ occurredAtMs: number;
7
+ actorName?: string;
8
+ cellId?: string;
9
+ details?: Record<string, string | number | boolean | null | undefined>;
10
+ }
11
+ export declare function createHexwarNarrationAdapter(events: HexwarNarrationEvent[]): NarrationMessage[];
12
+ //# sourceMappingURL=narration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"narration.d.ts","sourceRoot":"","sources":["../../src/territory/narration.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEzD,MAAM,MAAM,wBAAwB,GAChC,OAAO,GACP,oBAAoB,GACpB,WAAW,GACX,mBAAmB,GACnB,oBAAoB,GACpB,eAAe,GACf,iBAAiB,GACjB,qBAAqB,GACrB,cAAc,GACd,oBAAoB,GACpB,iBAAiB,GACjB,QAAQ,GACR,aAAa,GACb,iBAAiB,GACjB,kBAAkB,CAAC;AAEvB,MAAM,WAAW,oBAAoB;IACnC,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,wBAAwB,CAAC;IACpC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;CACxE;AAyED,wBAAgB,4BAA4B,CAC1C,MAAM,EAAE,oBAAoB,EAAE,GAC7B,gBAAgB,EAAE,CAmDpB"}