@almadar/ui 5.98.0 → 5.99.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.
@@ -1872,6 +1872,7 @@ function SideScene({
1872
1872
  var UNIT_BASE_MODEL_SCALE = 0.5;
1873
1873
  var UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
1874
1874
  var UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
1875
+ var TILE_TOP_OFFSET = 0.1;
1875
1876
  var EVENT_COLORS = {
1876
1877
  hit: "#ff5544",
1877
1878
  damage: "#ff5544",
@@ -1998,12 +1999,14 @@ function DefaultUnit({
1998
1999
  }
1999
2000
  ) })
2000
2001
  ) : model?.url ? (
2001
- /* GLB unit model — LOLO's `animation` field drives the named clip */
2002
+ /* GLB unit model — LOLO's `animation` field drives the named clip;
2003
+ `heading` (radians) faces the model along its travel direction (driving). */
2002
2004
  /* @__PURE__ */ jsxRuntime.jsx(
2003
2005
  ModelLoader,
2004
2006
  {
2005
2007
  url: model.url,
2006
2008
  scale: modelScale,
2009
+ rotation: [0, unit.heading ?? 0, 0],
2007
2010
  animation: unit.animation ?? "idle",
2008
2011
  fallbackGeometry: "box",
2009
2012
  castShadow: true
@@ -2394,6 +2397,16 @@ var GameCanvas3D = React8.forwardRef(
2394
2397
  }
2395
2398
  return cameraTarget;
2396
2399
  }, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
2400
+ const followOffset = React8.useMemo(() => {
2401
+ if (cameraMode === "chase") {
2402
+ const selected = units.find((u) => u.id === selectedUnitId);
2403
+ const h = selected?.heading ?? 0;
2404
+ const dist = 7;
2405
+ const height = 4;
2406
+ return [-Math.sin(h) * dist, height, -Math.cos(h) * dist];
2407
+ }
2408
+ return player ? [0, 2, 9] : [5, 7, 5];
2409
+ }, [cameraMode, units, selectedUnitId, player]);
2397
2410
  if (externalLoading || assetsLoading && preloadAssets.length > 0) {
2398
2411
  return /* @__PURE__ */ jsxRuntime.jsx(
2399
2412
  Canvas3DLoadingState,
@@ -2446,11 +2459,11 @@ var GameCanvas3D = React8.forwardRef(
2446
2459
  },
2447
2460
  children: [
2448
2461
  /* @__PURE__ */ jsxRuntime.jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
2449
- cameraMode === "follow" && /* @__PURE__ */ jsxRuntime.jsx(
2462
+ (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsxRuntime.jsx(
2450
2463
  FollowCamera,
2451
2464
  {
2452
2465
  target: followTarget,
2453
- offset: player ? [0, 2, 9] : [5, 7, 5]
2466
+ offset: followOffset
2454
2467
  }
2455
2468
  ),
2456
2469
  /* @__PURE__ */ jsxRuntime.jsx("ambientLight", { intensity: 0.6 }),
@@ -2536,7 +2549,7 @@ var GameCanvas3D = React8.forwardRef(
2536
2549
  const position = gridToWorld2(
2537
2550
  feature.x,
2538
2551
  feature.z ?? feature.y ?? 0,
2539
- (feature.elevation ?? 0) + 0.5
2552
+ (feature.elevation ?? 0) + TILE_TOP_OFFSET
2540
2553
  );
2541
2554
  const key = feature.id ?? `feature-${index}`;
2542
2555
  if (CustomFeatureRenderer) {
@@ -2557,7 +2570,7 @@ var GameCanvas3D = React8.forwardRef(
2557
2570
  const position = gridToWorld2(
2558
2571
  unit.x ?? unit.position?.x ?? 0,
2559
2572
  unit.z ?? unit.y ?? unit.position?.y ?? 0,
2560
- (unit.elevation ?? 0) + 0.5
2573
+ (unit.elevation ?? 0) + TILE_TOP_OFFSET
2561
2574
  );
2562
2575
  return /* @__PURE__ */ jsxRuntime.jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: CustomUnitRenderer ? /* @__PURE__ */ jsxRuntime.jsx(CustomUnitRenderer, { unit, position: [0, 0, 0] }) : /* @__PURE__ */ jsxRuntime.jsx(
2563
2576
  DefaultUnit,
@@ -2590,7 +2603,7 @@ var GameCanvas3D = React8.forwardRef(
2590
2603
  drei.OrbitControls,
2591
2604
  {
2592
2605
  ref: controlsRef,
2593
- enabled: cameraMode !== "follow",
2606
+ enabled: cameraMode !== "follow" && cameraMode !== "chase",
2594
2607
  target: cameraTarget,
2595
2608
  enableDamping: true,
2596
2609
  dampingFactor: 0.05,
@@ -1851,6 +1851,7 @@ function SideScene({
1851
1851
  var UNIT_BASE_MODEL_SCALE = 0.5;
1852
1852
  var UNIT_BASE_BILLBOARD_HEIGHT = 1.2;
1853
1853
  var UNIT_BASE_PRIMITIVE_RADIUS = 0.3;
1854
+ var TILE_TOP_OFFSET = 0.1;
1854
1855
  var EVENT_COLORS = {
1855
1856
  hit: "#ff5544",
1856
1857
  damage: "#ff5544",
@@ -1977,12 +1978,14 @@ function DefaultUnit({
1977
1978
  }
1978
1979
  ) })
1979
1980
  ) : model?.url ? (
1980
- /* GLB unit model — LOLO's `animation` field drives the named clip */
1981
+ /* GLB unit model — LOLO's `animation` field drives the named clip;
1982
+ `heading` (radians) faces the model along its travel direction (driving). */
1981
1983
  /* @__PURE__ */ jsx(
1982
1984
  ModelLoader,
1983
1985
  {
1984
1986
  url: model.url,
1985
1987
  scale: modelScale,
1988
+ rotation: [0, unit.heading ?? 0, 0],
1986
1989
  animation: unit.animation ?? "idle",
1987
1990
  fallbackGeometry: "box",
1988
1991
  castShadow: true
@@ -2373,6 +2376,16 @@ var GameCanvas3D = forwardRef(
2373
2376
  }
2374
2377
  return cameraTarget;
2375
2378
  }, [player, pixelsPerUnit, worldHeight, units, selectedUnitId, gridBounds, gridConfig, cameraTarget]);
2379
+ const followOffset = useMemo(() => {
2380
+ if (cameraMode === "chase") {
2381
+ const selected = units.find((u) => u.id === selectedUnitId);
2382
+ const h = selected?.heading ?? 0;
2383
+ const dist = 7;
2384
+ const height = 4;
2385
+ return [-Math.sin(h) * dist, height, -Math.cos(h) * dist];
2386
+ }
2387
+ return player ? [0, 2, 9] : [5, 7, 5];
2388
+ }, [cameraMode, units, selectedUnitId, player]);
2376
2389
  if (externalLoading || assetsLoading && preloadAssets.length > 0) {
2377
2390
  return /* @__PURE__ */ jsx(
2378
2391
  Canvas3DLoadingState,
@@ -2425,11 +2438,11 @@ var GameCanvas3D = forwardRef(
2425
2438
  },
2426
2439
  children: [
2427
2440
  /* @__PURE__ */ jsx(CameraController, { onCameraChange: eventHandlers.handleCameraChange }),
2428
- cameraMode === "follow" && /* @__PURE__ */ jsx(
2441
+ (cameraMode === "follow" || cameraMode === "chase") && /* @__PURE__ */ jsx(
2429
2442
  FollowCamera,
2430
2443
  {
2431
2444
  target: followTarget,
2432
- offset: player ? [0, 2, 9] : [5, 7, 5]
2445
+ offset: followOffset
2433
2446
  }
2434
2447
  ),
2435
2448
  /* @__PURE__ */ jsx("ambientLight", { intensity: 0.6 }),
@@ -2515,7 +2528,7 @@ var GameCanvas3D = forwardRef(
2515
2528
  const position = gridToWorld2(
2516
2529
  feature.x,
2517
2530
  feature.z ?? feature.y ?? 0,
2518
- (feature.elevation ?? 0) + 0.5
2531
+ (feature.elevation ?? 0) + TILE_TOP_OFFSET
2519
2532
  );
2520
2533
  const key = feature.id ?? `feature-${index}`;
2521
2534
  if (CustomFeatureRenderer) {
@@ -2536,7 +2549,7 @@ var GameCanvas3D = forwardRef(
2536
2549
  const position = gridToWorld2(
2537
2550
  unit.x ?? unit.position?.x ?? 0,
2538
2551
  unit.z ?? unit.y ?? unit.position?.y ?? 0,
2539
- (unit.elevation ?? 0) + 0.5
2552
+ (unit.elevation ?? 0) + TILE_TOP_OFFSET
2540
2553
  );
2541
2554
  return /* @__PURE__ */ jsx(LerpedGroup, { target: position, enabled: interpolateUnits, children: CustomUnitRenderer ? /* @__PURE__ */ jsx(CustomUnitRenderer, { unit, position: [0, 0, 0] }) : /* @__PURE__ */ jsx(
2542
2555
  DefaultUnit,
@@ -2569,7 +2582,7 @@ var GameCanvas3D = forwardRef(
2569
2582
  OrbitControls,
2570
2583
  {
2571
2584
  ref: controlsRef,
2572
- enabled: cameraMode !== "follow",
2585
+ enabled: cameraMode !== "follow" && cameraMode !== "chase",
2573
2586
  target: cameraTarget,
2574
2587
  enableDamping: true,
2575
2588
  dampingFactor: 0.05,
@@ -31,8 +31,10 @@ export interface GameEvent {
31
31
  y?: number;
32
32
  message?: string;
33
33
  }
34
- /** Camera mode for 3D view. `follow` tracks the side-view player (or the selected unit). */
35
- export type CameraMode = 'isometric' | 'perspective' | 'top-down' | 'follow';
34
+ /** Camera mode for 3D view.
35
+ * - `follow` tracks the side-view player (or the selected unit) from a fixed offset.
36
+ * - `chase` sits behind + above the followed unit relative to its `heading` (driving). */
37
+ export type CameraMode = 'isometric' | 'perspective' | 'top-down' | 'follow' | 'chase';
36
38
  /** Per-role model manifest — the 3D analogue of Canvas2D's assetManifest (values are GLB Assets). */
37
39
  export interface GameCanvas3DAssetManifest {
38
40
  terrains?: Record<string, Asset>;
@@ -99,6 +99,9 @@ export type IsometricUnit = {
99
99
  animation?: AnimationName;
100
100
  /** Frame index within the animation — driven by the LOLO state machine; renderer defaults to 0 */
101
101
  frame?: number;
102
+ /** Y-axis heading in radians (3D) — travel/facing direction. Orients the unit model and
103
+ * drives the `chase` camera (behind + above, looking along heading). Driving boards set this. */
104
+ heading?: number;
102
105
  };
103
106
  /** A map feature (resource, building, portal, etc.) */
104
107
  export type IsometricFeature = {
@@ -120,6 +123,8 @@ export type IsometricFeature = {
120
123
  color?: string;
121
124
  /** Elevation offset for 3D rendering */
122
125
  elevation?: number;
126
+ /** Y-axis rotation in radians (3D) — orients directional features (arches, signs, docks). */
127
+ rotation?: number;
123
128
  };
124
129
  /** Camera state for pan/zoom */
125
130
  export interface CameraState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@almadar/ui",
3
- "version": "5.98.0",
3
+ "version": "5.99.0",
4
4
  "description": "React UI components, hooks, and providers for Almadar",
5
5
  "type": "module",
6
6
  "sideEffects": [