@almadar/ui 5.71.0 → 5.73.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.
Files changed (55) hide show
  1. package/dist/avl/index.cjs +2178 -2309
  2. package/dist/avl/index.js +912 -1043
  3. package/dist/components/game/atoms/ActionButton.d.ts +3 -3
  4. package/dist/components/game/atoms/ChoiceButton.d.ts +3 -3
  5. package/dist/components/game/atoms/ComboCounter.d.ts +3 -3
  6. package/dist/components/game/atoms/ControlButton.d.ts +3 -3
  7. package/dist/components/game/atoms/DamageNumber.d.ts +3 -3
  8. package/dist/components/game/atoms/DialogueBubble.d.ts +3 -3
  9. package/dist/components/game/atoms/ItemSlot.d.ts +3 -3
  10. package/dist/components/game/atoms/ResourceCounter.d.ts +3 -3
  11. package/dist/components/game/atoms/ScoreDisplay.d.ts +4 -6
  12. package/dist/components/game/atoms/Sprite.d.ts +9 -21
  13. package/dist/components/game/atoms/StateIndicator.d.ts +2 -2
  14. package/dist/components/game/atoms/StatusEffect.d.ts +3 -3
  15. package/dist/components/game/atoms/TurnIndicator.d.ts +3 -3
  16. package/dist/components/game/atoms/WaypointMarker.d.ts +3 -3
  17. package/dist/components/game/atoms/XPBar.d.ts +1 -1
  18. package/dist/components/game/molecules/CardHand.d.ts +2 -2
  19. package/dist/components/game/molecules/DialogueBox.d.ts +3 -3
  20. package/dist/components/game/molecules/EnemyPlate.d.ts +3 -3
  21. package/dist/components/game/molecules/GameCanvas2D.d.ts +3 -3
  22. package/dist/components/game/molecules/GameCanvas3D.d.ts +0 -2
  23. package/dist/components/game/molecules/HealthPanel.d.ts +3 -3
  24. package/dist/components/game/molecules/IsometricCanvas.d.ts +11 -13
  25. package/dist/components/game/molecules/PlatformerCanvas.d.ts +4 -1
  26. package/dist/components/game/molecules/PowerupSlots.d.ts +3 -3
  27. package/dist/components/game/molecules/StatBadge.d.ts +6 -4
  28. package/dist/components/game/molecules/TurnPanel.d.ts +3 -3
  29. package/dist/components/game/molecules/three/index.cjs +55 -109
  30. package/dist/components/game/molecules/three/index.js +55 -109
  31. package/dist/components/game/molecules/useUnitSpriteAtlas.d.ts +6 -12
  32. package/dist/components/game/organisms/BattleBoard.d.ts +5 -6
  33. package/dist/components/game/organisms/CardBattlerBoard.d.ts +3 -4
  34. package/dist/components/game/organisms/CastleBoard.d.ts +5 -6
  35. package/dist/components/game/organisms/CityBuilderBoard.d.ts +6 -7
  36. package/dist/components/game/organisms/FishingBoard.d.ts +4 -6
  37. package/dist/components/game/organisms/PlatformerBoard.d.ts +5 -5
  38. package/dist/components/game/organisms/RoguelikeBoard.d.ts +4 -5
  39. package/dist/components/game/organisms/TopDownShooterBoard.d.ts +5 -6
  40. package/dist/components/game/organisms/TowerDefenseBoard.d.ts +5 -6
  41. package/dist/components/game/organisms/VisualNovelBoard.d.ts +4 -5
  42. package/dist/components/game/organisms/WorldMapBoard.d.ts +5 -6
  43. package/dist/components/game/organisms/types/effects.d.ts +7 -8
  44. package/dist/components/game/organisms/types/isometric.d.ts +24 -8
  45. package/dist/components/game/organisms/types/spriteAnimation.d.ts +3 -4
  46. package/dist/components/game/organisms/utils/makeAsset.d.ts +5 -0
  47. package/dist/components/game/templates/PlatformerTemplate.d.ts +5 -4
  48. package/dist/components/game/templates/game3dAssetManifest.d.ts +6 -8
  49. package/dist/components/index.cjs +2314 -2411
  50. package/dist/components/index.js +1203 -1300
  51. package/dist/providers/index.cjs +1987 -2118
  52. package/dist/providers/index.js +888 -1019
  53. package/dist/runtime/index.cjs +2043 -2174
  54. package/dist/runtime/index.js +892 -1023
  55. package/package.json +2 -2
@@ -1779,7 +1779,7 @@ function TileRenderer({
1779
1779
  children: /* @__PURE__ */ jsxRuntime.jsx(
1780
1780
  ModelLoader,
1781
1781
  {
1782
- url: tile.modelUrl,
1782
+ url: tile.modelUrl?.url,
1783
1783
  position,
1784
1784
  scale: cellSize * 0.9,
1785
1785
  fallbackGeometry: "box",
@@ -2181,7 +2181,7 @@ function FeatureModel({
2181
2181
  onHover
2182
2182
  }) {
2183
2183
  const groupRef = React11.useRef(null);
2184
- const { model: loadedModel, isLoading } = useGLTFModel2(feature.assetUrl);
2184
+ const { model: loadedModel, isLoading } = useGLTFModel2(feature.assetUrl?.url);
2185
2185
  const model = React11.useMemo(() => {
2186
2186
  if (!loadedModel) return null;
2187
2187
  const cloned = loadedModel.clone();
@@ -2293,13 +2293,6 @@ function preloadFeatures(urls) {
2293
2293
  }
2294
2294
 
2295
2295
  // components/game/organisms/utils/spriteAnimation.ts
2296
- function inferDirection(dx, dy) {
2297
- if (dx === 0 && dy === 0) return "se";
2298
- if (dx >= 0 && dy >= 0) return "se";
2299
- if (dx <= 0 && dy >= 0) return "sw";
2300
- if (dx >= 0 && dy <= 0) return "ne";
2301
- return "nw";
2302
- }
2303
2296
  function resolveSheetDirection(facing) {
2304
2297
  switch (facing) {
2305
2298
  case "se":
@@ -2320,29 +2313,10 @@ function frameRect(frame, row, columns, frameWidth, frameHeight) {
2320
2313
  sh: frameHeight
2321
2314
  };
2322
2315
  }
2323
- function getCurrentFrameFromDef(def, elapsed) {
2324
- const frameDuration = 1e3 / def.frameRate;
2325
- const totalDuration = def.frames * frameDuration;
2326
- if (def.loop) {
2327
- const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
2328
- return { frame: frame2, finished: false };
2329
- }
2330
- if (elapsed >= totalDuration) {
2331
- return { frame: def.frames - 1, finished: true };
2332
- }
2333
- const frame = Math.floor(elapsed / frameDuration);
2334
- return { frame, finished: false };
2335
- }
2336
2316
 
2337
2317
  // components/game/molecules/useUnitSpriteAtlas.ts
2338
- var WALK_HOLD_MS = 600;
2339
2318
  function unitAtlasUrl(unit) {
2340
- if (unit.spriteSheet) return unit.spriteSheet;
2341
- const sprite = unit.sprite;
2342
- if (!sprite) return null;
2343
- const match = /^(.*-sprite-sheet)(?:-(?:se|sw))?(?:-v\d+)?\.png$/.exec(sprite);
2344
- if (!match) return null;
2345
- return `${match[1]}.json`;
2319
+ return unit.spriteSheet?.url ?? null;
2346
2320
  }
2347
2321
  function resolveSheetUrl(atlasUrl, relativeSheetPath) {
2348
2322
  try {
@@ -2357,9 +2331,6 @@ function useUnitSpriteAtlas(units) {
2357
2331
  const loadingRef = React11.useRef(/* @__PURE__ */ new Set());
2358
2332
  const [pendingCount, setPendingCount] = React11.useState(0);
2359
2333
  const [, forceTick] = React11.useState(0);
2360
- const animStatesRef = React11.useRef(/* @__PURE__ */ new Map());
2361
- const lastTickRef = React11.useRef(0);
2362
- const rafRef = React11.useRef(0);
2363
2334
  const atlasUrls = React11.useMemo(() => {
2364
2335
  const set = /* @__PURE__ */ new Set();
2365
2336
  for (const unit of units) {
@@ -2405,54 +2376,6 @@ function useUnitSpriteAtlas(units) {
2405
2376
  }
2406
2377
  return [...urls];
2407
2378
  }, [units, pendingCount]);
2408
- React11.useEffect(() => {
2409
- const hasAtlasUnits = units.some((u) => unitAtlasUrl(u) !== null);
2410
- if (!hasAtlasUnits) return;
2411
- let running = true;
2412
- const tick = (ts) => {
2413
- if (!running) return;
2414
- const last = lastTickRef.current || ts;
2415
- const delta = ts - last;
2416
- lastTickRef.current = ts;
2417
- const states = animStatesRef.current;
2418
- const currentIds = /* @__PURE__ */ new Set();
2419
- for (const unit of units) {
2420
- if (unitAtlasUrl(unit) === null) continue;
2421
- currentIds.add(unit.id);
2422
- let state = states.get(unit.id);
2423
- if (!state) {
2424
- state = { animation: "idle", direction: "se", elapsed: 0, walkHold: 0, prev: null };
2425
- states.set(unit.id, state);
2426
- }
2427
- const posX = unit.position?.x ?? unit.x ?? 0;
2428
- const posY = unit.position?.y ?? unit.y ?? 0;
2429
- if (state.prev) {
2430
- const dx = posX - state.prev.x;
2431
- const dy = posY - state.prev.y;
2432
- if (dx !== 0 || dy !== 0) {
2433
- state.animation = "walk";
2434
- state.direction = inferDirection(dx, dy);
2435
- state.walkHold = WALK_HOLD_MS;
2436
- } else if (state.animation === "walk") {
2437
- state.walkHold -= delta;
2438
- if (state.walkHold <= 0) state.animation = "idle";
2439
- }
2440
- }
2441
- state.prev = { x: posX, y: posY };
2442
- state.elapsed += delta;
2443
- }
2444
- for (const id of states.keys()) {
2445
- if (!currentIds.has(id)) states.delete(id);
2446
- }
2447
- rafRef.current = requestAnimationFrame(tick);
2448
- };
2449
- rafRef.current = requestAnimationFrame(tick);
2450
- return () => {
2451
- running = false;
2452
- cancelAnimationFrame(rafRef.current);
2453
- lastTickRef.current = 0;
2454
- };
2455
- }, [units]);
2456
2379
  const resolveUnitFrame = React11.useCallback((unitId) => {
2457
2380
  const unit = units.find((u) => u.id === unitId);
2458
2381
  if (!unit) return null;
@@ -2460,18 +2383,14 @@ function useUnitSpriteAtlas(units) {
2460
2383
  if (!atlasUrl) return null;
2461
2384
  const atlas = atlasCacheRef.current.get(atlasUrl);
2462
2385
  if (!atlas) return null;
2463
- const state = animStatesRef.current.get(unitId);
2464
- const animation = state?.animation ?? "idle";
2465
- const direction = state?.direction ?? "se";
2466
- const elapsed = state?.elapsed ?? 0;
2386
+ const animation = unit.animation ?? "idle";
2387
+ const frame = unit.frame ?? 0;
2467
2388
  const def = atlas.animations[animation] ?? atlas.animations.idle;
2468
2389
  if (!def) return null;
2469
- const { sheetDir, flipX } = resolveSheetDirection(direction);
2390
+ const { sheetDir, flipX } = resolveSheetDirection("se");
2470
2391
  const rel = atlas.sheets[sheetDir] ?? atlas.sheets.se ?? atlas.sheets.sw;
2471
2392
  if (!rel) return null;
2472
2393
  const sheetUrl = resolveSheetUrl(atlasUrl, rel);
2473
- const isIdle = animation === "idle";
2474
- const frame = isIdle ? 0 : getCurrentFrameFromDef(def, elapsed).frame;
2475
2394
  const rect = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
2476
2395
  return {
2477
2396
  sheetUrl,
@@ -2480,7 +2399,7 @@ function useUnitSpriteAtlas(units) {
2480
2399
  sw: rect.sw,
2481
2400
  sh: rect.sh,
2482
2401
  flipX,
2483
- applyBreathing: isIdle
2402
+ applyBreathing: animation === "idle"
2484
2403
  };
2485
2404
  }, [units]);
2486
2405
  return { sheetUrls, resolveUnitFrame, pendingCount };
@@ -2576,7 +2495,6 @@ var GameCanvas3D = React11.forwardRef(
2576
2495
  className,
2577
2496
  isLoading: externalLoading,
2578
2497
  error: externalError,
2579
- entity,
2580
2498
  preloadAssets = [],
2581
2499
  tileClickEvent,
2582
2500
  unitClickEvent,
@@ -2809,7 +2727,7 @@ var GameCanvas3D = React11.forwardRef(
2809
2727
  children: /* @__PURE__ */ jsxRuntime.jsx(
2810
2728
  ModelLoader,
2811
2729
  {
2812
- url: tile.modelUrl,
2730
+ url: tile.modelUrl.url,
2813
2731
  scale: 0.95,
2814
2732
  fallbackGeometry: "box",
2815
2733
  castShadow: true,
@@ -2874,7 +2792,7 @@ var GameCanvas3D = React11.forwardRef(
2874
2792
  /* @__PURE__ */ jsxRuntime.jsx(
2875
2793
  ModelLoader,
2876
2794
  {
2877
- url: unit.modelUrl,
2795
+ url: unit.modelUrl.url,
2878
2796
  scale: modelScale,
2879
2797
  fallbackGeometry: "box",
2880
2798
  castShadow: true
@@ -2934,7 +2852,7 @@ var GameCanvas3D = React11.forwardRef(
2934
2852
  return /* @__PURE__ */ jsxRuntime.jsx(
2935
2853
  ModelLoader,
2936
2854
  {
2937
- url: feature.assetUrl,
2855
+ url: feature.assetUrl.url,
2938
2856
  position,
2939
2857
  scale: 0.5,
2940
2858
  rotation: [0, feature.rotation ?? 0, 0],
@@ -4302,7 +4220,18 @@ function GameBoard3D({
4302
4220
  const row = boardEntity(entity);
4303
4221
  const eventBus = useEventBus();
4304
4222
  const entityUnits = row ? rows(row.units) : [];
4305
- const units = entityUnits;
4223
+ const units = entityUnits.map((r) => ({
4224
+ id: str(r.id),
4225
+ x: num(r.x),
4226
+ y: num(r.y),
4227
+ z: num(r.z),
4228
+ faction: str(r.faction) || "neutral",
4229
+ team: str(r.team) || str(r.faction) || "neutral",
4230
+ unitType: r.unitType == null ? void 0 : str(r.unitType),
4231
+ name: r.name == null ? void 0 : str(r.name),
4232
+ health: r.health == null ? void 0 : num(r.health),
4233
+ maxHealth: r.maxHealth == null ? void 0 : num(r.maxHealth)
4234
+ }));
4306
4235
  const selectedUnitId = row ? str(row.selectedUnitId) || null : null;
4307
4236
  const phase = row ? str(row.phase) : "observation";
4308
4237
  const result = row ? str(row.result) : "none";
@@ -4694,16 +4623,31 @@ var Box = React11__default.default.forwardRef(
4694
4623
  );
4695
4624
  Box.displayName = "Box";
4696
4625
 
4697
- // components/game/templates/game3dAssetManifest.ts
4698
- function resolveManifestUrl(manifest, relative) {
4699
- if (relative == null) return void 0;
4700
- if (/^https?:\/\//.test(relative)) return relative;
4701
- const base = manifest?.baseUrl;
4702
- if (base == null) return relative;
4703
- const cleanBase = base.replace(/\/$/, "");
4704
- const cleanRel = relative.replace(/^\//, "");
4705
- return `${cleanBase}/${cleanRel}`;
4626
+ // components/game/organisms/utils/makeAsset.ts
4627
+ var ROLE_DEFAULTS = {
4628
+ tile: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4629
+ decoration: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4630
+ effect: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4631
+ item: { dimension: "2d", animations: ["static"], aspect: "5:7" },
4632
+ player: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4633
+ enemy: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4634
+ npc: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4635
+ projectile: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4636
+ vehicle: { dimension: "2d", animations: ["idle", "walk"], aspect: "1:1" },
4637
+ ui: { dimension: "2d", animations: ["static"], aspect: "1:1" }
4638
+ };
4639
+ function makeAsset(url, role, overrides = {}) {
4640
+ const defaults = ROLE_DEFAULTS[role] ?? { dimension: "2d", animations: ["static"], aspect: "1:1" };
4641
+ return {
4642
+ url,
4643
+ role,
4644
+ category: url.split("/").pop()?.replace(/\.[^.]+$/, "") ?? role,
4645
+ ...defaults,
4646
+ ...overrides
4647
+ };
4706
4648
  }
4649
+
4650
+ // components/game/templates/game3dAssetManifest.ts
4707
4651
  function tileKind(type, passable) {
4708
4652
  if (passable === false) return "wall";
4709
4653
  if (type === "dirt" || type === "road") return "dirt";
@@ -4719,7 +4663,7 @@ function resolveTilesWithModels(layout, manifest) {
4719
4663
  z: t.z,
4720
4664
  type: t.type,
4721
4665
  passable: t.passable,
4722
- modelUrl: resolveManifestUrl(manifest, manifest?.models?.[kind])
4666
+ modelUrl: manifest?.models?.[kind]
4723
4667
  };
4724
4668
  });
4725
4669
  }
@@ -4728,7 +4672,7 @@ function resolvePropTiles(tiles, manifest) {
4728
4672
  const kind = tileKind(t.type ?? "", t.passable);
4729
4673
  return {
4730
4674
  ...t,
4731
- modelUrl: t.modelUrl ?? resolveManifestUrl(manifest, manifest?.models?.[kind])
4675
+ modelUrl: t.modelUrl ?? manifest?.models?.[kind]
4732
4676
  };
4733
4677
  });
4734
4678
  }
@@ -4739,7 +4683,8 @@ function resolveEntityTiles(entity, manifest) {
4739
4683
  const type = str(r.type) || str(r.terrain);
4740
4684
  const passable = r.passable !== false;
4741
4685
  const kind = tileKind(type, passable);
4742
- const explicitModel = r.modelUrl == null ? void 0 : str(r.modelUrl);
4686
+ const explicitModelUrl = r.modelUrl == null ? void 0 : str(r.modelUrl);
4687
+ const explicitModel = explicitModelUrl ? makeAsset(explicitModelUrl, "decoration", { dimension: "3d" }) : void 0;
4743
4688
  return {
4744
4689
  id: r.id == null ? void 0 : str(r.id),
4745
4690
  x: num(r.x),
@@ -4747,14 +4692,14 @@ function resolveEntityTiles(entity, manifest) {
4747
4692
  z: r.z == null ? void 0 : num(r.z),
4748
4693
  type,
4749
4694
  passable,
4750
- modelUrl: explicitModel ?? resolveManifestUrl(manifest, manifest?.models?.[kind])
4695
+ modelUrl: explicitModel ?? manifest?.models?.[kind]
4751
4696
  };
4752
4697
  });
4753
4698
  }
4754
4699
  function resolveFeaturesWithModels(features, manifest) {
4755
4700
  return features.map((f) => ({
4756
4701
  ...f,
4757
- assetUrl: f.assetUrl ?? resolveManifestUrl(manifest, manifest?.features?.[f.type])
4702
+ assetUrl: f.assetUrl ?? manifest?.features?.[f.type]
4758
4703
  }));
4759
4704
  }
4760
4705
  function resolveEntityFeatures(entity, manifest) {
@@ -4762,7 +4707,8 @@ function resolveEntityFeatures(entity, manifest) {
4762
4707
  if (!row) return [];
4763
4708
  return rows(row.features).map((r) => {
4764
4709
  const type = str(r.type);
4765
- const explicit = r.assetUrl == null ? void 0 : str(r.assetUrl);
4710
+ const explicitUrl = r.assetUrl == null ? void 0 : str(r.assetUrl);
4711
+ const explicit = explicitUrl ? makeAsset(explicitUrl, "decoration", { dimension: "3d" }) : void 0;
4766
4712
  return {
4767
4713
  id: r.id == null ? void 0 : str(r.id),
4768
4714
  x: num(r.x),
@@ -4770,7 +4716,7 @@ function resolveEntityFeatures(entity, manifest) {
4770
4716
  z: r.z == null ? void 0 : num(r.z),
4771
4717
  type,
4772
4718
  color: r.color == null ? void 0 : str(r.color),
4773
- assetUrl: explicit ?? resolveManifestUrl(manifest, manifest?.features?.[type])
4719
+ assetUrl: explicit ?? manifest?.features?.[type]
4774
4720
  };
4775
4721
  });
4776
4722
  }
@@ -1755,7 +1755,7 @@ function TileRenderer({
1755
1755
  children: /* @__PURE__ */ jsx(
1756
1756
  ModelLoader,
1757
1757
  {
1758
- url: tile.modelUrl,
1758
+ url: tile.modelUrl?.url,
1759
1759
  position,
1760
1760
  scale: cellSize * 0.9,
1761
1761
  fallbackGeometry: "box",
@@ -2157,7 +2157,7 @@ function FeatureModel({
2157
2157
  onHover
2158
2158
  }) {
2159
2159
  const groupRef = useRef(null);
2160
- const { model: loadedModel, isLoading } = useGLTFModel2(feature.assetUrl);
2160
+ const { model: loadedModel, isLoading } = useGLTFModel2(feature.assetUrl?.url);
2161
2161
  const model = useMemo(() => {
2162
2162
  if (!loadedModel) return null;
2163
2163
  const cloned = loadedModel.clone();
@@ -2269,13 +2269,6 @@ function preloadFeatures(urls) {
2269
2269
  }
2270
2270
 
2271
2271
  // components/game/organisms/utils/spriteAnimation.ts
2272
- function inferDirection(dx, dy) {
2273
- if (dx === 0 && dy === 0) return "se";
2274
- if (dx >= 0 && dy >= 0) return "se";
2275
- if (dx <= 0 && dy >= 0) return "sw";
2276
- if (dx >= 0 && dy <= 0) return "ne";
2277
- return "nw";
2278
- }
2279
2272
  function resolveSheetDirection(facing) {
2280
2273
  switch (facing) {
2281
2274
  case "se":
@@ -2296,29 +2289,10 @@ function frameRect(frame, row, columns, frameWidth, frameHeight) {
2296
2289
  sh: frameHeight
2297
2290
  };
2298
2291
  }
2299
- function getCurrentFrameFromDef(def, elapsed) {
2300
- const frameDuration = 1e3 / def.frameRate;
2301
- const totalDuration = def.frames * frameDuration;
2302
- if (def.loop) {
2303
- const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
2304
- return { frame: frame2, finished: false };
2305
- }
2306
- if (elapsed >= totalDuration) {
2307
- return { frame: def.frames - 1, finished: true };
2308
- }
2309
- const frame = Math.floor(elapsed / frameDuration);
2310
- return { frame, finished: false };
2311
- }
2312
2292
 
2313
2293
  // components/game/molecules/useUnitSpriteAtlas.ts
2314
- var WALK_HOLD_MS = 600;
2315
2294
  function unitAtlasUrl(unit) {
2316
- if (unit.spriteSheet) return unit.spriteSheet;
2317
- const sprite = unit.sprite;
2318
- if (!sprite) return null;
2319
- const match = /^(.*-sprite-sheet)(?:-(?:se|sw))?(?:-v\d+)?\.png$/.exec(sprite);
2320
- if (!match) return null;
2321
- return `${match[1]}.json`;
2295
+ return unit.spriteSheet?.url ?? null;
2322
2296
  }
2323
2297
  function resolveSheetUrl(atlasUrl, relativeSheetPath) {
2324
2298
  try {
@@ -2333,9 +2307,6 @@ function useUnitSpriteAtlas(units) {
2333
2307
  const loadingRef = useRef(/* @__PURE__ */ new Set());
2334
2308
  const [pendingCount, setPendingCount] = useState(0);
2335
2309
  const [, forceTick] = useState(0);
2336
- const animStatesRef = useRef(/* @__PURE__ */ new Map());
2337
- const lastTickRef = useRef(0);
2338
- const rafRef = useRef(0);
2339
2310
  const atlasUrls = useMemo(() => {
2340
2311
  const set = /* @__PURE__ */ new Set();
2341
2312
  for (const unit of units) {
@@ -2381,54 +2352,6 @@ function useUnitSpriteAtlas(units) {
2381
2352
  }
2382
2353
  return [...urls];
2383
2354
  }, [units, pendingCount]);
2384
- useEffect(() => {
2385
- const hasAtlasUnits = units.some((u) => unitAtlasUrl(u) !== null);
2386
- if (!hasAtlasUnits) return;
2387
- let running = true;
2388
- const tick = (ts) => {
2389
- if (!running) return;
2390
- const last = lastTickRef.current || ts;
2391
- const delta = ts - last;
2392
- lastTickRef.current = ts;
2393
- const states = animStatesRef.current;
2394
- const currentIds = /* @__PURE__ */ new Set();
2395
- for (const unit of units) {
2396
- if (unitAtlasUrl(unit) === null) continue;
2397
- currentIds.add(unit.id);
2398
- let state = states.get(unit.id);
2399
- if (!state) {
2400
- state = { animation: "idle", direction: "se", elapsed: 0, walkHold: 0, prev: null };
2401
- states.set(unit.id, state);
2402
- }
2403
- const posX = unit.position?.x ?? unit.x ?? 0;
2404
- const posY = unit.position?.y ?? unit.y ?? 0;
2405
- if (state.prev) {
2406
- const dx = posX - state.prev.x;
2407
- const dy = posY - state.prev.y;
2408
- if (dx !== 0 || dy !== 0) {
2409
- state.animation = "walk";
2410
- state.direction = inferDirection(dx, dy);
2411
- state.walkHold = WALK_HOLD_MS;
2412
- } else if (state.animation === "walk") {
2413
- state.walkHold -= delta;
2414
- if (state.walkHold <= 0) state.animation = "idle";
2415
- }
2416
- }
2417
- state.prev = { x: posX, y: posY };
2418
- state.elapsed += delta;
2419
- }
2420
- for (const id of states.keys()) {
2421
- if (!currentIds.has(id)) states.delete(id);
2422
- }
2423
- rafRef.current = requestAnimationFrame(tick);
2424
- };
2425
- rafRef.current = requestAnimationFrame(tick);
2426
- return () => {
2427
- running = false;
2428
- cancelAnimationFrame(rafRef.current);
2429
- lastTickRef.current = 0;
2430
- };
2431
- }, [units]);
2432
2355
  const resolveUnitFrame = useCallback((unitId) => {
2433
2356
  const unit = units.find((u) => u.id === unitId);
2434
2357
  if (!unit) return null;
@@ -2436,18 +2359,14 @@ function useUnitSpriteAtlas(units) {
2436
2359
  if (!atlasUrl) return null;
2437
2360
  const atlas = atlasCacheRef.current.get(atlasUrl);
2438
2361
  if (!atlas) return null;
2439
- const state = animStatesRef.current.get(unitId);
2440
- const animation = state?.animation ?? "idle";
2441
- const direction = state?.direction ?? "se";
2442
- const elapsed = state?.elapsed ?? 0;
2362
+ const animation = unit.animation ?? "idle";
2363
+ const frame = unit.frame ?? 0;
2443
2364
  const def = atlas.animations[animation] ?? atlas.animations.idle;
2444
2365
  if (!def) return null;
2445
- const { sheetDir, flipX } = resolveSheetDirection(direction);
2366
+ const { sheetDir, flipX } = resolveSheetDirection("se");
2446
2367
  const rel = atlas.sheets[sheetDir] ?? atlas.sheets.se ?? atlas.sheets.sw;
2447
2368
  if (!rel) return null;
2448
2369
  const sheetUrl = resolveSheetUrl(atlasUrl, rel);
2449
- const isIdle = animation === "idle";
2450
- const frame = isIdle ? 0 : getCurrentFrameFromDef(def, elapsed).frame;
2451
2370
  const rect = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
2452
2371
  return {
2453
2372
  sheetUrl,
@@ -2456,7 +2375,7 @@ function useUnitSpriteAtlas(units) {
2456
2375
  sw: rect.sw,
2457
2376
  sh: rect.sh,
2458
2377
  flipX,
2459
- applyBreathing: isIdle
2378
+ applyBreathing: animation === "idle"
2460
2379
  };
2461
2380
  }, [units]);
2462
2381
  return { sheetUrls, resolveUnitFrame, pendingCount };
@@ -2552,7 +2471,6 @@ var GameCanvas3D = forwardRef(
2552
2471
  className,
2553
2472
  isLoading: externalLoading,
2554
2473
  error: externalError,
2555
- entity,
2556
2474
  preloadAssets = [],
2557
2475
  tileClickEvent,
2558
2476
  unitClickEvent,
@@ -2785,7 +2703,7 @@ var GameCanvas3D = forwardRef(
2785
2703
  children: /* @__PURE__ */ jsx(
2786
2704
  ModelLoader,
2787
2705
  {
2788
- url: tile.modelUrl,
2706
+ url: tile.modelUrl.url,
2789
2707
  scale: 0.95,
2790
2708
  fallbackGeometry: "box",
2791
2709
  castShadow: true,
@@ -2850,7 +2768,7 @@ var GameCanvas3D = forwardRef(
2850
2768
  /* @__PURE__ */ jsx(
2851
2769
  ModelLoader,
2852
2770
  {
2853
- url: unit.modelUrl,
2771
+ url: unit.modelUrl.url,
2854
2772
  scale: modelScale,
2855
2773
  fallbackGeometry: "box",
2856
2774
  castShadow: true
@@ -2910,7 +2828,7 @@ var GameCanvas3D = forwardRef(
2910
2828
  return /* @__PURE__ */ jsx(
2911
2829
  ModelLoader,
2912
2830
  {
2913
- url: feature.assetUrl,
2831
+ url: feature.assetUrl.url,
2914
2832
  position,
2915
2833
  scale: 0.5,
2916
2834
  rotation: [0, feature.rotation ?? 0, 0],
@@ -4278,7 +4196,18 @@ function GameBoard3D({
4278
4196
  const row = boardEntity(entity);
4279
4197
  const eventBus = useEventBus();
4280
4198
  const entityUnits = row ? rows(row.units) : [];
4281
- const units = entityUnits;
4199
+ const units = entityUnits.map((r) => ({
4200
+ id: str(r.id),
4201
+ x: num(r.x),
4202
+ y: num(r.y),
4203
+ z: num(r.z),
4204
+ faction: str(r.faction) || "neutral",
4205
+ team: str(r.team) || str(r.faction) || "neutral",
4206
+ unitType: r.unitType == null ? void 0 : str(r.unitType),
4207
+ name: r.name == null ? void 0 : str(r.name),
4208
+ health: r.health == null ? void 0 : num(r.health),
4209
+ maxHealth: r.maxHealth == null ? void 0 : num(r.maxHealth)
4210
+ }));
4282
4211
  const selectedUnitId = row ? str(row.selectedUnitId) || null : null;
4283
4212
  const phase = row ? str(row.phase) : "observation";
4284
4213
  const result = row ? str(row.result) : "none";
@@ -4670,16 +4599,31 @@ var Box = React11.forwardRef(
4670
4599
  );
4671
4600
  Box.displayName = "Box";
4672
4601
 
4673
- // components/game/templates/game3dAssetManifest.ts
4674
- function resolveManifestUrl(manifest, relative) {
4675
- if (relative == null) return void 0;
4676
- if (/^https?:\/\//.test(relative)) return relative;
4677
- const base = manifest?.baseUrl;
4678
- if (base == null) return relative;
4679
- const cleanBase = base.replace(/\/$/, "");
4680
- const cleanRel = relative.replace(/^\//, "");
4681
- return `${cleanBase}/${cleanRel}`;
4602
+ // components/game/organisms/utils/makeAsset.ts
4603
+ var ROLE_DEFAULTS = {
4604
+ tile: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4605
+ decoration: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4606
+ effect: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4607
+ item: { dimension: "2d", animations: ["static"], aspect: "5:7" },
4608
+ player: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4609
+ enemy: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4610
+ npc: { dimension: "2d", animations: ["idle", "walk", "attack", "hit", "death"], aspect: "1:1" },
4611
+ projectile: { dimension: "2d", animations: ["static"], aspect: "1:1" },
4612
+ vehicle: { dimension: "2d", animations: ["idle", "walk"], aspect: "1:1" },
4613
+ ui: { dimension: "2d", animations: ["static"], aspect: "1:1" }
4614
+ };
4615
+ function makeAsset(url, role, overrides = {}) {
4616
+ const defaults = ROLE_DEFAULTS[role] ?? { dimension: "2d", animations: ["static"], aspect: "1:1" };
4617
+ return {
4618
+ url,
4619
+ role,
4620
+ category: url.split("/").pop()?.replace(/\.[^.]+$/, "") ?? role,
4621
+ ...defaults,
4622
+ ...overrides
4623
+ };
4682
4624
  }
4625
+
4626
+ // components/game/templates/game3dAssetManifest.ts
4683
4627
  function tileKind(type, passable) {
4684
4628
  if (passable === false) return "wall";
4685
4629
  if (type === "dirt" || type === "road") return "dirt";
@@ -4695,7 +4639,7 @@ function resolveTilesWithModels(layout, manifest) {
4695
4639
  z: t.z,
4696
4640
  type: t.type,
4697
4641
  passable: t.passable,
4698
- modelUrl: resolveManifestUrl(manifest, manifest?.models?.[kind])
4642
+ modelUrl: manifest?.models?.[kind]
4699
4643
  };
4700
4644
  });
4701
4645
  }
@@ -4704,7 +4648,7 @@ function resolvePropTiles(tiles, manifest) {
4704
4648
  const kind = tileKind(t.type ?? "", t.passable);
4705
4649
  return {
4706
4650
  ...t,
4707
- modelUrl: t.modelUrl ?? resolveManifestUrl(manifest, manifest?.models?.[kind])
4651
+ modelUrl: t.modelUrl ?? manifest?.models?.[kind]
4708
4652
  };
4709
4653
  });
4710
4654
  }
@@ -4715,7 +4659,8 @@ function resolveEntityTiles(entity, manifest) {
4715
4659
  const type = str(r.type) || str(r.terrain);
4716
4660
  const passable = r.passable !== false;
4717
4661
  const kind = tileKind(type, passable);
4718
- const explicitModel = r.modelUrl == null ? void 0 : str(r.modelUrl);
4662
+ const explicitModelUrl = r.modelUrl == null ? void 0 : str(r.modelUrl);
4663
+ const explicitModel = explicitModelUrl ? makeAsset(explicitModelUrl, "decoration", { dimension: "3d" }) : void 0;
4719
4664
  return {
4720
4665
  id: r.id == null ? void 0 : str(r.id),
4721
4666
  x: num(r.x),
@@ -4723,14 +4668,14 @@ function resolveEntityTiles(entity, manifest) {
4723
4668
  z: r.z == null ? void 0 : num(r.z),
4724
4669
  type,
4725
4670
  passable,
4726
- modelUrl: explicitModel ?? resolveManifestUrl(manifest, manifest?.models?.[kind])
4671
+ modelUrl: explicitModel ?? manifest?.models?.[kind]
4727
4672
  };
4728
4673
  });
4729
4674
  }
4730
4675
  function resolveFeaturesWithModels(features, manifest) {
4731
4676
  return features.map((f) => ({
4732
4677
  ...f,
4733
- assetUrl: f.assetUrl ?? resolveManifestUrl(manifest, manifest?.features?.[f.type])
4678
+ assetUrl: f.assetUrl ?? manifest?.features?.[f.type]
4734
4679
  }));
4735
4680
  }
4736
4681
  function resolveEntityFeatures(entity, manifest) {
@@ -4738,7 +4683,8 @@ function resolveEntityFeatures(entity, manifest) {
4738
4683
  if (!row) return [];
4739
4684
  return rows(row.features).map((r) => {
4740
4685
  const type = str(r.type);
4741
- const explicit = r.assetUrl == null ? void 0 : str(r.assetUrl);
4686
+ const explicitUrl = r.assetUrl == null ? void 0 : str(r.assetUrl);
4687
+ const explicit = explicitUrl ? makeAsset(explicitUrl, "decoration", { dimension: "3d" }) : void 0;
4742
4688
  return {
4743
4689
  id: r.id == null ? void 0 : str(r.id),
4744
4690
  x: num(r.x),
@@ -4746,7 +4692,7 @@ function resolveEntityFeatures(entity, manifest) {
4746
4692
  z: r.z == null ? void 0 : num(r.z),
4747
4693
  type,
4748
4694
  color: r.color == null ? void 0 : str(r.color),
4749
- assetUrl: explicit ?? resolveManifestUrl(manifest, manifest?.features?.[type])
4695
+ assetUrl: explicit ?? manifest?.features?.[type]
4750
4696
  };
4751
4697
  });
4752
4698
  }
@@ -1,19 +1,12 @@
1
- import type { AssetUrl } from '@almadar/core';
2
1
  import type { IsometricUnit } from '../organisms/types/isometric';
3
2
  import type { ResolvedFrame } from '../organisms/types/spriteAnimation';
4
3
  /**
5
4
  * Resolve the sprite-sheet atlas-JSON URL for a unit.
6
5
  *
7
- * An explicit `unit.spriteSheet` wins. Otherwise, when the unit's static
8
- * `sprite` points at a directional sheet PNG, derive its sibling atlas JSON by
9
- * the deterministic naming convention used in `shared/sprite-sheets/`:
10
- * the shared atlas drops the `-se` / `-sw` direction suffix and any `-vN`
11
- * variant suffix, then swaps `.png` → `.json`
12
- * (`amir-sprite-sheet-se.png` → `amir-sprite-sheet.json`).
13
- *
14
- * Returns null when no atlas can be resolved (the unit keeps its static draw).
6
+ * A unit has an atlas iff `unit.spriteSheet.url` is set (explicit metadata wins).
7
+ * No filename-pattern matching a unit without a `spriteSheet` Asset keeps its static draw.
15
8
  */
16
- export declare function unitAtlasUrl(unit: Pick<IsometricUnit, 'spriteSheet' | 'sprite'>): AssetUrl | null;
9
+ export declare function unitAtlasUrl(unit: Pick<IsometricUnit, 'spriteSheet'>): string | null;
17
10
  export interface UseUnitSpriteAtlasResult {
18
11
  /** Atlas-JSON URLs successfully loaded — feed these to no one; sheet PNGs are returned below. */
19
12
  sheetUrls: string[];
@@ -23,8 +16,9 @@ export interface UseUnitSpriteAtlasResult {
23
16
  pendingCount: number;
24
17
  }
25
18
  /**
26
- * Load per-unit sprite atlases and drive single-frame animation.
19
+ * Load per-unit sprite atlases and expose a pure frame resolver.
20
+ * Frame is computed from each unit's own `animation` + `frame` props — no internal clock.
27
21
  *
28
- * @param units - Units to animate. Only units with a `spriteSheet` URL participate.
22
+ * @param units - Units whose atlases should be loaded.
29
23
  */
30
24
  export declare function useUnitSpriteAtlas(units: IsometricUnit[]): UseUnitSpriteAtlasResult;