@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.
@@ -41,7 +41,7 @@ var core$1 = require('@dnd-kit/core');
41
41
  var sortable = require('@dnd-kit/sortable');
42
42
  var utilities = require('@dnd-kit/utilities');
43
43
  var react = require('@xyflow/react');
44
- var THREE = require('three');
44
+ var THREE3 = require('three');
45
45
  var GLTFLoader_js = require('three/examples/jsm/loaders/GLTFLoader.js');
46
46
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
47
47
  var GLTFLoader = require('three/examples/jsm/loaders/GLTFLoader');
@@ -96,7 +96,7 @@ var ReactMarkdown__default = /*#__PURE__*/_interopDefault(ReactMarkdown);
96
96
  var remarkGfm__default = /*#__PURE__*/_interopDefault(remarkGfm);
97
97
  var remarkMath__default = /*#__PURE__*/_interopDefault(remarkMath);
98
98
  var rehypeKatex__default = /*#__PURE__*/_interopDefault(rehypeKatex);
99
- var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
99
+ var THREE3__namespace = /*#__PURE__*/_interopNamespace(THREE3);
100
100
 
101
101
  var __defProp = Object.defineProperty;
102
102
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -7100,6 +7100,52 @@ var init_ControlButton = __esm({
7100
7100
  ControlButton.displayName = "ControlButton";
7101
7101
  }
7102
7102
  });
7103
+
7104
+ // components/game/organisms/utils/spriteAnimation.ts
7105
+ function inferDirection(dx, dy) {
7106
+ if (dx === 0 && dy === 0) return "se";
7107
+ if (dx >= 0 && dy >= 0) return "se";
7108
+ if (dx <= 0 && dy >= 0) return "sw";
7109
+ if (dx >= 0 && dy <= 0) return "ne";
7110
+ return "nw";
7111
+ }
7112
+ function resolveSheetDirection(facing) {
7113
+ switch (facing) {
7114
+ case "se":
7115
+ return { sheetDir: "se", flipX: false };
7116
+ case "sw":
7117
+ return { sheetDir: "sw", flipX: false };
7118
+ case "ne":
7119
+ return { sheetDir: "sw", flipX: true };
7120
+ case "nw":
7121
+ return { sheetDir: "se", flipX: true };
7122
+ }
7123
+ }
7124
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
7125
+ return {
7126
+ sx: frame % columns * frameWidth,
7127
+ sy: row * frameHeight,
7128
+ sw: frameWidth,
7129
+ sh: frameHeight
7130
+ };
7131
+ }
7132
+ function getCurrentFrameFromDef(def, elapsed) {
7133
+ const frameDuration = 1e3 / def.frameRate;
7134
+ const totalDuration = def.frames * frameDuration;
7135
+ if (def.loop) {
7136
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
7137
+ return { frame: frame2, finished: false };
7138
+ }
7139
+ if (elapsed >= totalDuration) {
7140
+ return { frame: def.frames - 1, finished: true };
7141
+ }
7142
+ const frame = Math.floor(elapsed / frameDuration);
7143
+ return { frame, finished: false };
7144
+ }
7145
+ var init_spriteAnimation = __esm({
7146
+ "components/game/organisms/utils/spriteAnimation.ts"() {
7147
+ }
7148
+ });
7103
7149
  function Sprite({
7104
7150
  spritesheet = "https://almadar-kflow-assets.web.app/shared/isometric-blocks/Spritesheet/allTiles_sheet.png",
7105
7151
  frameWidth = 64,
@@ -7120,12 +7166,8 @@ function Sprite({
7120
7166
  }) {
7121
7167
  const eventBus = useEventBus();
7122
7168
  const sourcePosition = React81.useMemo(() => {
7123
- const frameX = frame % columns;
7124
- const frameY = Math.floor(frame / columns);
7125
- return {
7126
- x: frameX * frameWidth,
7127
- y: frameY * frameHeight
7128
- };
7169
+ const { sx, sy } = frameRect(frame, Math.floor(frame / columns), columns, frameWidth, frameHeight);
7170
+ return { x: sx, y: sy };
7129
7171
  }, [frame, columns, frameWidth, frameHeight]);
7130
7172
  const transform = React81.useMemo(() => {
7131
7173
  const transforms = [
@@ -7173,6 +7215,7 @@ var init_Sprite = __esm({
7173
7215
  "components/game/atoms/Sprite.tsx"() {
7174
7216
  "use client";
7175
7217
  init_useEventBus();
7218
+ init_spriteAnimation();
7176
7219
  }
7177
7220
  });
7178
7221
  function StateIndicator({
@@ -10564,6 +10607,163 @@ var init_useCamera = __esm({
10564
10607
  "use client";
10565
10608
  }
10566
10609
  });
10610
+ function unitAtlasUrl(unit) {
10611
+ if (unit.spriteSheet) return unit.spriteSheet;
10612
+ const sprite = unit.sprite;
10613
+ if (!sprite) return null;
10614
+ const match = /^(.*-sprite-sheet)(?:-(?:se|sw))?(?:-v\d+)?\.png$/.exec(sprite);
10615
+ if (!match) return null;
10616
+ return `${match[1]}.json`;
10617
+ }
10618
+ function resolveSheetUrl(atlasUrl, relativeSheetPath) {
10619
+ try {
10620
+ return new URL(relativeSheetPath, atlasUrl).toString();
10621
+ } catch {
10622
+ const base = atlasUrl.slice(0, atlasUrl.lastIndexOf("/") + 1);
10623
+ return `${base}${relativeSheetPath.replace(/^\.\//, "")}`;
10624
+ }
10625
+ }
10626
+ function useUnitSpriteAtlas(units) {
10627
+ const atlasCacheRef = React81.useRef(/* @__PURE__ */ new Map());
10628
+ const loadingRef = React81.useRef(/* @__PURE__ */ new Set());
10629
+ const [pendingCount, setPendingCount] = React81.useState(0);
10630
+ const [, forceTick] = React81.useState(0);
10631
+ const animStatesRef = React81.useRef(/* @__PURE__ */ new Map());
10632
+ const lastTickRef = React81.useRef(0);
10633
+ const rafRef = React81.useRef(0);
10634
+ const atlasUrls = React81.useMemo(() => {
10635
+ const set = /* @__PURE__ */ new Set();
10636
+ for (const unit of units) {
10637
+ const url = unitAtlasUrl(unit);
10638
+ if (url) set.add(url);
10639
+ }
10640
+ return [...set];
10641
+ }, [units]);
10642
+ React81.useEffect(() => {
10643
+ const cache = atlasCacheRef.current;
10644
+ const loading = loadingRef.current;
10645
+ const toLoad = atlasUrls.filter((url) => !cache.has(url) && !loading.has(url));
10646
+ if (toLoad.length === 0) return;
10647
+ let cancelled = false;
10648
+ setPendingCount((prev) => prev + toLoad.length);
10649
+ for (const url of toLoad) {
10650
+ loading.add(url);
10651
+ fetch(url).then((res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))).then((atlas) => {
10652
+ if (cancelled) return;
10653
+ cache.set(url, atlas);
10654
+ }).catch(() => {
10655
+ }).finally(() => {
10656
+ if (cancelled) return;
10657
+ loading.delete(url);
10658
+ setPendingCount((prev) => Math.max(0, prev - 1));
10659
+ forceTick((n) => n + 1);
10660
+ });
10661
+ }
10662
+ return () => {
10663
+ cancelled = true;
10664
+ };
10665
+ }, [atlasUrls]);
10666
+ const sheetUrls = React81.useMemo(() => {
10667
+ const urls = /* @__PURE__ */ new Set();
10668
+ for (const unit of units) {
10669
+ const atlasUrl = unitAtlasUrl(unit);
10670
+ if (!atlasUrl) continue;
10671
+ const atlas = atlasCacheRef.current.get(atlasUrl);
10672
+ if (!atlas) continue;
10673
+ for (const rel of Object.values(atlas.sheets)) {
10674
+ if (rel) urls.add(resolveSheetUrl(atlasUrl, rel));
10675
+ }
10676
+ }
10677
+ return [...urls];
10678
+ }, [units, pendingCount]);
10679
+ React81.useEffect(() => {
10680
+ const hasAtlasUnits = units.some((u) => unitAtlasUrl(u) !== null);
10681
+ if (!hasAtlasUnits) return;
10682
+ let running = true;
10683
+ const tick = (ts) => {
10684
+ if (!running) return;
10685
+ const last = lastTickRef.current || ts;
10686
+ const delta = ts - last;
10687
+ lastTickRef.current = ts;
10688
+ const states = animStatesRef.current;
10689
+ const currentIds = /* @__PURE__ */ new Set();
10690
+ for (const unit of units) {
10691
+ if (unitAtlasUrl(unit) === null) continue;
10692
+ currentIds.add(unit.id);
10693
+ let state = states.get(unit.id);
10694
+ if (!state) {
10695
+ state = { animation: "idle", direction: "se", elapsed: 0, walkHold: 0, prev: null };
10696
+ states.set(unit.id, state);
10697
+ }
10698
+ const posX = unit.position?.x ?? unit.x ?? 0;
10699
+ const posY = unit.position?.y ?? unit.y ?? 0;
10700
+ if (state.prev) {
10701
+ const dx = posX - state.prev.x;
10702
+ const dy = posY - state.prev.y;
10703
+ if (dx !== 0 || dy !== 0) {
10704
+ state.animation = "walk";
10705
+ state.direction = inferDirection(dx, dy);
10706
+ state.walkHold = WALK_HOLD_MS;
10707
+ } else if (state.animation === "walk") {
10708
+ state.walkHold -= delta;
10709
+ if (state.walkHold <= 0) state.animation = "idle";
10710
+ }
10711
+ }
10712
+ state.prev = { x: posX, y: posY };
10713
+ state.elapsed += delta;
10714
+ }
10715
+ for (const id of states.keys()) {
10716
+ if (!currentIds.has(id)) states.delete(id);
10717
+ }
10718
+ rafRef.current = requestAnimationFrame(tick);
10719
+ };
10720
+ rafRef.current = requestAnimationFrame(tick);
10721
+ return () => {
10722
+ running = false;
10723
+ cancelAnimationFrame(rafRef.current);
10724
+ lastTickRef.current = 0;
10725
+ };
10726
+ }, [units]);
10727
+ const resolveUnitFrame = React81.useCallback((unitId) => {
10728
+ const unit = units.find((u) => u.id === unitId);
10729
+ if (!unit) return null;
10730
+ const atlasUrl = unitAtlasUrl(unit);
10731
+ if (!atlasUrl) return null;
10732
+ const atlas = atlasCacheRef.current.get(atlasUrl);
10733
+ if (!atlas) return null;
10734
+ const state = animStatesRef.current.get(unitId);
10735
+ const animation = state?.animation ?? "idle";
10736
+ const direction = state?.direction ?? "se";
10737
+ const elapsed = state?.elapsed ?? 0;
10738
+ const def = atlas.animations[animation] ?? atlas.animations.idle;
10739
+ if (!def) return null;
10740
+ const { sheetDir, flipX } = resolveSheetDirection(direction);
10741
+ const rel = atlas.sheets[sheetDir] ?? atlas.sheets.se ?? atlas.sheets.sw;
10742
+ if (!rel) return null;
10743
+ const sheetUrl = resolveSheetUrl(atlasUrl, rel);
10744
+ const isIdle = animation === "idle";
10745
+ const frame = isIdle ? 0 : getCurrentFrameFromDef(def, elapsed).frame;
10746
+ const rect = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
10747
+ return {
10748
+ sheetUrl,
10749
+ sx: rect.sx,
10750
+ sy: rect.sy,
10751
+ sw: rect.sw,
10752
+ sh: rect.sh,
10753
+ flipX,
10754
+ applyBreathing: isIdle
10755
+ };
10756
+ }, [units]);
10757
+ return { sheetUrls, resolveUnitFrame, pendingCount };
10758
+ }
10759
+ var WALK_HOLD_MS;
10760
+ var init_useUnitSpriteAtlas = __esm({
10761
+ "components/game/molecules/useUnitSpriteAtlas.ts"() {
10762
+ "use client";
10763
+ init_spriteAnimation();
10764
+ WALK_HOLD_MS = 600;
10765
+ }
10766
+ });
10567
10767
 
10568
10768
  // components/game/organisms/utils/isometric.ts
10569
10769
  function isoToScreen(tileX, tileY, scale, baseOffsetX) {
@@ -10678,6 +10878,10 @@ function IsometricCanvas({
10678
10878
  () => unitsProp.map((u) => u.position ? u : { ...u, position: { x: u.x ?? 0, y: u.y ?? 0 } }),
10679
10879
  [unitsProp]
10680
10880
  );
10881
+ const { sheetUrls: atlasSheetUrls, resolveUnitFrame: resolveUnitFrameInternal, pendingCount: atlasPending } = useUnitSpriteAtlas(units);
10882
+ const resolveFrameForUnit = React81.useCallback((unitId) => {
10883
+ return resolveUnitFrame?.(unitId) ?? resolveUnitFrameInternal(unitId);
10884
+ }, [resolveUnitFrame, resolveUnitFrameInternal]);
10681
10885
  const features = React81.useMemo(
10682
10886
  () => featuresProp.map((f3) => {
10683
10887
  if (f3.type) return f3;
@@ -10761,9 +10965,10 @@ function IsometricCanvas({
10761
10965
  }
10762
10966
  }
10763
10967
  if (effectSpriteUrls) urls.push(...effectSpriteUrls);
10968
+ if (atlasSheetUrls.length) urls.push(...atlasSheetUrls);
10764
10969
  if (backgroundImage) urls.push(backgroundImage);
10765
10970
  return [...new Set(urls.filter(Boolean))];
10766
- }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, backgroundImage, assetManifest, resolveManifestUrl]);
10971
+ }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, atlasSheetUrls, backgroundImage, assetManifest, resolveManifestUrl]);
10767
10972
  const { getImage, pendingCount } = useImageCache(spriteUrls);
10768
10973
  React81.useEffect(() => {
10769
10974
  if (typeof window === "undefined") return;
@@ -11050,7 +11255,7 @@ function IsometricCanvas({
11050
11255
  ctx.lineWidth = 3;
11051
11256
  ctx.stroke();
11052
11257
  }
11053
- const frame = resolveUnitFrame?.(unit.id) ?? null;
11258
+ const frame = resolveFrameForUnit(unit.id);
11054
11259
  const frameImg = frame ? getImage(frame.sheetUrl) : null;
11055
11260
  if (frame && frameImg) {
11056
11261
  const frameAr = frame.sw / frame.sh;
@@ -11160,7 +11365,7 @@ function IsometricCanvas({
11160
11365
  resolveTerrainSpriteUrl,
11161
11366
  resolveFeatureSpriteUrl,
11162
11367
  resolveUnitSpriteUrl,
11163
- resolveUnitFrame,
11368
+ resolveFrameForUnit,
11164
11369
  getImage,
11165
11370
  gridWidth,
11166
11371
  gridHeight,
@@ -11192,7 +11397,7 @@ function IsometricCanvas({
11192
11397
  };
11193
11398
  }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
11194
11399
  React81.useEffect(() => {
11195
- const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0;
11400
+ const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
11196
11401
  draw(animTimeRef.current);
11197
11402
  if (!hasAnimations) return;
11198
11403
  let running = true;
@@ -11208,7 +11413,7 @@ function IsometricCanvas({
11208
11413
  running = false;
11209
11414
  cancelAnimationFrame(rafIdRef.current);
11210
11415
  };
11211
- }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, pendingCount, lerpToTarget, targetCameraRef]);
11416
+ }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, pendingCount, atlasPending, lerpToTarget, targetCameraRef]);
11212
11417
  const handleMouseMoveWithCamera = React81.useCallback((e) => {
11213
11418
  if (enableCamera) {
11214
11419
  const wasPanning = handleMouseMove(e, () => draw(animTimeRef.current));
@@ -11353,6 +11558,7 @@ var init_IsometricCanvas = __esm({
11353
11558
  init_ErrorState();
11354
11559
  init_useImageCache();
11355
11560
  init_useCamera();
11561
+ init_useUnitSpriteAtlas();
11356
11562
  init_verificationRegistry();
11357
11563
  init_isometric();
11358
11564
  IsometricCanvas.displayName = "IsometricCanvas";
@@ -28935,13 +29141,13 @@ var init_MapView = __esm({
28935
29141
  shadowSize: [41, 41]
28936
29142
  });
28937
29143
  L.Marker.prototype.options.icon = defaultIcon;
28938
- const { useEffect: useEffect80, useRef: useRef74, useCallback: useCallback118, useState: useState112 } = React81__namespace.default;
29144
+ const { useEffect: useEffect81, useRef: useRef75, useCallback: useCallback119, useState: useState113 } = React81__namespace.default;
28939
29145
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
28940
29146
  const { useEventBus: useEventBus4 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
28941
29147
  function MapUpdater({ centerLat, centerLng, zoom }) {
28942
29148
  const map = useMap();
28943
- const prevRef = useRef74({ centerLat, centerLng, zoom });
28944
- useEffect80(() => {
29149
+ const prevRef = useRef75({ centerLat, centerLng, zoom });
29150
+ useEffect81(() => {
28945
29151
  const prev = prevRef.current;
28946
29152
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
28947
29153
  map.setView([centerLat, centerLng], zoom);
@@ -28952,7 +29158,7 @@ var init_MapView = __esm({
28952
29158
  }
28953
29159
  function MapClickHandler({ onMapClick }) {
28954
29160
  const map = useMap();
28955
- useEffect80(() => {
29161
+ useEffect81(() => {
28956
29162
  if (!onMapClick) return;
28957
29163
  const handler = (e) => {
28958
29164
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -28980,8 +29186,8 @@ var init_MapView = __esm({
28980
29186
  showAttribution = true
28981
29187
  }) {
28982
29188
  const eventBus = useEventBus4();
28983
- const [clickedPosition, setClickedPosition] = useState112(null);
28984
- const handleMapClick = useCallback118((lat, lng) => {
29189
+ const [clickedPosition, setClickedPosition] = useState113(null);
29190
+ const handleMapClick = useCallback119((lat, lng) => {
28985
29191
  if (showClickedPin) {
28986
29192
  setClickedPosition({ lat, lng });
28987
29193
  }
@@ -28990,7 +29196,7 @@ var init_MapView = __esm({
28990
29196
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
28991
29197
  }
28992
29198
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
28993
- const handleMarkerClick = useCallback118((marker) => {
29199
+ const handleMarkerClick = useCallback119((marker) => {
28994
29200
  onMarkerClick?.(marker);
28995
29201
  if (markerClickEvent) {
28996
29202
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -40708,7 +40914,7 @@ var init_AssetLoader = __esm({
40708
40914
  __publicField(this, "textureCache");
40709
40915
  __publicField(this, "loadingPromises");
40710
40916
  this.objLoader = new OBJLoader_js.OBJLoader();
40711
- this.textureLoader = new THREE__namespace.TextureLoader();
40917
+ this.textureLoader = new THREE3__namespace.TextureLoader();
40712
40918
  this.modelCache = /* @__PURE__ */ new Map();
40713
40919
  this.textureCache = /* @__PURE__ */ new Map();
40714
40920
  this.loadingPromises = /* @__PURE__ */ new Map();
@@ -40782,7 +40988,7 @@ var init_AssetLoader = __esm({
40782
40988
  return this.loadingPromises.get(`texture:${url}`);
40783
40989
  }
40784
40990
  const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
40785
- texture.colorSpace = THREE__namespace.SRGBColorSpace;
40991
+ texture.colorSpace = THREE3__namespace.SRGBColorSpace;
40786
40992
  this.textureCache.set(url, texture);
40787
40993
  this.loadingPromises.delete(`texture:${url}`);
40788
40994
  return texture;
@@ -40856,7 +41062,7 @@ var init_AssetLoader = __esm({
40856
41062
  });
40857
41063
  this.modelCache.forEach((model) => {
40858
41064
  model.scene.traverse((child) => {
40859
- if (child instanceof THREE__namespace.Mesh) {
41065
+ if (child instanceof THREE3__namespace.Mesh) {
40860
41066
  child.geometry.dispose();
40861
41067
  if (Array.isArray(child.material)) {
40862
41068
  child.material.forEach((m) => m.dispose());
@@ -41412,7 +41618,7 @@ function ModelLoader({
41412
41618
  if (!loadedModel) return null;
41413
41619
  const cloned = loadedModel.clone();
41414
41620
  cloned.traverse((child) => {
41415
- if (child instanceof THREE__namespace.Mesh) {
41621
+ if (child instanceof THREE3__namespace.Mesh) {
41416
41622
  child.castShadow = castShadow;
41417
41623
  child.receiveShadow = receiveShadow;
41418
41624
  }
@@ -41526,6 +41732,47 @@ function CameraController({
41526
41732
  }, [camera.position, onCameraChange]);
41527
41733
  return null;
41528
41734
  }
41735
+ function UnitSpriteBillboard({
41736
+ sheetUrl,
41737
+ resolveFrame,
41738
+ height = 1.2
41739
+ }) {
41740
+ const texture = fiber.useLoader(THREE3__namespace.TextureLoader, sheetUrl);
41741
+ const meshRef = React81.useRef(null);
41742
+ const matRef = React81.useRef(null);
41743
+ const [aspect, setAspect] = React81.useState(1);
41744
+ fiber.useFrame(() => {
41745
+ const frame = resolveFrame();
41746
+ if (!frame || !texture.image) return;
41747
+ const imgW = texture.image.width;
41748
+ const imgH = texture.image.height;
41749
+ if (!imgW || !imgH) return;
41750
+ texture.repeat.set((frame.flipX ? -1 : 1) * (frame.sw / imgW), frame.sh / imgH);
41751
+ texture.offset.set(
41752
+ frame.flipX ? (frame.sx + frame.sw) / imgW : frame.sx / imgW,
41753
+ 1 - (frame.sy + frame.sh) / imgH
41754
+ );
41755
+ texture.magFilter = THREE3__namespace.NearestFilter;
41756
+ texture.minFilter = THREE3__namespace.NearestFilter;
41757
+ texture.needsUpdate = true;
41758
+ const nextAspect = frame.sw / frame.sh;
41759
+ if (Math.abs(nextAspect - aspect) > 1e-3) setAspect(nextAspect);
41760
+ if (matRef.current) matRef.current.needsUpdate = true;
41761
+ });
41762
+ return /* @__PURE__ */ jsxRuntime.jsxs("mesh", { ref: meshRef, position: [0, height / 2, 0], children: [
41763
+ /* @__PURE__ */ jsxRuntime.jsx("planeGeometry", { args: [height * aspect, height] }),
41764
+ /* @__PURE__ */ jsxRuntime.jsx(
41765
+ "meshBasicMaterial",
41766
+ {
41767
+ ref: matRef,
41768
+ map: texture,
41769
+ transparent: true,
41770
+ alphaTest: 0.1,
41771
+ side: THREE3__namespace.DoubleSide
41772
+ }
41773
+ )
41774
+ ] });
41775
+ }
41529
41776
  var DEFAULT_GRID_CONFIG, GameCanvas3D;
41530
41777
  var init_GameCanvas3D2 = __esm({
41531
41778
  "components/game/molecules/GameCanvas3D.tsx"() {
@@ -41536,6 +41783,7 @@ var init_GameCanvas3D2 = __esm({
41536
41783
  init_Canvas3DLoadingState2();
41537
41784
  init_Canvas3DErrorBoundary2();
41538
41785
  init_ModelLoader();
41786
+ init_useUnitSpriteAtlas();
41539
41787
  init_cn();
41540
41788
  init_GameCanvas3D();
41541
41789
  DEFAULT_GRID_CONFIG = {
@@ -41592,8 +41840,10 @@ var init_GameCanvas3D2 = __esm({
41592
41840
  const controlsRef = React81.useRef(null);
41593
41841
  const [hoveredTile, setHoveredTile] = React81.useState(null);
41594
41842
  const [internalError, setInternalError] = React81.useState(null);
41843
+ const { sheetUrls: atlasSheetUrls, resolveUnitFrame } = useUnitSpriteAtlas(units);
41844
+ const preloadUrls = React81.useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
41595
41845
  const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
41596
- preloadUrls: preloadAssets,
41846
+ preloadUrls,
41597
41847
  loader: customAssetLoader
41598
41848
  });
41599
41849
  const eventHandlers = useGameCanvas3DEvents({
@@ -41652,7 +41902,7 @@ var init_GameCanvas3D2 = __esm({
41652
41902
  getCameraPosition: () => {
41653
41903
  if (controlsRef.current) {
41654
41904
  const pos = controlsRef.current.object.position;
41655
- return new THREE__namespace.Vector3(pos.x, pos.y, pos.z);
41905
+ return new THREE3__namespace.Vector3(pos.x, pos.y, pos.z);
41656
41906
  }
41657
41907
  return null;
41658
41908
  },
@@ -41804,6 +42054,8 @@ var init_GameCanvas3D2 = __esm({
41804
42054
  ({ unit, position }) => {
41805
42055
  const isSelected = selectedUnitId === unit.id;
41806
42056
  const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
42057
+ const hasAtlas = unitAtlasUrl(unit) !== null;
42058
+ const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
41807
42059
  return /* @__PURE__ */ jsxRuntime.jsxs(
41808
42060
  "group",
41809
42061
  {
@@ -41815,7 +42067,16 @@ var init_GameCanvas3D2 = __esm({
41815
42067
  /* @__PURE__ */ jsxRuntime.jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
41816
42068
  /* @__PURE__ */ jsxRuntime.jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
41817
42069
  ] }),
41818
- unit.modelUrl ? (
42070
+ hasAtlas && initialFrame ? (
42071
+ /* Animated sprite-sheet billboard — single cropped frame, by state */
42072
+ /* @__PURE__ */ jsxRuntime.jsx(drei.Billboard, { children: /* @__PURE__ */ jsxRuntime.jsx(
42073
+ UnitSpriteBillboard,
42074
+ {
42075
+ sheetUrl: initialFrame.sheetUrl,
42076
+ resolveFrame: () => resolveUnitFrame(unit.id)
42077
+ }
42078
+ ) })
42079
+ ) : unit.modelUrl ? (
41819
42080
  /* GLB unit model (box fallback while loading / on error) */
41820
42081
  /* @__PURE__ */ jsxRuntime.jsx(
41821
42082
  ModelLoader,
@@ -41869,7 +42130,7 @@ var init_GameCanvas3D2 = __esm({
41869
42130
  }
41870
42131
  );
41871
42132
  },
41872
- [selectedUnitId, handleUnitClick]
42133
+ [selectedUnitId, handleUnitClick, resolveUnitFrame]
41873
42134
  );
41874
42135
  const DefaultFeatureRenderer = React81.useCallback(
41875
42136
  ({
@@ -51198,6 +51459,28 @@ init_verificationRegistry();
51198
51459
  var crossTraitLog = logger.createLogger("almadar:ui:cross-trait");
51199
51460
  var flushLog = logger.createLogger("almadar:ui:slot-flush");
51200
51461
  var stateLog = logger.createLogger("almadar:ui:state-transitions");
51462
+ var tickLog = logger.createLogger("almadar:ui:tick-effects");
51463
+ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
51464
+ "set",
51465
+ "emit",
51466
+ "render-ui",
51467
+ "render",
51468
+ "navigate",
51469
+ "notify",
51470
+ "log",
51471
+ // Synchronous structural forms that wrap sync effects. A tick authored as
51472
+ // a single top-level `(let ((..)) (do (set ..) (render-ui ..)))` or
51473
+ // `(if cond (set ..) ..)` is one of these at its head — the EffectExecutor
51474
+ // resolves the binding values / condition through the canonical evaluator
51475
+ // and runs the wrapped sync effects. Without these in the allow-list the
51476
+ // whole tick is filtered out (its head op isn't `set`/`emit`/...), so the
51477
+ // physics/gameflow/AI tick never runs.
51478
+ "let",
51479
+ "if",
51480
+ "do",
51481
+ "when"
51482
+ ]);
51483
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
51201
51484
  function toTraitDefinition(binding) {
51202
51485
  return {
51203
51486
  name: binding.trait.name,
@@ -51230,6 +51513,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51230
51513
  });
51231
51514
  const eventQueueRef = React81.useRef([]);
51232
51515
  const processingRef = React81.useRef(false);
51516
+ const initedTraitsRef = React81.useRef(/* @__PURE__ */ new Set());
51233
51517
  const traitBindingsRef = React81.useRef(traitBindings);
51234
51518
  const managerRef = React81.useRef(manager);
51235
51519
  const uiSlotsRef = React81.useRef(uiSlots);
@@ -51394,50 +51678,196 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51394
51678
  for (const unreg of snapshotUnregs) unreg();
51395
51679
  };
51396
51680
  }, [traitBindings]);
51397
- const runTickEffects = React81.useCallback((tick, binding) => {
51398
- const currentState = traitStatesRef.current.get(binding.trait.name)?.currentState ?? "";
51399
- if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
51400
- const bindingCtx = { entity: {}, payload: {}, state: currentState };
51401
- if (binding.config) {
51402
- bindingCtx.config = binding.config;
51403
- }
51404
- const evalCtx = runtime.createContextFromBindings(bindingCtx);
51405
- if (tick.guard !== void 0) {
51406
- const passed = runtime.interpolateValue(tick.guard, evalCtx);
51407
- if (!passed) return;
51408
- }
51681
+ const executeTransitionEffects = React81.useCallback(async (params) => {
51682
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log14 } = params;
51683
+ const traitName = binding.trait.name;
51684
+ const linkedEntity = binding.linkedEntity || "";
51685
+ const entityId = payload?.entityId;
51686
+ const effects = syncOnly ? params.effects.filter(
51687
+ (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
51688
+ ) : params.effects;
51689
+ if (effects.length === 0) return [];
51409
51690
  const pendingSlots = /* @__PURE__ */ new Map();
51410
51691
  ({
51411
- trait: binding.trait.name,
51412
- transition: `${currentState}->tick:${tick.name}`,
51413
- effects: tick.effects,
51414
51692
  traitDefinition: binding.trait
51415
51693
  });
51416
- for (const effect of tick.effects) {
51417
- if (!Array.isArray(effect)) continue;
51418
- const op = effect[0];
51419
- if (op === "render-ui" || op === "render") {
51420
- const slot = effect[1];
51421
- const rawPattern = effect[2];
51422
- if (rawPattern === null || rawPattern === void 0) {
51694
+ const clientHandlers = createClientEffectHandlers({
51695
+ eventBus,
51696
+ slotSetter: {
51697
+ addPattern: (slot, pattern, props) => {
51698
+ const existing = pendingSlots.get(slot) || [];
51699
+ existing.push({ pattern, props: props || {} });
51700
+ pendingSlots.set(slot, existing);
51701
+ },
51702
+ clearSlot: (slot) => {
51423
51703
  pendingSlots.set(slot, []);
51424
- continue;
51425
51704
  }
51426
- const updatedCtx = runtime.createContextFromBindings(bindingCtx);
51427
- const resolved = runtime.interpolateValue(rawPattern, updatedCtx);
51428
- const existing = pendingSlots.get(slot) ?? [];
51429
- existing.push({ pattern: resolved, props: {} });
51430
- pendingSlots.set(slot, existing);
51705
+ },
51706
+ navigate: optionsRef.current?.navigate,
51707
+ notify: optionsRef.current?.notify,
51708
+ callService: optionsRef.current?.callService
51709
+ });
51710
+ const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
51711
+ let handlers = clientHandlers;
51712
+ if (persistence) {
51713
+ const sharedBindings = {
51714
+ // Seed `@entity` from the trait's scalar field state (a real
51715
+ // EntityRow), the same source the executor's own bindingCtx
51716
+ // uses below. `@payload.*` resolves from `payload` separately,
51717
+ // so dropping the prior `payload as EntityRow` cast loses
51718
+ // nothing — it just stops mislabelling the payload as an entity.
51719
+ entity: traitFieldStatesRef.current.get(traitName) ?? {},
51720
+ payload: payload || {},
51721
+ state: previousState
51722
+ };
51723
+ const sharedDeclared = runtime.collectDeclaredConfigDefaults(binding.trait);
51724
+ const sharedCallSite = binding.config;
51725
+ if (sharedDeclared || sharedCallSite) {
51726
+ sharedBindings.config = {
51727
+ ...sharedDeclared ?? {},
51728
+ ...sharedCallSite ?? {}
51729
+ };
51431
51730
  }
51731
+ const serverHandlers = runtime.createServerEffectHandlers({
51732
+ persistence,
51733
+ eventBus,
51734
+ entityType: linkedEntity,
51735
+ entityId,
51736
+ bindings: sharedBindings,
51737
+ context: {
51738
+ traitName,
51739
+ state: previousState,
51740
+ transition: `${previousState}->${newState}`,
51741
+ linkedEntity,
51742
+ entityId
51743
+ },
51744
+ source: { trait: traitName },
51745
+ callService: optionsRef.current?.callService
51746
+ });
51747
+ handlers = {
51748
+ ...serverHandlers,
51749
+ emit: clientHandlers.emit,
51750
+ renderUI: clientHandlers.renderUI,
51751
+ navigate: clientHandlers.navigate,
51752
+ notify: clientHandlers.notify
51753
+ };
51754
+ }
51755
+ const baseSet = handlers.set;
51756
+ handlers = {
51757
+ ...handlers,
51758
+ set: async (targetId, field, value) => {
51759
+ let fieldState = traitFieldStatesRef.current.get(traitName);
51760
+ if (!fieldState) {
51761
+ fieldState = {};
51762
+ traitFieldStatesRef.current.set(traitName, fieldState);
51763
+ }
51764
+ fieldState[field] = value;
51765
+ log14.debug("set:write", {
51766
+ traitName,
51767
+ field,
51768
+ value: JSON.stringify(value),
51769
+ transition: `${previousState}->${newState}`
51770
+ });
51771
+ if (baseSet) await baseSet(targetId, field, value);
51772
+ }
51773
+ };
51774
+ const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
51775
+ const bindingCtx = {
51776
+ entity: entityForBinding,
51777
+ payload: payload || {},
51778
+ state: previousState
51779
+ };
51780
+ const declaredDefaults = runtime.collectDeclaredConfigDefaults(binding.trait);
51781
+ const callSiteConfig = binding.config;
51782
+ if (declaredDefaults || callSiteConfig) {
51783
+ bindingCtx.config = {
51784
+ ...declaredDefaults ?? {},
51785
+ ...callSiteConfig ?? {}
51786
+ };
51432
51787
  }
51433
- for (const [slot, patterns] of pendingSlots) {
51434
- flushSlot(binding.trait.name, slot, patterns, {
51435
- event: `tick:${tick.name}`,
51436
- state: currentState,
51437
- entity: binding.linkedEntity
51788
+ const effectContext = {
51789
+ traitName,
51790
+ state: previousState,
51791
+ transition: `${previousState}->${newState}`,
51792
+ linkedEntity,
51793
+ entityId
51794
+ };
51795
+ const emittedDuringExec = [];
51796
+ const baseEmit = handlers.emit;
51797
+ const trackingHandlers = {
51798
+ ...handlers,
51799
+ emit: (event, eventPayload, source) => {
51800
+ emittedDuringExec.push(event);
51801
+ baseEmit(event, eventPayload, source);
51802
+ }
51803
+ };
51804
+ const executor = new runtime.EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
51805
+ try {
51806
+ await executor.executeAll(effects);
51807
+ log14.debug("effects:executed", () => ({
51808
+ traitName,
51809
+ transition: `${previousState}->${newState}`,
51810
+ event: flushEvent,
51811
+ effectCount: effects.length,
51812
+ emitted: emittedDuringExec.join(","),
51813
+ entityAfter: JSON.stringify(traitFieldStatesRef.current.get(traitName) ?? {}),
51814
+ slotsTouched: Array.from(pendingSlots.keys()).join(",")
51815
+ }));
51816
+ for (const [slot, patterns] of pendingSlots) {
51817
+ log14.debug("flush:slot", {
51818
+ traitName,
51819
+ slot,
51820
+ patternCount: patterns.length,
51821
+ event: flushEvent,
51822
+ transition: `${previousState}->${newState}`,
51823
+ cleared: patterns.length === 0
51824
+ });
51825
+ flushSlot(traitName, slot, patterns, {
51826
+ event: flushEvent,
51827
+ state: previousState,
51828
+ entity: binding.linkedEntity
51829
+ });
51830
+ }
51831
+ } catch (error) {
51832
+ log14.error("effects:error", {
51833
+ traitName,
51834
+ transition: `${previousState}->${newState}`,
51835
+ event: flushEvent,
51836
+ error: error instanceof Error ? error.message : String(error),
51837
+ effectCount: effects.length
51438
51838
  });
51439
51839
  }
51440
- }, [flushSlot]);
51840
+ return emittedDuringExec;
51841
+ }, [eventBus, flushSlot]);
51842
+ const runTickEffects = React81.useCallback((tick, binding) => {
51843
+ const traitName = binding.trait.name;
51844
+ const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
51845
+ if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
51846
+ if (tick.guard !== void 0) {
51847
+ const guardCtx = {
51848
+ entity: traitFieldStatesRef.current.get(traitName) ?? {},
51849
+ payload: {},
51850
+ state: currentState
51851
+ };
51852
+ if (binding.config) {
51853
+ guardCtx.config = binding.config;
51854
+ }
51855
+ const passed = runtime.interpolateValue(tick.guard, runtime.createContextFromBindings(guardCtx));
51856
+ if (!passed) {
51857
+ tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
51858
+ return;
51859
+ }
51860
+ }
51861
+ void executeTransitionEffects({
51862
+ binding,
51863
+ effects: tick.effects,
51864
+ previousState: currentState,
51865
+ newState: currentState,
51866
+ flushEvent: `tick:${tick.name}`,
51867
+ syncOnly: true,
51868
+ log: tickLog
51869
+ });
51870
+ }, [executeTransitionEffects]);
51441
51871
  React81.useEffect(() => {
51442
51872
  const hasFrameTicks = traitBindingsRef.current.some(
51443
51873
  (b) => b.trait.ticks?.some((t) => t.interval === "frame")
@@ -51548,161 +51978,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51548
51978
  transition: `${result.previousState} -> ${result.newState}`,
51549
51979
  effects: JSON.stringify(result.effects)
51550
51980
  }));
51551
- const linkedEntity = binding.linkedEntity || "";
51552
- const entityId = payload?.entityId;
51553
- const pendingSlots = /* @__PURE__ */ new Map();
51554
- const slotSource = {
51555
- trait: binding.trait.name,
51556
- state: result.previousState,
51557
- transition: `${result.previousState}->${result.newState}`,
51981
+ const emittedDuringExec = await executeTransitionEffects({
51982
+ binding,
51558
51983
  effects: result.effects,
51559
- traitDefinition: binding.trait
51560
- };
51561
- const clientHandlers = createClientEffectHandlers({
51562
- eventBus,
51563
- slotSetter: {
51564
- addPattern: (slot, pattern, props) => {
51565
- const existing = pendingSlots.get(slot) || [];
51566
- existing.push({ pattern, props: props || {} });
51567
- pendingSlots.set(slot, existing);
51568
- },
51569
- clearSlot: (slot) => {
51570
- pendingSlots.set(slot, []);
51571
- }
51572
- },
51573
- navigate: optionsRef.current?.navigate,
51574
- notify: optionsRef.current?.notify,
51575
- callService: optionsRef.current?.callService
51984
+ previousState: result.previousState,
51985
+ newState: result.newState,
51986
+ payload,
51987
+ flushEvent: eventKey,
51988
+ syncOnly: false,
51989
+ log: stateLog
51576
51990
  });
51577
- const persistence = optionsRef.current?.persistence;
51578
- let handlers = clientHandlers;
51579
- if (persistence) {
51580
- const sharedBindings = {
51581
- entity: payload ?? {},
51582
- payload: payload || {},
51583
- state: result.previousState
51584
- };
51585
- const sharedDeclared = runtime.collectDeclaredConfigDefaults(
51586
- binding.trait
51587
- );
51588
- const sharedCallSite = binding.config;
51589
- if (sharedDeclared || sharedCallSite) {
51590
- sharedBindings.config = {
51591
- ...sharedDeclared ?? {},
51592
- ...sharedCallSite ?? {}
51593
- };
51594
- }
51595
- const serverHandlers = runtime.createServerEffectHandlers({
51596
- persistence,
51597
- eventBus,
51598
- entityType: linkedEntity,
51599
- entityId,
51600
- bindings: sharedBindings,
51601
- context: {
51602
- traitName: binding.trait.name,
51603
- state: result.previousState,
51604
- transition: `${result.previousState}->${result.newState}`,
51605
- linkedEntity,
51606
- entityId
51607
- },
51608
- source: { trait: binding.trait.name },
51609
- callService: optionsRef.current?.callService
51610
- });
51611
- handlers = {
51612
- ...serverHandlers,
51613
- // Client handlers own UI + emit: keep the slot setter
51614
- // and pre-prefixed UI:* emit path intact.
51615
- emit: clientHandlers.emit,
51616
- renderUI: clientHandlers.renderUI,
51617
- navigate: clientHandlers.navigate,
51618
- notify: clientHandlers.notify
51619
- };
51620
- }
51621
- const baseSet = handlers.set;
51622
- handlers = {
51623
- ...handlers,
51624
- set: async (targetId, field, value) => {
51625
- let fieldState = traitFieldStatesRef.current.get(traitName);
51626
- if (!fieldState) {
51627
- fieldState = {};
51628
- traitFieldStatesRef.current.set(traitName, fieldState);
51629
- }
51630
- fieldState[field] = value;
51631
- if (baseSet) await baseSet(targetId, field, value);
51632
- }
51633
- };
51634
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
51635
- const bindingCtx = {
51636
- entity: entityForBinding,
51637
- payload: payload || {},
51638
- state: result.previousState
51639
- };
51640
- const declaredDefaults = runtime.collectDeclaredConfigDefaults(
51641
- binding.trait
51642
- );
51643
- const callSiteConfig = binding.config;
51644
- if (declaredDefaults || callSiteConfig) {
51645
- bindingCtx.config = {
51646
- ...declaredDefaults ?? {},
51647
- ...callSiteConfig ?? {}
51648
- };
51649
- }
51650
- const effectContext = {
51651
- traitName: binding.trait.name,
51652
- state: result.previousState,
51653
- transition: `${result.previousState}->${result.newState}`,
51654
- linkedEntity,
51655
- entityId
51656
- };
51657
- const emittedDuringExec = [];
51658
51991
  emittedByTrait.set(traitName, emittedDuringExec);
51659
- const baseEmit = handlers.emit;
51660
- const trackingHandlers = {
51661
- ...handlers,
51662
- emit: (event, eventPayload, source) => {
51663
- emittedDuringExec.push(event);
51664
- baseEmit(event, eventPayload, source);
51665
- }
51666
- };
51667
- const executor = new runtime.EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
51668
- try {
51669
- await executor.executeAll(result.effects);
51670
- stateLog.debug("transition:render-ui-dispatched", () => ({
51671
- traitName,
51672
- fromState: result.previousState,
51673
- toState: result.newState,
51674
- event: eventKey,
51675
- slotsTouched: Array.from(pendingSlots.keys()).join(","),
51676
- patternTypes: Array.from(pendingSlots.entries()).map(
51677
- ([slot, patterns]) => `${slot}:[${patterns.map((p2) => p2.pattern?.type ?? "null").join(",")}]`
51678
- ).join(";")
51679
- }));
51680
- void slotSource;
51681
- for (const [slot, patterns] of pendingSlots) {
51682
- stateLog.debug("flush:slot", {
51683
- traitName,
51684
- slot,
51685
- patternCount: patterns.length,
51686
- event: eventKey,
51687
- transition: `${result.previousState}->${result.newState}`,
51688
- cleared: patterns.length === 0
51689
- });
51690
- flushSlot(traitName, slot, patterns, {
51691
- event: eventKey,
51692
- state: result.previousState,
51693
- entity: binding.linkedEntity
51694
- });
51695
- }
51696
- } catch (error) {
51697
- stateLog.error("transition:effect-error", {
51698
- traitName,
51699
- fromState: result.previousState,
51700
- toState: result.newState,
51701
- event: eventKey,
51702
- error: error instanceof Error ? error.message : String(error),
51703
- effectCount: result.effects.length
51704
- });
51705
- }
51706
51992
  } else if (!result.executed) {
51707
51993
  if (result.guardResult === false) {
51708
51994
  stateLog.debug("guard-blocked-transition", {
@@ -51840,7 +52126,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51840
52126
  if (!orbitalName) continue;
51841
52127
  for (const transition of binding.trait.transitions) {
51842
52128
  const eventKey = transition.event;
51843
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
52129
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
51844
52130
  continue;
51845
52131
  }
51846
52132
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -51895,6 +52181,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
51895
52181
  crossTraitLog.debug("cleanup:done", {});
51896
52182
  };
51897
52183
  }, [traitBindings, eventBus, enqueueAndDrain]);
52184
+ React81.useEffect(() => {
52185
+ const mgr = managerRef.current;
52186
+ const inited = initedTraitsRef.current;
52187
+ for (const binding of traitBindings) {
52188
+ const traitName = binding.trait.name;
52189
+ if (inited.has(traitName)) continue;
52190
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
52191
+ (evt) => mgr.canHandleEvent(traitName, evt)
52192
+ );
52193
+ if (lifecycleEvent === void 0) continue;
52194
+ inited.add(traitName);
52195
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
52196
+ enqueueAndDrain(lifecycleEvent, {});
52197
+ }
52198
+ return () => {
52199
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
52200
+ };
52201
+ }, [traitBindings, enqueueAndDrain]);
51898
52202
  return {
51899
52203
  traitStates,
51900
52204
  sendEvent,