@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.
package/dist/avl/index.js CHANGED
@@ -39,15 +39,15 @@ import { FieldTypeSchema, isInlineTrait, isPageReference, isEntityCall, schemaTo
39
39
  import { useDroppable, useDraggable, DndContext, DragOverlay, useSensors, useSensor, PointerSensor, KeyboardSensor, pointerWithin, rectIntersection, closestCorners } from '@dnd-kit/core';
40
40
  import { sortableKeyboardCoordinates, useSortable, arrayMove, SortableContext, rectSortingStrategy, verticalListSortingStrategy } from '@dnd-kit/sortable';
41
41
  import { CSS } from '@dnd-kit/utilities';
42
- import * as THREE from 'three';
42
+ import * as THREE3 from 'three';
43
43
  import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
44
44
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
45
45
  import { GLTFLoader as GLTFLoader$1 } from 'three/examples/jsm/loaders/GLTFLoader';
46
- import { Canvas, useThree } from '@react-three/fiber';
47
- import { Grid as Grid$1, OrbitControls } from '@react-three/drei';
46
+ import { Canvas, useLoader, useFrame, useThree } from '@react-three/fiber';
47
+ import { Billboard, Grid as Grid$1, OrbitControls } from '@react-three/drei';
48
48
  import { getPatternDefinition, getComponentForPattern as getComponentForPattern$1, isEntityAwarePattern } from '@almadar/patterns';
49
49
  import { OrbitalServerRuntime } from '@almadar/runtime/OrbitalServerRuntime';
50
- import { InMemoryPersistence, StateMachineManager, createContextFromBindings, interpolateValue, collectDeclaredConfigDefaults, createServerEffectHandlers, EffectExecutor } from '@almadar/runtime';
50
+ import { InMemoryPersistence, StateMachineManager, collectDeclaredConfigDefaults, createServerEffectHandlers, EffectExecutor, interpolateValue, createContextFromBindings } from '@almadar/runtime';
51
51
 
52
52
  var __defProp = Object.defineProperty;
53
53
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
@@ -10792,6 +10792,52 @@ var init_ControlButton = __esm({
10792
10792
  ControlButton.displayName = "ControlButton";
10793
10793
  }
10794
10794
  });
10795
+
10796
+ // components/game/organisms/utils/spriteAnimation.ts
10797
+ function inferDirection(dx, dy) {
10798
+ if (dx === 0 && dy === 0) return "se";
10799
+ if (dx >= 0 && dy >= 0) return "se";
10800
+ if (dx <= 0 && dy >= 0) return "sw";
10801
+ if (dx >= 0 && dy <= 0) return "ne";
10802
+ return "nw";
10803
+ }
10804
+ function resolveSheetDirection(facing) {
10805
+ switch (facing) {
10806
+ case "se":
10807
+ return { sheetDir: "se", flipX: false };
10808
+ case "sw":
10809
+ return { sheetDir: "sw", flipX: false };
10810
+ case "ne":
10811
+ return { sheetDir: "sw", flipX: true };
10812
+ case "nw":
10813
+ return { sheetDir: "se", flipX: true };
10814
+ }
10815
+ }
10816
+ function frameRect(frame, row, columns, frameWidth, frameHeight) {
10817
+ return {
10818
+ sx: frame % columns * frameWidth,
10819
+ sy: row * frameHeight,
10820
+ sw: frameWidth,
10821
+ sh: frameHeight
10822
+ };
10823
+ }
10824
+ function getCurrentFrameFromDef(def, elapsed) {
10825
+ const frameDuration = 1e3 / def.frameRate;
10826
+ const totalDuration = def.frames * frameDuration;
10827
+ if (def.loop) {
10828
+ const frame2 = Math.floor(elapsed % totalDuration / frameDuration);
10829
+ return { frame: frame2, finished: false };
10830
+ }
10831
+ if (elapsed >= totalDuration) {
10832
+ return { frame: def.frames - 1, finished: true };
10833
+ }
10834
+ const frame = Math.floor(elapsed / frameDuration);
10835
+ return { frame, finished: false };
10836
+ }
10837
+ var init_spriteAnimation = __esm({
10838
+ "components/game/organisms/utils/spriteAnimation.ts"() {
10839
+ }
10840
+ });
10795
10841
  function Sprite({
10796
10842
  spritesheet = "https://almadar-kflow-assets.web.app/shared/isometric-blocks/Spritesheet/allTiles_sheet.png",
10797
10843
  frameWidth = 64,
@@ -10812,12 +10858,8 @@ function Sprite({
10812
10858
  }) {
10813
10859
  const eventBus = useEventBus();
10814
10860
  const sourcePosition = useMemo(() => {
10815
- const frameX = frame % columns;
10816
- const frameY = Math.floor(frame / columns);
10817
- return {
10818
- x: frameX * frameWidth,
10819
- y: frameY * frameHeight
10820
- };
10861
+ const { sx, sy } = frameRect(frame, Math.floor(frame / columns), columns, frameWidth, frameHeight);
10862
+ return { x: sx, y: sy };
10821
10863
  }, [frame, columns, frameWidth, frameHeight]);
10822
10864
  const transform = useMemo(() => {
10823
10865
  const transforms = [
@@ -10865,6 +10907,7 @@ var init_Sprite = __esm({
10865
10907
  "components/game/atoms/Sprite.tsx"() {
10866
10908
  "use client";
10867
10909
  init_useEventBus();
10910
+ init_spriteAnimation();
10868
10911
  }
10869
10912
  });
10870
10913
  function StateIndicator({
@@ -14256,6 +14299,163 @@ var init_useCamera = __esm({
14256
14299
  "use client";
14257
14300
  }
14258
14301
  });
14302
+ function unitAtlasUrl(unit) {
14303
+ if (unit.spriteSheet) return unit.spriteSheet;
14304
+ const sprite = unit.sprite;
14305
+ if (!sprite) return null;
14306
+ const match = /^(.*-sprite-sheet)(?:-(?:se|sw))?(?:-v\d+)?\.png$/.exec(sprite);
14307
+ if (!match) return null;
14308
+ return `${match[1]}.json`;
14309
+ }
14310
+ function resolveSheetUrl(atlasUrl, relativeSheetPath) {
14311
+ try {
14312
+ return new URL(relativeSheetPath, atlasUrl).toString();
14313
+ } catch {
14314
+ const base = atlasUrl.slice(0, atlasUrl.lastIndexOf("/") + 1);
14315
+ return `${base}${relativeSheetPath.replace(/^\.\//, "")}`;
14316
+ }
14317
+ }
14318
+ function useUnitSpriteAtlas(units) {
14319
+ const atlasCacheRef = useRef(/* @__PURE__ */ new Map());
14320
+ const loadingRef = useRef(/* @__PURE__ */ new Set());
14321
+ const [pendingCount, setPendingCount] = useState(0);
14322
+ const [, forceTick] = useState(0);
14323
+ const animStatesRef = useRef(/* @__PURE__ */ new Map());
14324
+ const lastTickRef = useRef(0);
14325
+ const rafRef = useRef(0);
14326
+ const atlasUrls = useMemo(() => {
14327
+ const set = /* @__PURE__ */ new Set();
14328
+ for (const unit of units) {
14329
+ const url = unitAtlasUrl(unit);
14330
+ if (url) set.add(url);
14331
+ }
14332
+ return [...set];
14333
+ }, [units]);
14334
+ useEffect(() => {
14335
+ const cache = atlasCacheRef.current;
14336
+ const loading = loadingRef.current;
14337
+ const toLoad = atlasUrls.filter((url) => !cache.has(url) && !loading.has(url));
14338
+ if (toLoad.length === 0) return;
14339
+ let cancelled = false;
14340
+ setPendingCount((prev) => prev + toLoad.length);
14341
+ for (const url of toLoad) {
14342
+ loading.add(url);
14343
+ fetch(url).then((res) => res.ok ? res.json() : Promise.reject(new Error(String(res.status)))).then((atlas) => {
14344
+ if (cancelled) return;
14345
+ cache.set(url, atlas);
14346
+ }).catch(() => {
14347
+ }).finally(() => {
14348
+ if (cancelled) return;
14349
+ loading.delete(url);
14350
+ setPendingCount((prev) => Math.max(0, prev - 1));
14351
+ forceTick((n) => n + 1);
14352
+ });
14353
+ }
14354
+ return () => {
14355
+ cancelled = true;
14356
+ };
14357
+ }, [atlasUrls]);
14358
+ const sheetUrls = useMemo(() => {
14359
+ const urls = /* @__PURE__ */ new Set();
14360
+ for (const unit of units) {
14361
+ const atlasUrl = unitAtlasUrl(unit);
14362
+ if (!atlasUrl) continue;
14363
+ const atlas = atlasCacheRef.current.get(atlasUrl);
14364
+ if (!atlas) continue;
14365
+ for (const rel of Object.values(atlas.sheets)) {
14366
+ if (rel) urls.add(resolveSheetUrl(atlasUrl, rel));
14367
+ }
14368
+ }
14369
+ return [...urls];
14370
+ }, [units, pendingCount]);
14371
+ useEffect(() => {
14372
+ const hasAtlasUnits = units.some((u) => unitAtlasUrl(u) !== null);
14373
+ if (!hasAtlasUnits) return;
14374
+ let running = true;
14375
+ const tick = (ts) => {
14376
+ if (!running) return;
14377
+ const last = lastTickRef.current || ts;
14378
+ const delta = ts - last;
14379
+ lastTickRef.current = ts;
14380
+ const states = animStatesRef.current;
14381
+ const currentIds = /* @__PURE__ */ new Set();
14382
+ for (const unit of units) {
14383
+ if (unitAtlasUrl(unit) === null) continue;
14384
+ currentIds.add(unit.id);
14385
+ let state = states.get(unit.id);
14386
+ if (!state) {
14387
+ state = { animation: "idle", direction: "se", elapsed: 0, walkHold: 0, prev: null };
14388
+ states.set(unit.id, state);
14389
+ }
14390
+ const posX = unit.position?.x ?? unit.x ?? 0;
14391
+ const posY = unit.position?.y ?? unit.y ?? 0;
14392
+ if (state.prev) {
14393
+ const dx = posX - state.prev.x;
14394
+ const dy = posY - state.prev.y;
14395
+ if (dx !== 0 || dy !== 0) {
14396
+ state.animation = "walk";
14397
+ state.direction = inferDirection(dx, dy);
14398
+ state.walkHold = WALK_HOLD_MS;
14399
+ } else if (state.animation === "walk") {
14400
+ state.walkHold -= delta;
14401
+ if (state.walkHold <= 0) state.animation = "idle";
14402
+ }
14403
+ }
14404
+ state.prev = { x: posX, y: posY };
14405
+ state.elapsed += delta;
14406
+ }
14407
+ for (const id of states.keys()) {
14408
+ if (!currentIds.has(id)) states.delete(id);
14409
+ }
14410
+ rafRef.current = requestAnimationFrame(tick);
14411
+ };
14412
+ rafRef.current = requestAnimationFrame(tick);
14413
+ return () => {
14414
+ running = false;
14415
+ cancelAnimationFrame(rafRef.current);
14416
+ lastTickRef.current = 0;
14417
+ };
14418
+ }, [units]);
14419
+ const resolveUnitFrame = useCallback((unitId) => {
14420
+ const unit = units.find((u) => u.id === unitId);
14421
+ if (!unit) return null;
14422
+ const atlasUrl = unitAtlasUrl(unit);
14423
+ if (!atlasUrl) return null;
14424
+ const atlas = atlasCacheRef.current.get(atlasUrl);
14425
+ if (!atlas) return null;
14426
+ const state = animStatesRef.current.get(unitId);
14427
+ const animation = state?.animation ?? "idle";
14428
+ const direction = state?.direction ?? "se";
14429
+ const elapsed = state?.elapsed ?? 0;
14430
+ const def = atlas.animations[animation] ?? atlas.animations.idle;
14431
+ if (!def) return null;
14432
+ const { sheetDir, flipX } = resolveSheetDirection(direction);
14433
+ const rel = atlas.sheets[sheetDir] ?? atlas.sheets.se ?? atlas.sheets.sw;
14434
+ if (!rel) return null;
14435
+ const sheetUrl = resolveSheetUrl(atlasUrl, rel);
14436
+ const isIdle = animation === "idle";
14437
+ const frame = isIdle ? 0 : getCurrentFrameFromDef(def, elapsed).frame;
14438
+ const rect = frameRect(frame, def.row, atlas.columns, atlas.frameWidth, atlas.frameHeight);
14439
+ return {
14440
+ sheetUrl,
14441
+ sx: rect.sx,
14442
+ sy: rect.sy,
14443
+ sw: rect.sw,
14444
+ sh: rect.sh,
14445
+ flipX,
14446
+ applyBreathing: isIdle
14447
+ };
14448
+ }, [units]);
14449
+ return { sheetUrls, resolveUnitFrame, pendingCount };
14450
+ }
14451
+ var WALK_HOLD_MS;
14452
+ var init_useUnitSpriteAtlas = __esm({
14453
+ "components/game/molecules/useUnitSpriteAtlas.ts"() {
14454
+ "use client";
14455
+ init_spriteAnimation();
14456
+ WALK_HOLD_MS = 600;
14457
+ }
14458
+ });
14259
14459
 
14260
14460
  // components/game/organisms/utils/isometric.ts
14261
14461
  function isoToScreen(tileX, tileY, scale, baseOffsetX) {
@@ -14370,6 +14570,10 @@ function IsometricCanvas({
14370
14570
  () => unitsProp.map((u) => u.position ? u : { ...u, position: { x: u.x ?? 0, y: u.y ?? 0 } }),
14371
14571
  [unitsProp]
14372
14572
  );
14573
+ const { sheetUrls: atlasSheetUrls, resolveUnitFrame: resolveUnitFrameInternal, pendingCount: atlasPending } = useUnitSpriteAtlas(units);
14574
+ const resolveFrameForUnit = useCallback((unitId) => {
14575
+ return resolveUnitFrame?.(unitId) ?? resolveUnitFrameInternal(unitId);
14576
+ }, [resolveUnitFrame, resolveUnitFrameInternal]);
14373
14577
  const features = useMemo(
14374
14578
  () => featuresProp.map((f3) => {
14375
14579
  if (f3.type) return f3;
@@ -14453,9 +14657,10 @@ function IsometricCanvas({
14453
14657
  }
14454
14658
  }
14455
14659
  if (effectSpriteUrls) urls.push(...effectSpriteUrls);
14660
+ if (atlasSheetUrls.length) urls.push(...atlasSheetUrls);
14456
14661
  if (backgroundImage) urls.push(backgroundImage);
14457
14662
  return [...new Set(urls.filter(Boolean))];
14458
- }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, backgroundImage, assetManifest, resolveManifestUrl]);
14663
+ }, [sortedTiles, features, units, getTerrainSprite, getFeatureSprite, getUnitSprite, effectSpriteUrls, atlasSheetUrls, backgroundImage, assetManifest, resolveManifestUrl]);
14459
14664
  const { getImage, pendingCount } = useImageCache(spriteUrls);
14460
14665
  useEffect(() => {
14461
14666
  if (typeof window === "undefined") return;
@@ -14742,7 +14947,7 @@ function IsometricCanvas({
14742
14947
  ctx.lineWidth = 3;
14743
14948
  ctx.stroke();
14744
14949
  }
14745
- const frame = resolveUnitFrame?.(unit.id) ?? null;
14950
+ const frame = resolveFrameForUnit(unit.id);
14746
14951
  const frameImg = frame ? getImage(frame.sheetUrl) : null;
14747
14952
  if (frame && frameImg) {
14748
14953
  const frameAr = frame.sw / frame.sh;
@@ -14852,7 +15057,7 @@ function IsometricCanvas({
14852
15057
  resolveTerrainSpriteUrl,
14853
15058
  resolveFeatureSpriteUrl,
14854
15059
  resolveUnitSpriteUrl,
14855
- resolveUnitFrame,
15060
+ resolveFrameForUnit,
14856
15061
  getImage,
14857
15062
  gridWidth,
14858
15063
  gridHeight,
@@ -14884,7 +15089,7 @@ function IsometricCanvas({
14884
15089
  };
14885
15090
  }, [selectedUnitId, units, scale, baseOffsetX, scaledTileWidth, scaledDiamondTopY, scaledFloorHeight, viewportSize, targetCameraRef]);
14886
15091
  useEffect(() => {
14887
- const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0;
15092
+ const hasAnimations = units.length > 0 || validMoves.length > 0 || attackTargets.length > 0 || selectedUnitId != null || targetCameraRef.current != null || hasActiveEffects2 || pendingCount > 0 || atlasPending > 0;
14888
15093
  draw(animTimeRef.current);
14889
15094
  if (!hasAnimations) return;
14890
15095
  let running = true;
@@ -14900,7 +15105,7 @@ function IsometricCanvas({
14900
15105
  running = false;
14901
15106
  cancelAnimationFrame(rafIdRef.current);
14902
15107
  };
14903
- }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, pendingCount, lerpToTarget, targetCameraRef]);
15108
+ }, [draw, units.length, validMoves.length, attackTargets.length, selectedUnitId, hasActiveEffects2, pendingCount, atlasPending, lerpToTarget, targetCameraRef]);
14904
15109
  const handleMouseMoveWithCamera = useCallback((e) => {
14905
15110
  if (enableCamera) {
14906
15111
  const wasPanning = handleMouseMove(e, () => draw(animTimeRef.current));
@@ -15045,6 +15250,7 @@ var init_IsometricCanvas = __esm({
15045
15250
  init_ErrorState();
15046
15251
  init_useImageCache();
15047
15252
  init_useCamera();
15253
+ init_useUnitSpriteAtlas();
15048
15254
  init_verificationRegistry();
15049
15255
  init_isometric();
15050
15256
  IsometricCanvas.displayName = "IsometricCanvas";
@@ -31798,13 +32004,13 @@ var init_MapView = __esm({
31798
32004
  shadowSize: [41, 41]
31799
32005
  });
31800
32006
  L.Marker.prototype.options.icon = defaultIcon;
31801
- const { useEffect: useEffect84, useRef: useRef76, useCallback: useCallback124, useState: useState120 } = React90__default;
32007
+ const { useEffect: useEffect85, useRef: useRef77, useCallback: useCallback125, useState: useState121 } = React90__default;
31802
32008
  const { Typography: Typography2 } = await Promise.resolve().then(() => (init_Typography(), Typography_exports));
31803
32009
  const { useEventBus: useEventBus4 } = await Promise.resolve().then(() => (init_useEventBus(), useEventBus_exports));
31804
32010
  function MapUpdater({ centerLat, centerLng, zoom }) {
31805
32011
  const map = useMap();
31806
- const prevRef = useRef76({ centerLat, centerLng, zoom });
31807
- useEffect84(() => {
32012
+ const prevRef = useRef77({ centerLat, centerLng, zoom });
32013
+ useEffect85(() => {
31808
32014
  const prev = prevRef.current;
31809
32015
  if (prev.centerLat !== centerLat || prev.centerLng !== centerLng || prev.zoom !== zoom) {
31810
32016
  map.setView([centerLat, centerLng], zoom);
@@ -31815,7 +32021,7 @@ var init_MapView = __esm({
31815
32021
  }
31816
32022
  function MapClickHandler({ onMapClick }) {
31817
32023
  const map = useMap();
31818
- useEffect84(() => {
32024
+ useEffect85(() => {
31819
32025
  if (!onMapClick) return;
31820
32026
  const handler = (e) => {
31821
32027
  onMapClick(e.latlng.lat, e.latlng.lng);
@@ -31843,8 +32049,8 @@ var init_MapView = __esm({
31843
32049
  showAttribution = true
31844
32050
  }) {
31845
32051
  const eventBus = useEventBus4();
31846
- const [clickedPosition, setClickedPosition] = useState120(null);
31847
- const handleMapClick = useCallback124((lat, lng) => {
32052
+ const [clickedPosition, setClickedPosition] = useState121(null);
32053
+ const handleMapClick = useCallback125((lat, lng) => {
31848
32054
  if (showClickedPin) {
31849
32055
  setClickedPosition({ lat, lng });
31850
32056
  }
@@ -31853,7 +32059,7 @@ var init_MapView = __esm({
31853
32059
  eventBus.emit(`UI:${mapClickEvent}`, { latitude: lat, longitude: lng });
31854
32060
  }
31855
32061
  }, [onMapClick, mapClickEvent, eventBus, showClickedPin]);
31856
- const handleMarkerClick = useCallback124((marker) => {
32062
+ const handleMarkerClick = useCallback125((marker) => {
31857
32063
  onMarkerClick?.(marker);
31858
32064
  if (markerClickEvent) {
31859
32065
  eventBus.emit(`UI:${markerClickEvent}`, { ...marker });
@@ -43162,7 +43368,7 @@ var init_AssetLoader = __esm({
43162
43368
  __publicField(this, "textureCache");
43163
43369
  __publicField(this, "loadingPromises");
43164
43370
  this.objLoader = new OBJLoader();
43165
- this.textureLoader = new THREE.TextureLoader();
43371
+ this.textureLoader = new THREE3.TextureLoader();
43166
43372
  this.modelCache = /* @__PURE__ */ new Map();
43167
43373
  this.textureCache = /* @__PURE__ */ new Map();
43168
43374
  this.loadingPromises = /* @__PURE__ */ new Map();
@@ -43236,7 +43442,7 @@ var init_AssetLoader = __esm({
43236
43442
  return this.loadingPromises.get(`texture:${url}`);
43237
43443
  }
43238
43444
  const loadPromise = this.textureLoader.loadAsync(url).then((texture) => {
43239
- texture.colorSpace = THREE.SRGBColorSpace;
43445
+ texture.colorSpace = THREE3.SRGBColorSpace;
43240
43446
  this.textureCache.set(url, texture);
43241
43447
  this.loadingPromises.delete(`texture:${url}`);
43242
43448
  return texture;
@@ -43310,7 +43516,7 @@ var init_AssetLoader = __esm({
43310
43516
  });
43311
43517
  this.modelCache.forEach((model) => {
43312
43518
  model.scene.traverse((child) => {
43313
- if (child instanceof THREE.Mesh) {
43519
+ if (child instanceof THREE3.Mesh) {
43314
43520
  child.geometry.dispose();
43315
43521
  if (Array.isArray(child.material)) {
43316
43522
  child.material.forEach((m) => m.dispose());
@@ -43866,7 +44072,7 @@ function ModelLoader({
43866
44072
  if (!loadedModel) return null;
43867
44073
  const cloned = loadedModel.clone();
43868
44074
  cloned.traverse((child) => {
43869
- if (child instanceof THREE.Mesh) {
44075
+ if (child instanceof THREE3.Mesh) {
43870
44076
  child.castShadow = castShadow;
43871
44077
  child.receiveShadow = receiveShadow;
43872
44078
  }
@@ -43980,6 +44186,47 @@ function CameraController({
43980
44186
  }, [camera.position, onCameraChange]);
43981
44187
  return null;
43982
44188
  }
44189
+ function UnitSpriteBillboard({
44190
+ sheetUrl,
44191
+ resolveFrame,
44192
+ height = 1.2
44193
+ }) {
44194
+ const texture = useLoader(THREE3.TextureLoader, sheetUrl);
44195
+ const meshRef = useRef(null);
44196
+ const matRef = useRef(null);
44197
+ const [aspect, setAspect] = useState(1);
44198
+ useFrame(() => {
44199
+ const frame = resolveFrame();
44200
+ if (!frame || !texture.image) return;
44201
+ const imgW = texture.image.width;
44202
+ const imgH = texture.image.height;
44203
+ if (!imgW || !imgH) return;
44204
+ texture.repeat.set((frame.flipX ? -1 : 1) * (frame.sw / imgW), frame.sh / imgH);
44205
+ texture.offset.set(
44206
+ frame.flipX ? (frame.sx + frame.sw) / imgW : frame.sx / imgW,
44207
+ 1 - (frame.sy + frame.sh) / imgH
44208
+ );
44209
+ texture.magFilter = THREE3.NearestFilter;
44210
+ texture.minFilter = THREE3.NearestFilter;
44211
+ texture.needsUpdate = true;
44212
+ const nextAspect = frame.sw / frame.sh;
44213
+ if (Math.abs(nextAspect - aspect) > 1e-3) setAspect(nextAspect);
44214
+ if (matRef.current) matRef.current.needsUpdate = true;
44215
+ });
44216
+ return /* @__PURE__ */ jsxs("mesh", { ref: meshRef, position: [0, height / 2, 0], children: [
44217
+ /* @__PURE__ */ jsx("planeGeometry", { args: [height * aspect, height] }),
44218
+ /* @__PURE__ */ jsx(
44219
+ "meshBasicMaterial",
44220
+ {
44221
+ ref: matRef,
44222
+ map: texture,
44223
+ transparent: true,
44224
+ alphaTest: 0.1,
44225
+ side: THREE3.DoubleSide
44226
+ }
44227
+ )
44228
+ ] });
44229
+ }
43983
44230
  var DEFAULT_GRID_CONFIG, GameCanvas3D;
43984
44231
  var init_GameCanvas3D2 = __esm({
43985
44232
  "components/game/molecules/GameCanvas3D.tsx"() {
@@ -43990,6 +44237,7 @@ var init_GameCanvas3D2 = __esm({
43990
44237
  init_Canvas3DLoadingState2();
43991
44238
  init_Canvas3DErrorBoundary2();
43992
44239
  init_ModelLoader();
44240
+ init_useUnitSpriteAtlas();
43993
44241
  init_cn();
43994
44242
  init_GameCanvas3D();
43995
44243
  DEFAULT_GRID_CONFIG = {
@@ -44046,8 +44294,10 @@ var init_GameCanvas3D2 = __esm({
44046
44294
  const controlsRef = useRef(null);
44047
44295
  const [hoveredTile, setHoveredTile] = useState(null);
44048
44296
  const [internalError, setInternalError] = useState(null);
44297
+ const { sheetUrls: atlasSheetUrls, resolveUnitFrame } = useUnitSpriteAtlas(units);
44298
+ const preloadUrls = useMemo(() => [...preloadAssets, ...atlasSheetUrls], [preloadAssets, atlasSheetUrls]);
44049
44299
  const { isLoading: assetsLoading, progress, loaded, total } = useAssetLoader({
44050
- preloadUrls: preloadAssets,
44300
+ preloadUrls,
44051
44301
  loader: customAssetLoader
44052
44302
  });
44053
44303
  const eventHandlers = useGameCanvas3DEvents({
@@ -44106,7 +44356,7 @@ var init_GameCanvas3D2 = __esm({
44106
44356
  getCameraPosition: () => {
44107
44357
  if (controlsRef.current) {
44108
44358
  const pos = controlsRef.current.object.position;
44109
- return new THREE.Vector3(pos.x, pos.y, pos.z);
44359
+ return new THREE3.Vector3(pos.x, pos.y, pos.z);
44110
44360
  }
44111
44361
  return null;
44112
44362
  },
@@ -44258,6 +44508,8 @@ var init_GameCanvas3D2 = __esm({
44258
44508
  ({ unit, position }) => {
44259
44509
  const isSelected = selectedUnitId === unit.id;
44260
44510
  const color = unit.faction === "player" ? 4491519 : unit.faction === "enemy" ? 16729156 : 16777028;
44511
+ const hasAtlas = unitAtlasUrl(unit) !== null;
44512
+ const initialFrame = hasAtlas ? resolveUnitFrame(unit.id) : null;
44261
44513
  return /* @__PURE__ */ jsxs(
44262
44514
  "group",
44263
44515
  {
@@ -44269,7 +44521,16 @@ var init_GameCanvas3D2 = __esm({
44269
44521
  /* @__PURE__ */ jsx("ringGeometry", { args: [0.4, 0.5, 32] }),
44270
44522
  /* @__PURE__ */ jsx("meshBasicMaterial", { color: "#ffff00", transparent: true, opacity: 0.8 })
44271
44523
  ] }),
44272
- unit.modelUrl ? (
44524
+ hasAtlas && initialFrame ? (
44525
+ /* Animated sprite-sheet billboard — single cropped frame, by state */
44526
+ /* @__PURE__ */ jsx(Billboard, { children: /* @__PURE__ */ jsx(
44527
+ UnitSpriteBillboard,
44528
+ {
44529
+ sheetUrl: initialFrame.sheetUrl,
44530
+ resolveFrame: () => resolveUnitFrame(unit.id)
44531
+ }
44532
+ ) })
44533
+ ) : unit.modelUrl ? (
44273
44534
  /* GLB unit model (box fallback while loading / on error) */
44274
44535
  /* @__PURE__ */ jsx(
44275
44536
  ModelLoader,
@@ -44323,7 +44584,7 @@ var init_GameCanvas3D2 = __esm({
44323
44584
  }
44324
44585
  );
44325
44586
  },
44326
- [selectedUnitId, handleUnitClick]
44587
+ [selectedUnitId, handleUnitClick, resolveUnitFrame]
44327
44588
  );
44328
44589
  const DefaultFeatureRenderer = useCallback(
44329
44590
  ({
@@ -57097,6 +57358,28 @@ init_verificationRegistry();
57097
57358
  var crossTraitLog = createLogger("almadar:ui:cross-trait");
57098
57359
  var flushLog = createLogger("almadar:ui:slot-flush");
57099
57360
  var stateLog = createLogger("almadar:ui:state-transitions");
57361
+ var tickLog = createLogger("almadar:ui:tick-effects");
57362
+ var SYNC_TICK_OPERATORS = /* @__PURE__ */ new Set([
57363
+ "set",
57364
+ "emit",
57365
+ "render-ui",
57366
+ "render",
57367
+ "navigate",
57368
+ "notify",
57369
+ "log",
57370
+ // Synchronous structural forms that wrap sync effects. A tick authored as
57371
+ // a single top-level `(let ((..)) (do (set ..) (render-ui ..)))` or
57372
+ // `(if cond (set ..) ..)` is one of these at its head — the EffectExecutor
57373
+ // resolves the binding values / condition through the canonical evaluator
57374
+ // and runs the wrapped sync effects. Without these in the allow-list the
57375
+ // whole tick is filtered out (its head op isn't `set`/`emit`/...), so the
57376
+ // physics/gameflow/AI tick never runs.
57377
+ "let",
57378
+ "if",
57379
+ "do",
57380
+ "when"
57381
+ ]);
57382
+ var LIFECYCLE_EVENTS = ["INIT", "LOAD", "$MOUNT"];
57100
57383
  function toTraitDefinition(binding) {
57101
57384
  return {
57102
57385
  name: binding.trait.name,
@@ -57129,6 +57412,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57129
57412
  });
57130
57413
  const eventQueueRef = useRef([]);
57131
57414
  const processingRef = useRef(false);
57415
+ const initedTraitsRef = useRef(/* @__PURE__ */ new Set());
57132
57416
  const traitBindingsRef = useRef(traitBindings);
57133
57417
  const managerRef = useRef(manager);
57134
57418
  const uiSlotsRef = useRef(uiSlots);
@@ -57293,50 +57577,196 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57293
57577
  for (const unreg of snapshotUnregs) unreg();
57294
57578
  };
57295
57579
  }, [traitBindings]);
57296
- const runTickEffects = useCallback((tick, binding) => {
57297
- const currentState = traitStatesRef.current.get(binding.trait.name)?.currentState ?? "";
57298
- if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
57299
- const bindingCtx = { entity: {}, payload: {}, state: currentState };
57300
- if (binding.config) {
57301
- bindingCtx.config = binding.config;
57302
- }
57303
- const evalCtx = createContextFromBindings(bindingCtx);
57304
- if (tick.guard !== void 0) {
57305
- const passed = interpolateValue(tick.guard, evalCtx);
57306
- if (!passed) return;
57307
- }
57580
+ const executeTransitionEffects = useCallback(async (params) => {
57581
+ const { binding, previousState, newState, payload, flushEvent, syncOnly, log: log15 } = params;
57582
+ const traitName = binding.trait.name;
57583
+ const linkedEntity = binding.linkedEntity || "";
57584
+ const entityId = payload?.entityId;
57585
+ const effects = syncOnly ? params.effects.filter(
57586
+ (e) => Array.isArray(e) && SYNC_TICK_OPERATORS.has(String(e[0]))
57587
+ ) : params.effects;
57588
+ if (effects.length === 0) return [];
57308
57589
  const pendingSlots = /* @__PURE__ */ new Map();
57309
57590
  ({
57310
- trait: binding.trait.name,
57311
- transition: `${currentState}->tick:${tick.name}`,
57312
- effects: tick.effects,
57313
57591
  traitDefinition: binding.trait
57314
57592
  });
57315
- for (const effect of tick.effects) {
57316
- if (!Array.isArray(effect)) continue;
57317
- const op = effect[0];
57318
- if (op === "render-ui" || op === "render") {
57319
- const slot = effect[1];
57320
- const rawPattern = effect[2];
57321
- if (rawPattern === null || rawPattern === void 0) {
57593
+ const clientHandlers = createClientEffectHandlers({
57594
+ eventBus,
57595
+ slotSetter: {
57596
+ addPattern: (slot, pattern, props) => {
57597
+ const existing = pendingSlots.get(slot) || [];
57598
+ existing.push({ pattern, props: props || {} });
57599
+ pendingSlots.set(slot, existing);
57600
+ },
57601
+ clearSlot: (slot) => {
57322
57602
  pendingSlots.set(slot, []);
57323
- continue;
57324
57603
  }
57325
- const updatedCtx = createContextFromBindings(bindingCtx);
57326
- const resolved = interpolateValue(rawPattern, updatedCtx);
57327
- const existing = pendingSlots.get(slot) ?? [];
57328
- existing.push({ pattern: resolved, props: {} });
57329
- pendingSlots.set(slot, existing);
57604
+ },
57605
+ navigate: optionsRef.current?.navigate,
57606
+ notify: optionsRef.current?.notify,
57607
+ callService: optionsRef.current?.callService
57608
+ });
57609
+ const persistence = syncOnly ? void 0 : optionsRef.current?.persistence;
57610
+ let handlers = clientHandlers;
57611
+ if (persistence) {
57612
+ const sharedBindings = {
57613
+ // Seed `@entity` from the trait's scalar field state (a real
57614
+ // EntityRow), the same source the executor's own bindingCtx
57615
+ // uses below. `@payload.*` resolves from `payload` separately,
57616
+ // so dropping the prior `payload as EntityRow` cast loses
57617
+ // nothing — it just stops mislabelling the payload as an entity.
57618
+ entity: traitFieldStatesRef.current.get(traitName) ?? {},
57619
+ payload: payload || {},
57620
+ state: previousState
57621
+ };
57622
+ const sharedDeclared = collectDeclaredConfigDefaults(binding.trait);
57623
+ const sharedCallSite = binding.config;
57624
+ if (sharedDeclared || sharedCallSite) {
57625
+ sharedBindings.config = {
57626
+ ...sharedDeclared ?? {},
57627
+ ...sharedCallSite ?? {}
57628
+ };
57330
57629
  }
57630
+ const serverHandlers = createServerEffectHandlers({
57631
+ persistence,
57632
+ eventBus,
57633
+ entityType: linkedEntity,
57634
+ entityId,
57635
+ bindings: sharedBindings,
57636
+ context: {
57637
+ traitName,
57638
+ state: previousState,
57639
+ transition: `${previousState}->${newState}`,
57640
+ linkedEntity,
57641
+ entityId
57642
+ },
57643
+ source: { trait: traitName },
57644
+ callService: optionsRef.current?.callService
57645
+ });
57646
+ handlers = {
57647
+ ...serverHandlers,
57648
+ emit: clientHandlers.emit,
57649
+ renderUI: clientHandlers.renderUI,
57650
+ navigate: clientHandlers.navigate,
57651
+ notify: clientHandlers.notify
57652
+ };
57331
57653
  }
57332
- for (const [slot, patterns] of pendingSlots) {
57333
- flushSlot(binding.trait.name, slot, patterns, {
57334
- event: `tick:${tick.name}`,
57335
- state: currentState,
57336
- entity: binding.linkedEntity
57654
+ const baseSet = handlers.set;
57655
+ handlers = {
57656
+ ...handlers,
57657
+ set: async (targetId, field, value) => {
57658
+ let fieldState = traitFieldStatesRef.current.get(traitName);
57659
+ if (!fieldState) {
57660
+ fieldState = {};
57661
+ traitFieldStatesRef.current.set(traitName, fieldState);
57662
+ }
57663
+ fieldState[field] = value;
57664
+ log15.debug("set:write", {
57665
+ traitName,
57666
+ field,
57667
+ value: JSON.stringify(value),
57668
+ transition: `${previousState}->${newState}`
57669
+ });
57670
+ if (baseSet) await baseSet(targetId, field, value);
57671
+ }
57672
+ };
57673
+ const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
57674
+ const bindingCtx = {
57675
+ entity: entityForBinding,
57676
+ payload: payload || {},
57677
+ state: previousState
57678
+ };
57679
+ const declaredDefaults = collectDeclaredConfigDefaults(binding.trait);
57680
+ const callSiteConfig = binding.config;
57681
+ if (declaredDefaults || callSiteConfig) {
57682
+ bindingCtx.config = {
57683
+ ...declaredDefaults ?? {},
57684
+ ...callSiteConfig ?? {}
57685
+ };
57686
+ }
57687
+ const effectContext = {
57688
+ traitName,
57689
+ state: previousState,
57690
+ transition: `${previousState}->${newState}`,
57691
+ linkedEntity,
57692
+ entityId
57693
+ };
57694
+ const emittedDuringExec = [];
57695
+ const baseEmit = handlers.emit;
57696
+ const trackingHandlers = {
57697
+ ...handlers,
57698
+ emit: (event, eventPayload, source) => {
57699
+ emittedDuringExec.push(event);
57700
+ baseEmit(event, eventPayload, source);
57701
+ }
57702
+ };
57703
+ const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
57704
+ try {
57705
+ await executor.executeAll(effects);
57706
+ log15.debug("effects:executed", () => ({
57707
+ traitName,
57708
+ transition: `${previousState}->${newState}`,
57709
+ event: flushEvent,
57710
+ effectCount: effects.length,
57711
+ emitted: emittedDuringExec.join(","),
57712
+ entityAfter: JSON.stringify(traitFieldStatesRef.current.get(traitName) ?? {}),
57713
+ slotsTouched: Array.from(pendingSlots.keys()).join(",")
57714
+ }));
57715
+ for (const [slot, patterns] of pendingSlots) {
57716
+ log15.debug("flush:slot", {
57717
+ traitName,
57718
+ slot,
57719
+ patternCount: patterns.length,
57720
+ event: flushEvent,
57721
+ transition: `${previousState}->${newState}`,
57722
+ cleared: patterns.length === 0
57723
+ });
57724
+ flushSlot(traitName, slot, patterns, {
57725
+ event: flushEvent,
57726
+ state: previousState,
57727
+ entity: binding.linkedEntity
57728
+ });
57729
+ }
57730
+ } catch (error) {
57731
+ log15.error("effects:error", {
57732
+ traitName,
57733
+ transition: `${previousState}->${newState}`,
57734
+ event: flushEvent,
57735
+ error: error instanceof Error ? error.message : String(error),
57736
+ effectCount: effects.length
57337
57737
  });
57338
57738
  }
57339
- }, [flushSlot]);
57739
+ return emittedDuringExec;
57740
+ }, [eventBus, flushSlot]);
57741
+ const runTickEffects = useCallback((tick, binding) => {
57742
+ const traitName = binding.trait.name;
57743
+ const currentState = traitStatesRef.current.get(traitName)?.currentState ?? "";
57744
+ if (tick.appliesTo.length > 0 && !tick.appliesTo.includes(currentState)) return;
57745
+ if (tick.guard !== void 0) {
57746
+ const guardCtx = {
57747
+ entity: traitFieldStatesRef.current.get(traitName) ?? {},
57748
+ payload: {},
57749
+ state: currentState
57750
+ };
57751
+ if (binding.config) {
57752
+ guardCtx.config = binding.config;
57753
+ }
57754
+ const passed = interpolateValue(tick.guard, createContextFromBindings(guardCtx));
57755
+ if (!passed) {
57756
+ tickLog.debug("guard-blocked", { traitName, tick: tick.name, state: currentState });
57757
+ return;
57758
+ }
57759
+ }
57760
+ void executeTransitionEffects({
57761
+ binding,
57762
+ effects: tick.effects,
57763
+ previousState: currentState,
57764
+ newState: currentState,
57765
+ flushEvent: `tick:${tick.name}`,
57766
+ syncOnly: true,
57767
+ log: tickLog
57768
+ });
57769
+ }, [executeTransitionEffects]);
57340
57770
  useEffect(() => {
57341
57771
  const hasFrameTicks = traitBindingsRef.current.some(
57342
57772
  (b) => b.trait.ticks?.some((t) => t.interval === "frame")
@@ -57447,161 +57877,17 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57447
57877
  transition: `${result.previousState} -> ${result.newState}`,
57448
57878
  effects: JSON.stringify(result.effects)
57449
57879
  }));
57450
- const linkedEntity = binding.linkedEntity || "";
57451
- const entityId = payload?.entityId;
57452
- const pendingSlots = /* @__PURE__ */ new Map();
57453
- const slotSource = {
57454
- trait: binding.trait.name,
57455
- state: result.previousState,
57456
- transition: `${result.previousState}->${result.newState}`,
57880
+ const emittedDuringExec = await executeTransitionEffects({
57881
+ binding,
57457
57882
  effects: result.effects,
57458
- traitDefinition: binding.trait
57459
- };
57460
- const clientHandlers = createClientEffectHandlers({
57461
- eventBus,
57462
- slotSetter: {
57463
- addPattern: (slot, pattern, props) => {
57464
- const existing = pendingSlots.get(slot) || [];
57465
- existing.push({ pattern, props: props || {} });
57466
- pendingSlots.set(slot, existing);
57467
- },
57468
- clearSlot: (slot) => {
57469
- pendingSlots.set(slot, []);
57470
- }
57471
- },
57472
- navigate: optionsRef.current?.navigate,
57473
- notify: optionsRef.current?.notify,
57474
- callService: optionsRef.current?.callService
57883
+ previousState: result.previousState,
57884
+ newState: result.newState,
57885
+ payload,
57886
+ flushEvent: eventKey,
57887
+ syncOnly: false,
57888
+ log: stateLog
57475
57889
  });
57476
- const persistence = optionsRef.current?.persistence;
57477
- let handlers = clientHandlers;
57478
- if (persistence) {
57479
- const sharedBindings = {
57480
- entity: payload ?? {},
57481
- payload: payload || {},
57482
- state: result.previousState
57483
- };
57484
- const sharedDeclared = collectDeclaredConfigDefaults(
57485
- binding.trait
57486
- );
57487
- const sharedCallSite = binding.config;
57488
- if (sharedDeclared || sharedCallSite) {
57489
- sharedBindings.config = {
57490
- ...sharedDeclared ?? {},
57491
- ...sharedCallSite ?? {}
57492
- };
57493
- }
57494
- const serverHandlers = createServerEffectHandlers({
57495
- persistence,
57496
- eventBus,
57497
- entityType: linkedEntity,
57498
- entityId,
57499
- bindings: sharedBindings,
57500
- context: {
57501
- traitName: binding.trait.name,
57502
- state: result.previousState,
57503
- transition: `${result.previousState}->${result.newState}`,
57504
- linkedEntity,
57505
- entityId
57506
- },
57507
- source: { trait: binding.trait.name },
57508
- callService: optionsRef.current?.callService
57509
- });
57510
- handlers = {
57511
- ...serverHandlers,
57512
- // Client handlers own UI + emit: keep the slot setter
57513
- // and pre-prefixed UI:* emit path intact.
57514
- emit: clientHandlers.emit,
57515
- renderUI: clientHandlers.renderUI,
57516
- navigate: clientHandlers.navigate,
57517
- notify: clientHandlers.notify
57518
- };
57519
- }
57520
- const baseSet = handlers.set;
57521
- handlers = {
57522
- ...handlers,
57523
- set: async (targetId, field, value) => {
57524
- let fieldState = traitFieldStatesRef.current.get(traitName);
57525
- if (!fieldState) {
57526
- fieldState = {};
57527
- traitFieldStatesRef.current.set(traitName, fieldState);
57528
- }
57529
- fieldState[field] = value;
57530
- if (baseSet) await baseSet(targetId, field, value);
57531
- }
57532
- };
57533
- const entityForBinding = traitFieldStatesRef.current.get(traitName) ?? {};
57534
- const bindingCtx = {
57535
- entity: entityForBinding,
57536
- payload: payload || {},
57537
- state: result.previousState
57538
- };
57539
- const declaredDefaults = collectDeclaredConfigDefaults(
57540
- binding.trait
57541
- );
57542
- const callSiteConfig = binding.config;
57543
- if (declaredDefaults || callSiteConfig) {
57544
- bindingCtx.config = {
57545
- ...declaredDefaults ?? {},
57546
- ...callSiteConfig ?? {}
57547
- };
57548
- }
57549
- const effectContext = {
57550
- traitName: binding.trait.name,
57551
- state: result.previousState,
57552
- transition: `${result.previousState}->${result.newState}`,
57553
- linkedEntity,
57554
- entityId
57555
- };
57556
- const emittedDuringExec = [];
57557
57890
  emittedByTrait.set(traitName, emittedDuringExec);
57558
- const baseEmit = handlers.emit;
57559
- const trackingHandlers = {
57560
- ...handlers,
57561
- emit: (event, eventPayload, source) => {
57562
- emittedDuringExec.push(event);
57563
- baseEmit(event, eventPayload, source);
57564
- }
57565
- };
57566
- const executor = new EffectExecutor({ handlers: trackingHandlers, bindings: bindingCtx, context: effectContext });
57567
- try {
57568
- await executor.executeAll(result.effects);
57569
- stateLog.debug("transition:render-ui-dispatched", () => ({
57570
- traitName,
57571
- fromState: result.previousState,
57572
- toState: result.newState,
57573
- event: eventKey,
57574
- slotsTouched: Array.from(pendingSlots.keys()).join(","),
57575
- patternTypes: Array.from(pendingSlots.entries()).map(
57576
- ([slot, patterns]) => `${slot}:[${patterns.map((p2) => p2.pattern?.type ?? "null").join(",")}]`
57577
- ).join(";")
57578
- }));
57579
- void slotSource;
57580
- for (const [slot, patterns] of pendingSlots) {
57581
- stateLog.debug("flush:slot", {
57582
- traitName,
57583
- slot,
57584
- patternCount: patterns.length,
57585
- event: eventKey,
57586
- transition: `${result.previousState}->${result.newState}`,
57587
- cleared: patterns.length === 0
57588
- });
57589
- flushSlot(traitName, slot, patterns, {
57590
- event: eventKey,
57591
- state: result.previousState,
57592
- entity: binding.linkedEntity
57593
- });
57594
- }
57595
- } catch (error) {
57596
- stateLog.error("transition:effect-error", {
57597
- traitName,
57598
- fromState: result.previousState,
57599
- toState: result.newState,
57600
- event: eventKey,
57601
- error: error instanceof Error ? error.message : String(error),
57602
- effectCount: result.effects.length
57603
- });
57604
- }
57605
57891
  } else if (!result.executed) {
57606
57892
  if (result.guardResult === false) {
57607
57893
  stateLog.debug("guard-blocked-transition", {
@@ -57739,7 +58025,7 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57739
58025
  if (!orbitalName) continue;
57740
58026
  for (const transition of binding.trait.transitions) {
57741
58027
  const eventKey = transition.event;
57742
- if (eventKey === "INIT" || eventKey === "LOAD" || eventKey === "$MOUNT") {
58028
+ if (LIFECYCLE_EVENTS.includes(eventKey)) {
57743
58029
  continue;
57744
58030
  }
57745
58031
  const selfBusKey = `UI:${orbitalName}.${traitName}.${eventKey}`;
@@ -57794,6 +58080,24 @@ function useTraitStateMachine(traitBindings, uiSlots, options) {
57794
58080
  crossTraitLog.debug("cleanup:done", {});
57795
58081
  };
57796
58082
  }, [traitBindings, eventBus, enqueueAndDrain]);
58083
+ useEffect(() => {
58084
+ const mgr = managerRef.current;
58085
+ const inited = initedTraitsRef.current;
58086
+ for (const binding of traitBindings) {
58087
+ const traitName = binding.trait.name;
58088
+ if (inited.has(traitName)) continue;
58089
+ const lifecycleEvent = LIFECYCLE_EVENTS.find(
58090
+ (evt) => mgr.canHandleEvent(traitName, evt)
58091
+ );
58092
+ if (lifecycleEvent === void 0) continue;
58093
+ inited.add(traitName);
58094
+ stateLog.debug("mount:fire-lifecycle", { traitName, event: lifecycleEvent });
58095
+ enqueueAndDrain(lifecycleEvent, {});
58096
+ }
58097
+ return () => {
58098
+ initedTraitsRef.current = /* @__PURE__ */ new Set();
58099
+ };
58100
+ }, [traitBindings, enqueueAndDrain]);
57797
58101
  return {
57798
58102
  traitStates,
57799
58103
  sendEvent,