@almadar/ui 5.49.1 → 5.51.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.
@@ -39,7 +39,7 @@ var core = require('@almadar/core');
39
39
  var core$1 = require('@dnd-kit/core');
40
40
  var sortable = require('@dnd-kit/sortable');
41
41
  var utilities = require('@dnd-kit/utilities');
42
- var THREE = require('three');
42
+ var THREE3 = require('three');
43
43
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
44
44
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
45
45
  var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
@@ -94,7 +94,7 @@ var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
94
94
  var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
95
95
  var remarkMath__default = /*#__PURE__*/_interopDefault(remarkMath);
96
96
  var rehypeKatex__default = /*#__PURE__*/_interopDefault(rehypeKatex);
97
- var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
97
+ var THREE3__namespace = /*#__PURE__*/_interopNamespace(THREE3);
98
98
 
99
99
  var __defProp = Object.defineProperty;
100
100
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -10839,6 +10839,52 @@ var init_ControlButton = __esm({
10839
10839
  ControlButton.displayName = "ControlButton";
10840
10840
  }
10841
10841
  });
10842
+
10843
+ // components/game/organisms/utils/spriteAnimation.ts
10844
+ function inferDirection(dx, dy) {
10845
+ if (dx === 0 && dy === 0) return "se";
10846
+ if (dx >= 0 && dy >= 0) return "se";
10847
+ if (dx <= 0 && dy >= 0) return "sw";
10848
+ if (dx >= 0 && dy <= 0) return "ne";
10849
+ return "nw";
10850
+ }
10851
+ function resolveSheetDirection(facing) {
10852
+ switch (facing) {
10853
+ case "se":
10854
+ return { sheetDir: "se", flipX: false };
10855
+ case "sw":
10856
+ return { sheetDir: "sw", flipX: false };
10857
+ case "ne":
10858
+ return { sheetDir: "sw", flipX: true };
10859
+ case "nw":
10860
+ return { sheetDir: "se", flipX: true };
10861
+ }
10862
+ }
10863
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
10864
+ return {
10865
+ sx: frame % columns * frameWidth,
10866
+ sy: row * frameHeight,
10867
+ sw: frameWidth,
10868
+ sh: frameHeight
10869
+ };
10870
+ }
10871
+ function getCurrentFrameFromDef(def, elapsed) {
10872
+ const frameDuration = 1e3 / def.frameRate;
10873
+ const totalDuration = def.frames * frameDuration;
10874
+ if (def.loop) {
10875
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
10876
+ return { frame: frame2, finished: false };
10877
+ }
10878
+ if (elapsed >= totalDuration) {
10879
+ return { frame: def.frames - 1, finished: true };
10880
+ }
10881
+ const frame = Math.floor(elapsed / frameDuration);
10882
+ return { frame, finished: false };
10883
+ }
10884
+ var init_spriteAnimation = __esm({
10885
+ "components/game/organisms/utils/spriteAnimation.ts"() {
10886
+ }
10887
+ });
10842
10888
  function Sprite({
10843
10889
  spritesheet = "https://almadar-kflow-assets.web.app/shared/isometric-blocks/Spritesheet/allTiles_sheet.png",
10844
10890
  frameWidth = 64,
@@ -10859,12 +10905,8 @@ function Sprite({
10859
10905
  }) {
10860
10906
  const eventBus = useEventBus();
10861
10907
  const sourcePosition = React90.useMemo(() => {
10862
- const frameX = frame % columns;
10863
- const frameY = Math.floor(frame / columns);
10864
- return {
10865
- x: frameX * frameWidth,
10866
- y: frameY * frameHeight
10867
- };
10908
+ const { sx, sy } = frameRect(frame, Math.floor(frame / columns), columns, frameWidth, frameHeight);
10909
+ return { x: sx, y: sy };
10868
10910
  }, [frame, columns, frameWidth, frameHeight]);
10869
10911
  const transform = React90.useMemo(() => {
10870
10912
  const transforms = [
@@ -10912,6 +10954,7 @@ var init_Sprite = __esm({
10912
10954
  "components/game/atoms/Sprite.tsx"() {
10913
10955
  "use client";
10914
10956
  init_useEventBus();
10957
+ init_spriteAnimation();
10915
10958
  }
10916
10959
  });
10917
10960
  function StateIndicator({
@@ -14303,6 +14346,163 @@ var init_useCamera = __esm({
14303
14346
  "use client";
14304
14347
  }
14305
14348
  });
14349
+ function unitAtlasUrl(unit) {
14350
+ if (unit.spriteSheet) return unit.spriteSheet;
14351
+ const sprite = unit.sprite;
14352
+ if (!sprite) return null;
14353
+ const match = /^(.*-sprite-sheet)(?:-(?:se|sw))?(?:-v\d+)?\.png$/.exec(sprite);
14354
+ if (!match) return null;
14355
+ return `${match[1]}.json`;
14356
+ }
14357
+ function resolveSheetUrl(atlasUrl, relativeSheetPath) {
14358
+ try {
14359
+ return new URL(relativeSheetPath, atlasUrl).toString();
14360
+ } catch {
14361
+ const base = atlasUrl.slice(0, atlasUrl.lastIndexOf("/") + 1);
14362
+ return `${base}${relativeSheetPath.replace(/^\.\//, "")}`;
14363
+ }
14364
+ }
14365
+ function useUnitSpriteAtlas(units) {
14366
+ const atlasCacheRef = React90.useRef(/* @__PURE__ */ new Map());
14367
+ const loadingRef = React90.useRef(/* @__PURE__ */ new Set());
14368
+ const [pendingCount, setPendingCount] = React90.useState(0);
14369
+ const [, forceTick] = React90.useState(0);
14370
+ const animStatesRef = React90.useRef(/* @__PURE__ */ new Map());
14371
+ const lastTickRef = React90.useRef(0);
14372
+ const rafRef = React90.useRef(0);
14373
+ const atlasUrls = React90.useMemo(() => {
14374
+ const set = /* @__PURE__ */ new Set();
14375
+ for (const unit of units) {
14376
+ const url = unitAtlasUrl(unit);
14377
+ if (url) set.add(url);
14378
+ }
14379
+ return [...set];
14380
+ }, [units]);
14381
+ React90.useEffect(() => {
14382
+ const cache = atlasCacheRef.current;
14383
+ const loading = loadingRef.current;
14384
+ const toLoad = atlasUrls.filter((url) => !cache.has(url) && !loading.has(url));
14385
+ if (toLoad.length === 0) return;
14386
+ let cancelled = false;
14387
+ setPendingCount((prev) => prev + toLoad.length);
14388
+ for (const url of toLoad) {
14389
+ loading.add(url);
14390
+ fetch(url).then((res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))).then((atlas) => {
14391
+ if (cancelled) return;
14392
+ cache.set(url, atlas);
14393
+ }).catch(() => {
14394
+ }).finally(() => {
14395
+ if (cancelled) return;
14396
+ loading.delete(url);
14397
+ setPendingCount((prev) => Math.max(0, prev - 1));
14398
+ forceTick((n) => n + 1);
14399
+ });
14400
+ }
14401
+ return () => {
14402
+ cancelled = true;
14403
+ };
14404
+ }, [atlasUrls]);
14405
+ const sheetUrls = React90.useMemo(() => {
14406
+ const urls = /* @__PURE__ */ new Set();
14407
+ for (const unit of units) {
14408
+ const atlasUrl = unitAtlasUrl(unit);
14409
+ if (!atlasUrl) continue;
14410
+ const atlas = atlasCacheRef.current.get(atlasUrl);
14411
+ if (!atlas) continue;
14412
+ for (const rel of Object.values(atlas.sheets)) {
14413
+ if (rel) urls.add(resolveSheetUrl(atlasUrl, rel));
14414
+ }
14415
+ }
14416
+ return [...urls];
14417
+ }, [units, pendingCount]);
14418
+ React90.useEffect(() => {
14419
+ const hasAtlasUnits = units.some((u) => unitAtlasUrl(u) !== null);
14420
+ if (!hasAtlasUnits) return;
14421
+ let running = true;
14422
+ const tick = (ts) => {
14423
+ if (!running) return;
14424
+ const last = lastTickRef.current || ts;
14425
+ const delta = ts - last;
14426
+ lastTickRef.current = ts;
14427
+ const states = animStatesRef.current;
14428
+ const currentIds = /* @__PURE__ */ new Set();
14429
+ for (const unit of units) {
14430
+ if (unitAtlasUrl(unit) === null) continue;
14431
+ currentIds.add(unit.id);
14432
+ let state = states.get(unit.id);
14433
+ if (!state) {
14434
+ state = { animation: "idle", direction: "se", elapsed: 0, walkHold: 0, prev: null };
14435
+ states.set(unit.id, state);
14436
+ }
14437
+ const posX = unit.position?.x ?? unit.x ?? 0;
14438
+ const posY = unit.position?.y ?? unit.y ?? 0;
14439
+ if (state.prev) {
14440
+ const dx = posX - state.prev.x;
14441
+ const dy = posY - state.prev.y;
14442
+ if (dx !== 0 || dy !== 0) {
14443
+ state.animation = "walk";
14444
+ state.direction = inferDirection(dx, dy);
14445
+ state.walkHold = WALK_HOLD_MS;
14446
+ } else if (state.animation === "walk") {
14447
+ state.walkHold -= delta;
14448
+ if (state.walkHold <= 0) state.animation = "idle";
14449
+ }
14450
+ }
14451
+ state.prev = { x: posX, y: posY };
14452
+ state.elapsed += delta;
14453
+ }
14454
+ for (const id of states.keys()) {
14455
+ if (!currentIds.has(id)) states.delete(id);
14456
+ }
14457
+ rafRef.current = requestAnimationFrame(tick);
14458
+ };
14459
+ rafRef.current = requestAnimationFrame(tick);
14460
+ return () => {
14461
+ running = false;
14462
+ cancelAnimationFrame(rafRef.current);
14463
+ lastTickRef.current = 0;
14464
+ };
14465
+ }, [units]);
14466
+ const resolveUnitFrame = React90.useCallback((unitId) => {
14467
+ const unit = units.find((u) => u.id === unitId);
14468
+ if (!unit) return null;
14469
+ const atlasUrl = unitAtlasUrl(unit);
14470
+ if (!atlasUrl) return null;
14471
+ const atlas = atlasCacheRef.current.get(atlasUrl);
14472
+ if (!atlas) return null;
14473
+ const state = animStatesRef.current.get(unitId);
14474
+ const animation = state?.animation ?? "idle";
14475
+ const direction = state?.direction ?? "se";
14476
+ const elapsed = state?.elapsed ?? 0;
14477
+ const def = atlas.animations[animation] ?? atlas.animations.idle;
14478
+ if (!def) return null;
14479
+ const { sheetDir, flipX } = resolveSheetDirection(direction);
14480
+ const rel = atlas.sheets[sheetDir] ?? atlas.sheets.se ?? atlas.sheets.sw;
14481
+ if (!rel) return null;
14482
+ const sheetUrl = resolveSheetUrl(atlasUrl, rel);
14483
+ const isIdle = animation === "idle";
14484
+ const frame = isIdle ? 0 : getCurrentFrameFromDef(def, elapsed).frame;
14485
+ const rect = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
14486
+ return {
14487
+ sheetUrl,
14488
+ sx: rect.sx,
14489
+ sy: rect.sy,
14490
+ sw: rect.sw,
14491
+ sh: rect.sh,
14492
+ flipX,
14493
+ applyBreathing: isIdle
14494
+ };
14495
+ }, [units]);
14496
+ return { sheetUrls, resolveUnitFrame, pendingCount };
14497
+ }
14498
+ var WALK_HOLD_MS;
14499
+ var init_useUnitSpriteAtlas = __esm({
14500
+ "components/game/molecules/useUnitSpriteAtlas.ts"() {
14501
+ "use client";
14502
+ init_spriteAnimation();
14503
+ WALK_HOLD_MS = 600;
14504
+ }
14505
+ });
14306
14506
 
14307
14507
  // components/game/organisms/utils/isometric.ts
14308
14508
  function isoToScreen(tileX, tileY, scale, baseOffsetX) {
@@ -14417,6 +14617,10 @@ function IsometricCanvas({
14417
14617
  () => unitsProp.map((u) => u.position ? u : { ...u, position: { x: u.x ?? 0, y: u.y ?? 0 } }),
14418
14618
  [unitsProp]
14419
14619
  );
14620
+ const { sheetUrls: atlasSheetUrls, resolveUnitFrame: resolveUnitFrameInternal, pendingCount: atlasPending } = useUnitSpriteAtlas(units);
14621
+ const resolveFrameForUnit = React90.useCallback((unitId) => {
14622
+ return resolveUnitFrame?.(unitId) ?? resolveUnitFrameInternal(unitId);
14623
+ }, [resolveUnitFrame, resolveUnitFrameInternal]);
14420
14624
  const features = React90.useMemo(
14421
14625
  () => featuresProp.map((f3) => {
14422
14626
  if (f3.type) return f3;
@@ -14500,9 +14704,10 @@ function IsometricCanvas({
14500
14704
  }
14501
14705
  }
14502
14706
  if (effectSpriteUrls) urls.push(...effectSpriteUrls);
14707
+ if (atlasSheetUrls.length) urls.push(...atlasSheetUrls);
14503
14708
  if (backgroundImage) urls.push(backgroundImage);
14504
14709
  return [...new Set(urls.filter(Boolean))];
14505
- }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, backgroundImage, assetManifest, resolveManifestUrl]);
14710
+ }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, atlasSheetUrls, backgroundImage, assetManifest, resolveManifestUrl]);
14506
14711
  const { getImage, pendingCount } = useImageCache(spriteUrls);
14507
14712
  React90.useEffect(() => {
14508
14713
  if (typeof window === "undefined") return;
@@ -14789,7 +14994,7 @@ function IsometricCanvas({
14789
14994
  ctx.lineWidth = 3;
14790
14995
  ctx.stroke();
14791
14996
  }
14792
- const frame = resolveUnitFrame?.(unit.id) ?? null;
14997
+ const frame = resolveFrameForUnit(unit.id);
14793
14998
  const frameImg = frame ? getImage(frame.sheetUrl) : null;
14794
14999
  if (frame && frameImg) {
14795
15000
  const frameAr = frame.sw / frame.sh;
@@ -14899,7 +15104,7 @@ function IsometricCanvas({
14899
15104
  resolveTerrainSpriteUrl,
14900
15105
  resolveFeatureSpriteUrl,
14901
15106
  resolveUnitSpriteUrl,
14902
- resolveUnitFrame,
15107
+ resolveFrameForUnit,
14903
15108
  getImage,
14904
15109
  gridWidth,
14905
15110
  gridHeight,
@@ -14931,7 +15136,7 @@ function IsometricCanvas({
14931
15136
  };
14932
15137
  }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
14933
15138
  React90.useEffect(() => {
14934
- const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0;
15139
+ const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
14935
15140
  draw(animTimeRef.current);
14936
15141
  if (!hasAnimations) return;
14937
15142
  let running = true;
@@ -14947,7 +15152,7 @@ function IsometricCanvas({
14947
15152
  running = false;
14948
15153
  cancelAnimationFrame(rafIdRef.current);
14949
15154
  };
14950
- }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, pendingCount, lerpToTarget, targetCameraRef]);
15155
+ }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, pendingCount, atlasPending, lerpToTarget, targetCameraRef]);
14951
15156
  const handleMouseMoveWithCamera = React90.useCallback((e) => {
14952
15157
  if (enableCamera) {
14953
15158
  const wasPanning = handleMouseMove(e, () => draw(animTimeRef.current));
@@ -15092,6 +15297,7 @@ var init_IsometricCanvas = __esm({
15092
15297
  init_ErrorState();
15093
15298
  init_useImageCache();
15094
15299
  init_useCamera();
15300
+ init_useUnitSpriteAtlas();
15095
15301
  init_verificationRegistry();
15096
15302
  init_isometric();
15097
15303
  IsometricCanvas.displayName = "IsometricCanvas";
@@ -31845,13 +32051,13 @@ var init_MapView = __esm({
31845
32051
  shadowSize: [41, 41]
31846
32052
  });
31847
32053
  L.Marker.prototype.options.icon = defaultIcon;
31848
- const { useEffect: useEffect84, useRef: useRef76, useCallback: useCallback124, useState: useState120 } = React90__namespace.default;
32054
+ const { useEffect: useEffect85, useRef: useRef77, useCallback: useCallback125, useState: useState121 } = React90__namespace.default;
31849
32055
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31850
32056
  const { useEventBus: useEventBus4 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31851
32057
  function MapUpdater({ centerLat, centerLng, zoom }) {
31852
32058
  const map = useMap();
31853
- const prevRef = useRef76({ centerLat, centerLng, zoom });
31854
- useEffect84(() => {
32059
+ const prevRef = useRef77({ centerLat, centerLng, zoom });
32060
+ useEffect85(() => {
31855
32061
  const prev = prevRef.current;
31856
32062
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
31857
32063
  map.setView([centerLat, centerLng], zoom);
@@ -31862,7 +32068,7 @@ var init_MapView = __esm({
31862
32068
  }
31863
32069
  function MapClickHandler({ onMapClick }) {
31864
32070
  const map = useMap();
31865
- useEffect84(() => {
32071
+ useEffect85(() => {
31866
32072
  if (!onMapClick) return;
31867
32073
  const handler = (e) => {
31868
32074
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -31890,8 +32096,8 @@ var init_MapView = __esm({
31890
32096
  showAttribution = true
31891
32097
  }) {
31892
32098
  const eventBus = useEventBus4();
31893
- const [clickedPosition, setClickedPosition] = useState120(null);
31894
- const handleMapClick = useCallback124((lat, lng) => {
32099
+ const [clickedPosition, setClickedPosition] = useState121(null);
32100
+ const handleMapClick = useCallback125((lat, lng) => {
31895
32101
  if (showClickedPin) {
31896
32102
  setClickedPosition({ lat, lng });
31897
32103
  }
@@ -31900,7 +32106,7 @@ var init_MapView = __esm({
31900
32106
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
31901
32107
  }
31902
32108
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
31903
- const handleMarkerClick = useCallback124((marker) => {
32109
+ const handleMarkerClick = useCallback125((marker) => {
31904
32110
  onMarkerClick?.(marker);
31905
32111
  if (markerClickEvent) {
31906
32112
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -43209,7 +43415,7 @@ var init_AssetLoader = __esm({
43209
43415
  __publicField(this, "textureCache");
43210
43416
  __publicField(this, "loadingPromises");
43211
43417
  this.objLoader = new OBJLoader_js.OBJLoader();
43212
- this.textureLoader = new THREE__namespace.TextureLoader();
43418
+ this.textureLoader = new THREE3__namespace.TextureLoader();
43213
43419
  this.modelCache = /* @__PURE__ */ new Map();
43214
43420
  this.textureCache = /* @__PURE__ */ new Map();
43215
43421
  this.loadingPromises = /* @__PURE__ */ new Map();
@@ -43283,7 +43489,7 @@ var init_AssetLoader = __esm({
43283
43489
  return this.loadingPromises.get(`texture:${url}`);
43284
43490
  }
43285
43491
  const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
43286
- texture.colorSpace = THREE__namespace.SRGBColorSpace;
43492
+ texture.colorSpace = THREE3__namespace.SRGBColorSpace;
43287
43493
  this.textureCache.set(url, texture);
43288
43494
  this.loadingPromises.delete(`texture:${url}`);
43289
43495
  return texture;
@@ -43357,7 +43563,7 @@ var init_AssetLoader = __esm({
43357
43563
  });
43358
43564
  this.modelCache.forEach((model) => {
43359
43565
  model.scene.traverse((child) => {
43360
- if (child instanceof THREE__namespace.Mesh) {
43566
+ if (child instanceof THREE3__namespace.Mesh) {
43361
43567
  child.geometry.dispose();
43362
43568
  if (Array.isArray(child.material)) {
43363
43569
  child.material.forEach((m) => m.dispose());
@@ -43913,7 +44119,7 @@ function ModelLoader({
43913
44119
  if (!loadedModel) return null;
43914
44120
  const cloned = loadedModel.clone();
43915
44121
  cloned.traverse((child) => {
43916
- if (child instanceof THREE__namespace.Mesh) {
44122
+ if (child instanceof THREE3__namespace.Mesh) {
43917
44123
  child.castShadow = castShadow;
43918
44124
  child.receiveShadow = receiveShadow;
43919
44125
  }
@@ -44027,6 +44233,47 @@ function CameraController({
44027
44233
  }, [camera.position, onCameraChange]);
44028
44234
  return null;
44029
44235
  }
44236
+ function UnitSpriteBillboard({
44237
+ sheetUrl,
44238
+ resolveFrame,
44239
+ height = 1.2
44240
+ }) {
44241
+ const texture = fiber.useLoader(THREE3__namespace.TextureLoader, sheetUrl);
44242
+ const meshRef = React90.useRef(null);
44243
+ const matRef = React90.useRef(null);
44244
+ const [aspect, setAspect] = React90.useState(1);
44245
+ fiber.useFrame(() => {
44246
+ const frame = resolveFrame();
44247
+ if (!frame || !texture.image) return;
44248
+ const imgW = texture.image.width;
44249
+ const imgH = texture.image.height;
44250
+ if (!imgW || !imgH) return;
44251
+ texture.repeat.set((frame.flipX ? -1 : 1) * (frame.sw / imgW), frame.sh / imgH);
44252
+ texture.offset.set(
44253
+ frame.flipX ? (frame.sx + frame.sw) / imgW : frame.sx / imgW,
44254
+ 1 - (frame.sy + frame.sh) / imgH
44255
+ );
44256
+ texture.magFilter = THREE3__namespace.NearestFilter;
44257
+ texture.minFilter = THREE3__namespace.NearestFilter;
44258
+ texture.needsUpdate = true;
44259
+ const nextAspect = frame.sw / frame.sh;
44260
+ if (Math.abs(nextAspect - aspect) > 1e-3) setAspect(nextAspect);
44261
+ if (matRef.current) matRef.current.needsUpdate = true;
44262
+ });
44263
+ return /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ref: meshRef, position: [0, height / 2, 0], children: [
44264
+ /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [height * aspect, height] }),
44265
+ /* @__PURE__ */ jsxRuntime.jsx(
44266
+ "meshBasicMaterial",
44267
+ {
44268
+ ref: matRef,
44269
+ map: texture,
44270
+ transparent: true,
44271
+ alphaTest: 0.1,
44272
+ side: THREE3__namespace.DoubleSide
44273
+ }
44274
+ )
44275
+ ] });
44276
+ }
44030
44277
  var DEFAULT_GRID_CONFIG, GameCanvas3D;
44031
44278
  var init_GameCanvas3D2 = __esm({
44032
44279
  "components/game/molecules/GameCanvas3D.tsx"() {
@@ -44037,6 +44284,7 @@ var init_GameCanvas3D2 = __esm({
44037
44284
  init_Canvas3DLoadingState2();
44038
44285
  init_Canvas3DErrorBoundary2();
44039
44286
  init_ModelLoader();
44287
+ init_useUnitSpriteAtlas();
44040
44288
  init_cn();
44041
44289
  init_GameCanvas3D();
44042
44290
  DEFAULT_GRID_CONFIG = {
@@ -44093,8 +44341,10 @@ var init_GameCanvas3D2 = __esm({
44093
44341
  const controlsRef = React90.useRef(null);
44094
44342
  const [hoveredTile, setHoveredTile] = React90.useState(null);
44095
44343
  const [internalError, setInternalError] = React90.useState(null);
44344
+ const { sheetUrls: atlasSheetUrls, resolveUnitFrame } = useUnitSpriteAtlas(units);
44345
+ const preloadUrls = React90.useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
44096
44346
  const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
44097
- preloadUrls: preloadAssets,
44347
+ preloadUrls,
44098
44348
  loader: customAssetLoader
44099
44349
  });
44100
44350
  const eventHandlers = useGameCanvas3DEvents({
@@ -44153,7 +44403,7 @@ var init_GameCanvas3D2 = __esm({
44153
44403
  getCameraPosition: () => {
44154
44404
  if (controlsRef.current) {
44155
44405
  const pos = controlsRef.current.object.position;
44156
- return new THREE__namespace.Vector3(pos.x, pos.y, pos.z);
44406
+ return new THREE3__namespace.Vector3(pos.x, pos.y, pos.z);
44157
44407
  }
44158
44408
  return null;
44159
44409
  },
@@ -44305,6 +44555,8 @@ var init_GameCanvas3D2 = __esm({
44305
44555
  ({ unit, position }) => {
44306
44556
  const isSelected = selectedUnitId === unit.id;
44307
44557
  const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
44558
+ const hasAtlas = unitAtlasUrl(unit) !== null;
44559
+ const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
44308
44560
  return /* @__PURE__ */ jsxRuntime.jsxs(
44309
44561
  "group",
44310
44562
  {
@@ -44316,7 +44568,16 @@ var init_GameCanvas3D2 = __esm({
44316
44568
  /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
44317
44569
  /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
44318
44570
  ] }),
44319
- unit.modelUrl ? (
44571
+ hasAtlas && initialFrame ? (
44572
+ /* Animated sprite-sheet billboard — single cropped frame, by state */
44573
+ /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { children: /* @__PURE__ */ jsxRuntime.jsx(
44574
+ UnitSpriteBillboard,
44575
+ {
44576
+ sheetUrl: initialFrame.sheetUrl,
44577
+ resolveFrame: () => resolveUnitFrame(unit.id)
44578
+ }
44579
+ ) })
44580
+ ) : unit.modelUrl ? (
44320
44581
  /* GLB unit model (box fallback while loading / on error) */
44321
44582
  /* @__PURE__ */ jsxRuntime.jsx(
44322
44583
  ModelLoader,
@@ -44370,7 +44631,7 @@ var init_GameCanvas3D2 = __esm({
44370
44631
  }
44371
44632
  );
44372
44633
  },
44373
- [selectedUnitId, handleUnitClick]
44634
+ [selectedUnitId, handleUnitClick, resolveUnitFrame]
44374
44635
  );
44375
44636
  const DefaultFeatureRenderer = React90.useCallback(
44376
44637
  ({
@@ -57144,6 +57405,28 @@ init_verificationRegistry();
57144
57405
  var crossTraitLog = logger.createLogger("almadar:ui:cross-trait");
57145
57406
  var flushLog = logger.createLogger("almadar:ui:slot-flush");
57146
57407
  var stateLog = logger.createLogger("almadar:ui:state-transitions");
57408
+ var tickLog = logger.createLogger("almadar:ui:tick-effects");
57409
+ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
57410
+ "set",
57411
+ "emit",
57412
+ "render-ui",
57413
+ "render",
57414
+ "navigate",
57415
+ "notify",
57416
+ "log",
57417
+ // Synchronous structural forms that wrap sync effects. A tick authored as
57418
+ // a single top-level `(let ((..)) (do (set ..) (render-ui ..)))` or
57419
+ // `(if cond (set ..) ..)` is one of these at its head — the EffectExecutor
57420
+ // resolves the binding values / condition through the canonical evaluator
57421
+ // and runs the wrapped sync effects. Without these in the allow-list the
57422
+ // whole tick is filtered out (its head op isn't `set`/`emit`/...), so the
57423
+ // physics/gameflow/AI tick never runs.
57424
+ "let",
57425
+ "if",
57426
+ "do",
57427
+ "when"
57428
+ ]);
57429
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
57147
57430
  function toTraitDefinition(binding) {
57148
57431
  return {
57149
57432
  name: binding.trait.name,
@@ -57176,6 +57459,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57176
57459
  });
57177
57460
  const eventQueueRef = React90.useRef([]);
57178
57461
  const processingRef = React90.useRef(false);
57462
+ const initedTraitsRef = React90.useRef(/* @__PURE__ */ new Set());
57179
57463
  const traitBindingsRef = React90.useRef(traitBindings);
57180
57464
  const managerRef = React90.useRef(manager);
57181
57465
  const uiSlotsRef = React90.useRef(uiSlots);
@@ -57340,50 +57624,196 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57340
57624
  for (const unreg of snapshotUnregs) unreg();
57341
57625
  };
57342
57626
  }, [traitBindings]);
57343
- const runTickEffects = React90.useCallback((tick, binding) => {
57344
- const currentState = traitStatesRef.current.get(binding.trait.name)?.currentState ?? "";
57345
- if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
57346
- const bindingCtx = { entity: {}, payload: {}, state: currentState };
57347
- if (binding.config) {
57348
- bindingCtx.config = binding.config;
57349
- }
57350
- const evalCtx = runtime.createContextFromBindings(bindingCtx);
57351
- if (tick.guard !== void 0) {
57352
- const passed = runtime.interpolateValue(tick.guard, evalCtx);
57353
- if (!passed) return;
57354
- }
57627
+ const executeTransitionEffects = React90.useCallback(async (params) => {
57628
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log15 } = params;
57629
+ const traitName = binding.trait.name;
57630
+ const linkedEntity = binding.linkedEntity || "";
57631
+ const entityId = payload?.entityId;
57632
+ const effects = syncOnly ? params.effects.filter(
57633
+ (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
57634
+ ) : params.effects;
57635
+ if (effects.length === 0) return [];
57355
57636
  const pendingSlots = /* @__PURE__ */ new Map();
57356
57637
  ({
57357
- trait: binding.trait.name,
57358
- transition: `${currentState}->tick:${tick.name}`,
57359
- effects: tick.effects,
57360
57638
  traitDefinition: binding.trait
57361
57639
  });
57362
- for (const effect of tick.effects) {
57363
- if (!Array.isArray(effect)) continue;
57364
- const op = effect[0];
57365
- if (op === "render-ui" || op === "render") {
57366
- const slot = effect[1];
57367
- const rawPattern = effect[2];
57368
- if (rawPattern === null || rawPattern === void 0) {
57640
+ const clientHandlers = createClientEffectHandlers({
57641
+ eventBus,
57642
+ slotSetter: {
57643
+ addPattern: (slot, pattern, props) => {
57644
+ const existing = pendingSlots.get(slot) || [];
57645
+ existing.push({ pattern, props: props || {} });
57646
+ pendingSlots.set(slot, existing);
57647
+ },
57648
+ clearSlot: (slot) => {
57369
57649
  pendingSlots.set(slot, []);
57370
- continue;
57371
57650
  }
57372
- const updatedCtx = runtime.createContextFromBindings(bindingCtx);
57373
- const resolved = runtime.interpolateValue(rawPattern, updatedCtx);
57374
- const existing = pendingSlots.get(slot) ?? [];
57375
- existing.push({ pattern: resolved, props: {} });
57376
- pendingSlots.set(slot, existing);
57651
+ },
57652
+ navigate: optionsRef.current?.navigate,
57653
+ notify: optionsRef.current?.notify,
57654
+ callService: optionsRef.current?.callService
57655
+ });
57656
+ const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
57657
+ let handlers = clientHandlers;
57658
+ if (persistence) {
57659
+ const sharedBindings = {
57660
+ // Seed `@entity` from the trait's scalar field state (a real
57661
+ // EntityRow), the same source the executor's own bindingCtx
57662
+ // uses below. `@payload.*` resolves from `payload` separately,
57663
+ // so dropping the prior `payload as EntityRow` cast loses
57664
+ // nothing — it just stops mislabelling the payload as an entity.
57665
+ entity: traitFieldStatesRef.current.get(traitName) ?? {},
57666
+ payload: payload || {},
57667
+ state: previousState
57668
+ };
57669
+ const sharedDeclared = runtime.collectDeclaredConfigDefaults(binding.trait);
57670
+ const sharedCallSite = binding.config;
57671
+ if (sharedDeclared || sharedCallSite) {
57672
+ sharedBindings.config = {
57673
+ ...sharedDeclared ?? {},
57674
+ ...sharedCallSite ?? {}
57675
+ };
57377
57676
  }
57677
+ const serverHandlers = runtime.createServerEffectHandlers({
57678
+ persistence,
57679
+ eventBus,
57680
+ entityType: linkedEntity,
57681
+ entityId,
57682
+ bindings: sharedBindings,
57683
+ context: {
57684
+ traitName,
57685
+ state: previousState,
57686
+ transition: `${previousState}->${newState}`,
57687
+ linkedEntity,
57688
+ entityId
57689
+ },
57690
+ source: { trait: traitName },
57691
+ callService: optionsRef.current?.callService
57692
+ });
57693
+ handlers = {
57694
+ ...serverHandlers,
57695
+ emit: clientHandlers.emit,
57696
+ renderUI: clientHandlers.renderUI,
57697
+ navigate: clientHandlers.navigate,
57698
+ notify: clientHandlers.notify
57699
+ };
57378
57700
  }
57379
- for (const [slot, patterns] of pendingSlots) {
57380
- flushSlot(binding.trait.name, slot, patterns, {
57381
- event: `tick:${tick.name}`,
57382
- state: currentState,
57383
- entity: binding.linkedEntity
57701
+ const baseSet = handlers.set;
57702
+ handlers = {
57703
+ ...handlers,
57704
+ set: async (targetId, field, value) => {
57705
+ let fieldState = traitFieldStatesRef.current.get(traitName);
57706
+ if (!fieldState) {
57707
+ fieldState = {};
57708
+ traitFieldStatesRef.current.set(traitName, fieldState);
57709
+ }
57710
+ fieldState[field] = value;
57711
+ log15.debug("set:write", {
57712
+ traitName,
57713
+ field,
57714
+ value: JSON.stringify(value),
57715
+ transition: `${previousState}->${newState}`
57716
+ });
57717
+ if (baseSet) await baseSet(targetId, field, value);
57718
+ }
57719
+ };
57720
+ const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
57721
+ const bindingCtx = {
57722
+ entity: entityForBinding,
57723
+ payload: payload || {},
57724
+ state: previousState
57725
+ };
57726
+ const declaredDefaults = runtime.collectDeclaredConfigDefaults(binding.trait);
57727
+ const callSiteConfig = binding.config;
57728
+ if (declaredDefaults || callSiteConfig) {
57729
+ bindingCtx.config = {
57730
+ ...declaredDefaults ?? {},
57731
+ ...callSiteConfig ?? {}
57732
+ };
57733
+ }
57734
+ const effectContext = {
57735
+ traitName,
57736
+ state: previousState,
57737
+ transition: `${previousState}->${newState}`,
57738
+ linkedEntity,
57739
+ entityId
57740
+ };
57741
+ const emittedDuringExec = [];
57742
+ const baseEmit = handlers.emit;
57743
+ const trackingHandlers = {
57744
+ ...handlers,
57745
+ emit: (event, eventPayload, source) => {
57746
+ emittedDuringExec.push(event);
57747
+ baseEmit(event, eventPayload, source);
57748
+ }
57749
+ };
57750
+ const executor = new runtime.EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
57751
+ try {
57752
+ await executor.executeAll(effects);
57753
+ log15.debug("effects:executed", () => ({
57754
+ traitName,
57755
+ transition: `${previousState}->${newState}`,
57756
+ event: flushEvent,
57757
+ effectCount: effects.length,
57758
+ emitted: emittedDuringExec.join(","),
57759
+ entityAfter: JSON.stringify(traitFieldStatesRef.current.get(traitName) ?? {}),
57760
+ slotsTouched: Array.from(pendingSlots.keys()).join(",")
57761
+ }));
57762
+ for (const [slot, patterns] of pendingSlots) {
57763
+ log15.debug("flush:slot", {
57764
+ traitName,
57765
+ slot,
57766
+ patternCount: patterns.length,
57767
+ event: flushEvent,
57768
+ transition: `${previousState}->${newState}`,
57769
+ cleared: patterns.length === 0
57770
+ });
57771
+ flushSlot(traitName, slot, patterns, {
57772
+ event: flushEvent,
57773
+ state: previousState,
57774
+ entity: binding.linkedEntity
57775
+ });
57776
+ }
57777
+ } catch (error) {
57778
+ log15.error("effects:error", {
57779
+ traitName,
57780
+ transition: `${previousState}->${newState}`,
57781
+ event: flushEvent,
57782
+ error: error instanceof Error ? error.message : String(error),
57783
+ effectCount: effects.length
57384
57784
  });
57385
57785
  }
57386
- }, [flushSlot]);
57786
+ return emittedDuringExec;
57787
+ }, [eventBus, flushSlot]);
57788
+ const runTickEffects = React90.useCallback((tick, binding) => {
57789
+ const traitName = binding.trait.name;
57790
+ const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
57791
+ if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
57792
+ if (tick.guard !== void 0) {
57793
+ const guardCtx = {
57794
+ entity: traitFieldStatesRef.current.get(traitName) ?? {},
57795
+ payload: {},
57796
+ state: currentState
57797
+ };
57798
+ if (binding.config) {
57799
+ guardCtx.config = binding.config;
57800
+ }
57801
+ const passed = runtime.interpolateValue(tick.guard, runtime.createContextFromBindings(guardCtx));
57802
+ if (!passed) {
57803
+ tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
57804
+ return;
57805
+ }
57806
+ }
57807
+ void executeTransitionEffects({
57808
+ binding,
57809
+ effects: tick.effects,
57810
+ previousState: currentState,
57811
+ newState: currentState,
57812
+ flushEvent: `tick:${tick.name}`,
57813
+ syncOnly: true,
57814
+ log: tickLog
57815
+ });
57816
+ }, [executeTransitionEffects]);
57387
57817
  React90.useEffect(() => {
57388
57818
  const hasFrameTicks = traitBindingsRef.current.some(
57389
57819
  (b) => b.trait.ticks?.some((t) => t.interval === "frame")
@@ -57494,161 +57924,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57494
57924
  transition: `${result.previousState} -> ${result.newState}`,
57495
57925
  effects: JSON.stringify(result.effects)
57496
57926
  }));
57497
- const linkedEntity = binding.linkedEntity || "";
57498
- const entityId = payload?.entityId;
57499
- const pendingSlots = /* @__PURE__ */ new Map();
57500
- const slotSource = {
57501
- trait: binding.trait.name,
57502
- state: result.previousState,
57503
- transition: `${result.previousState}->${result.newState}`,
57927
+ const emittedDuringExec = await executeTransitionEffects({
57928
+ binding,
57504
57929
  effects: result.effects,
57505
- traitDefinition: binding.trait
57506
- };
57507
- const clientHandlers = createClientEffectHandlers({
57508
- eventBus,
57509
- slotSetter: {
57510
- addPattern: (slot, pattern, props) => {
57511
- const existing = pendingSlots.get(slot) || [];
57512
- existing.push({ pattern, props: props || {} });
57513
- pendingSlots.set(slot, existing);
57514
- },
57515
- clearSlot: (slot) => {
57516
- pendingSlots.set(slot, []);
57517
- }
57518
- },
57519
- navigate: optionsRef.current?.navigate,
57520
- notify: optionsRef.current?.notify,
57521
- callService: optionsRef.current?.callService
57930
+ previousState: result.previousState,
57931
+ newState: result.newState,
57932
+ payload,
57933
+ flushEvent: eventKey,
57934
+ syncOnly: false,
57935
+ log: stateLog
57522
57936
  });
57523
- const persistence = optionsRef.current?.persistence;
57524
- let handlers = clientHandlers;
57525
- if (persistence) {
57526
- const sharedBindings = {
57527
- entity: payload ?? {},
57528
- payload: payload || {},
57529
- state: result.previousState
57530
- };
57531
- const sharedDeclared = runtime.collectDeclaredConfigDefaults(
57532
- binding.trait
57533
- );
57534
- const sharedCallSite = binding.config;
57535
- if (sharedDeclared || sharedCallSite) {
57536
- sharedBindings.config = {
57537
- ...sharedDeclared ?? {},
57538
- ...sharedCallSite ?? {}
57539
- };
57540
- }
57541
- const serverHandlers = runtime.createServerEffectHandlers({
57542
- persistence,
57543
- eventBus,
57544
- entityType: linkedEntity,
57545
- entityId,
57546
- bindings: sharedBindings,
57547
- context: {
57548
- traitName: binding.trait.name,
57549
- state: result.previousState,
57550
- transition: `${result.previousState}->${result.newState}`,
57551
- linkedEntity,
57552
- entityId
57553
- },
57554
- source: { trait: binding.trait.name },
57555
- callService: optionsRef.current?.callService
57556
- });
57557
- handlers = {
57558
- ...serverHandlers,
57559
- // Client handlers own UI + emit: keep the slot setter
57560
- // and pre-prefixed UI:* emit path intact.
57561
- emit: clientHandlers.emit,
57562
- renderUI: clientHandlers.renderUI,
57563
- navigate: clientHandlers.navigate,
57564
- notify: clientHandlers.notify
57565
- };
57566
- }
57567
- const baseSet = handlers.set;
57568
- handlers = {
57569
- ...handlers,
57570
- set: async (targetId, field, value) => {
57571
- let fieldState = traitFieldStatesRef.current.get(traitName);
57572
- if (!fieldState) {
57573
- fieldState = {};
57574
- traitFieldStatesRef.current.set(traitName, fieldState);
57575
- }
57576
- fieldState[field] = value;
57577
- if (baseSet) await baseSet(targetId, field, value);
57578
- }
57579
- };
57580
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
57581
- const bindingCtx = {
57582
- entity: entityForBinding,
57583
- payload: payload || {},
57584
- state: result.previousState
57585
- };
57586
- const declaredDefaults = runtime.collectDeclaredConfigDefaults(
57587
- binding.trait
57588
- );
57589
- const callSiteConfig = binding.config;
57590
- if (declaredDefaults || callSiteConfig) {
57591
- bindingCtx.config = {
57592
- ...declaredDefaults ?? {},
57593
- ...callSiteConfig ?? {}
57594
- };
57595
- }
57596
- const effectContext = {
57597
- traitName: binding.trait.name,
57598
- state: result.previousState,
57599
- transition: `${result.previousState}->${result.newState}`,
57600
- linkedEntity,
57601
- entityId
57602
- };
57603
- const emittedDuringExec = [];
57604
57937
  emittedByTrait.set(traitName, emittedDuringExec);
57605
- const baseEmit = handlers.emit;
57606
- const trackingHandlers = {
57607
- ...handlers,
57608
- emit: (event, eventPayload, source) => {
57609
- emittedDuringExec.push(event);
57610
- baseEmit(event, eventPayload, source);
57611
- }
57612
- };
57613
- const executor = new runtime.EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
57614
- try {
57615
- await executor.executeAll(result.effects);
57616
- stateLog.debug("transition:render-ui-dispatched", () => ({
57617
- traitName,
57618
- fromState: result.previousState,
57619
- toState: result.newState,
57620
- event: eventKey,
57621
- slotsTouched: Array.from(pendingSlots.keys()).join(","),
57622
- patternTypes: Array.from(pendingSlots.entries()).map(
57623
- ([slot, patterns]) => `${slot}:[${patterns.map((p2) => p2.pattern?.type ?? "null").join(",")}]`
57624
- ).join(";")
57625
- }));
57626
- void slotSource;
57627
- for (const [slot, patterns] of pendingSlots) {
57628
- stateLog.debug("flush:slot", {
57629
- traitName,
57630
- slot,
57631
- patternCount: patterns.length,
57632
- event: eventKey,
57633
- transition: `${result.previousState}->${result.newState}`,
57634
- cleared: patterns.length === 0
57635
- });
57636
- flushSlot(traitName, slot, patterns, {
57637
- event: eventKey,
57638
- state: result.previousState,
57639
- entity: binding.linkedEntity
57640
- });
57641
- }
57642
- } catch (error) {
57643
- stateLog.error("transition:effect-error", {
57644
- traitName,
57645
- fromState: result.previousState,
57646
- toState: result.newState,
57647
- event: eventKey,
57648
- error: error instanceof Error ? error.message : String(error),
57649
- effectCount: result.effects.length
57650
- });
57651
- }
57652
57938
  } else if (!result.executed) {
57653
57939
  if (result.guardResult === false) {
57654
57940
  stateLog.debug("guard-blocked-transition", {
@@ -57786,7 +58072,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57786
58072
  if (!orbitalName) continue;
57787
58073
  for (const transition of binding.trait.transitions) {
57788
58074
  const eventKey = transition.event;
57789
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
58075
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
57790
58076
  continue;
57791
58077
  }
57792
58078
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -57841,6 +58127,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57841
58127
  crossTraitLog.debug("cleanup:done", {});
57842
58128
  };
57843
58129
  }, [traitBindings, eventBus, enqueueAndDrain]);
58130
+ React90.useEffect(() => {
58131
+ const mgr = managerRef.current;
58132
+ const inited = initedTraitsRef.current;
58133
+ for (const binding of traitBindings) {
58134
+ const traitName = binding.trait.name;
58135
+ if (inited.has(traitName)) continue;
58136
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
58137
+ (evt) => mgr.canHandleEvent(traitName, evt)
58138
+ );
58139
+ if (lifecycleEvent === void 0) continue;
58140
+ inited.add(traitName);
58141
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
58142
+ enqueueAndDrain(lifecycleEvent, {});
58143
+ }
58144
+ return () => {
58145
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
58146
+ };
58147
+ }, [traitBindings, enqueueAndDrain]);
57844
58148
  return {
57845
58149
  traitStates,
57846
58150
  sendEvent,